From 85b7bc695629926641c7cb752fd478adfdf374f3 Mon Sep 17 00:00:00 2001
From: Marius <mariausol@gmail.com>
Date: Sun, 4 Jul 2010 15:32:09 +0300
Subject: stable 2010-05-24 13:10

---
 scripts/context/lua/luatools.lua                |  8185 ++++++++++++++
 scripts/context/lua/luatools.rme                |     3 +
 scripts/context/lua/mtx-babel.lua               |   430 +
 scripts/context/lua/mtx-cache.lua               |    96 +
 scripts/context/lua/mtx-chars.lua               |   322 +
 scripts/context/lua/mtx-check.lua               |   143 +
 scripts/context/lua/mtx-context.lua             |  1554 +++
 scripts/context/lua/mtx-convert.lua             |   139 +
 scripts/context/lua/mtx-fonts.lua               |   345 +
 scripts/context/lua/mtx-grep.lua                |   114 +
 scripts/context/lua/mtx-interface.lua           |   274 +
 scripts/context/lua/mtx-metatex.lua             |    69 +
 scripts/context/lua/mtx-modules.lua             |   167 +
 scripts/context/lua/mtx-mptopdf.lua             |   127 +
 scripts/context/lua/mtx-mtxworks.lua            |    14 +
 scripts/context/lua/mtx-package.lua             |    68 +
 scripts/context/lua/mtx-patterns.lua            |   366 +
 scripts/context/lua/mtx-profile.lua             |   170 +
 scripts/context/lua/mtx-scite.lua               |   166 +
 scripts/context/lua/mtx-server-ctx-fonttest.lua |   733 ++
 scripts/context/lua/mtx-server-ctx-help.lua     |   665 ++
 scripts/context/lua/mtx-server-ctx-startup.lua  |    39 +
 scripts/context/lua/mtx-server.lua              |   361 +
 scripts/context/lua/mtx-texworks.lua            |    99 +
 scripts/context/lua/mtx-timing.lua              |   193 +
 scripts/context/lua/mtx-tools.lua               |   176 +
 scripts/context/lua/mtx-unzip.lua               |   101 +
 scripts/context/lua/mtx-update.lua              |   580 +
 scripts/context/lua/mtx-watch.lua               |   382 +
 scripts/context/lua/mtxrun.lua                  | 12639 ++++++++++++++++++++++
 scripts/context/lua/mtxrun.rme                  |    18 +
 scripts/context/lua/x-ldx.lua                   |   322 +
 scripts/context/perl/makempy.pl                 |   361 +
 scripts/context/perl/mptopdf.pl                 |   160 +
 scripts/context/perl/path_tre.pm                |    36 +
 scripts/context/perl/pdftrimwhite.pl            |   525 +
 scripts/context/perl/texfind.pl                 |   270 +
 scripts/context/perl/texfont.pl                 |  1373 +++
 scripts/context/ruby/base/ctx.rb                |   462 +
 scripts/context/ruby/base/exa.rb                |   407 +
 scripts/context/ruby/base/file.rb               |   150 +
 scripts/context/ruby/base/kpse.rb               |   389 +
 scripts/context/ruby/base/kpse/drb.rb           |    57 +
 scripts/context/ruby/base/kpse/soap.rb          |    79 +
 scripts/context/ruby/base/kpse/trees.rb         |    84 +
 scripts/context/ruby/base/kpsedirect.rb         |    34 +
 scripts/context/ruby/base/kpsefast.rb           |   934 ++
 scripts/context/ruby/base/kpseremote.rb         |   116 +
 scripts/context/ruby/base/kpserunner.rb         |    87 +
 scripts/context/ruby/base/logger.rb             |   104 +
 scripts/context/ruby/base/merge.rb              |   139 +
 scripts/context/ruby/base/mp.rb                 |   167 +
 scripts/context/ruby/base/pdf.rb                |    75 +
 scripts/context/ruby/base/state.rb              |    75 +
 scripts/context/ruby/base/switch.rb             |   635 ++
 scripts/context/ruby/base/system.rb             |   121 +
 scripts/context/ruby/base/tex.rb                |  2299 ++++
 scripts/context/ruby/base/texutil.rb            |  1097 ++
 scripts/context/ruby/base/tool.rb               |   291 +
 scripts/context/ruby/base/variables.rb          |   132 +
 scripts/context/ruby/concheck.rb                |   471 +
 scripts/context/ruby/ctxtools.rb                |  2785 +++++
 scripts/context/ruby/fcd_start.rb               |   472 +
 scripts/context/ruby/graphics/gs.rb             |   684 ++
 scripts/context/ruby/graphics/inkscape.rb       |   112 +
 scripts/context/ruby/graphics/magick.rb         |   161 +
 scripts/context/ruby/imgtopdf.rb                |    86 +
 scripts/context/ruby/mpstools.rb                |     7 +
 scripts/context/ruby/mtxtools.rb                |   475 +
 scripts/context/ruby/pdftools.rb                |   861 ++
 scripts/context/ruby/pstopdf.rb                 |   533 +
 scripts/context/ruby/rlxtools.rb                |   368 +
 scripts/context/ruby/rscortool.rb               |    63 +
 scripts/context/ruby/rsfiltool.rb               |   341 +
 scripts/context/ruby/rslibtool.rb               |   114 +
 scripts/context/ruby/runtools.rb                |   506 +
 scripts/context/ruby/texexec.rb                 |   792 ++
 scripts/context/ruby/texmfstart.rb              |  1277 +++
 scripts/context/ruby/texsync.rb                 |   206 +
 scripts/context/ruby/textools.rb                |  1033 ++
 scripts/context/ruby/texutil.rb                 |    93 +
 scripts/context/ruby/tmftools.rb                |   165 +
 scripts/context/ruby/xmltools.rb                |   656 ++
 scripts/context/stubs/mswin/context.exe         |   Bin 0 -> 6144 bytes
 scripts/context/stubs/mswin/luatools.exe        |   Bin 0 -> 6144 bytes
 scripts/context/stubs/mswin/luatools.lua        |  8185 ++++++++++++++
 scripts/context/stubs/mswin/metatex.exe         |   Bin 0 -> 6144 bytes
 scripts/context/stubs/mswin/mtxrun.dll          |   Bin 0 -> 9216 bytes
 scripts/context/stubs/mswin/mtxrun.exe          |   Bin 0 -> 6144 bytes
 scripts/context/stubs/mswin/mtxrun.lua          | 12639 ++++++++++++++++++++++
 scripts/context/stubs/mswin/texexec.exe         |   Bin 0 -> 6144 bytes
 scripts/context/stubs/mswin/texmfstart.exe      |   Bin 0 -> 6144 bytes
 scripts/context/stubs/source/mtxrun_dll.c       |   221 +
 scripts/context/stubs/source/mtxrun_exe.c       |     8 +
 scripts/context/stubs/source/readme.txt         |    36 +
 scripts/context/stubs/unix/context              |     2 +
 scripts/context/stubs/unix/luatools             |  8185 ++++++++++++++
 scripts/context/stubs/unix/metatex              |     2 +
 scripts/context/stubs/unix/mtxrun               | 12639 ++++++++++++++++++++++
 scripts/context/stubs/unix/texexec              |     2 +
 scripts/context/stubs/unix/texmfstart           |     2 +
 101 files changed, 93901 insertions(+)
 create mode 100644 scripts/context/lua/luatools.lua
 create mode 100644 scripts/context/lua/luatools.rme
 create mode 100644 scripts/context/lua/mtx-babel.lua
 create mode 100644 scripts/context/lua/mtx-cache.lua
 create mode 100644 scripts/context/lua/mtx-chars.lua
 create mode 100644 scripts/context/lua/mtx-check.lua
 create mode 100644 scripts/context/lua/mtx-context.lua
 create mode 100644 scripts/context/lua/mtx-convert.lua
 create mode 100644 scripts/context/lua/mtx-fonts.lua
 create mode 100644 scripts/context/lua/mtx-grep.lua
 create mode 100644 scripts/context/lua/mtx-interface.lua
 create mode 100644 scripts/context/lua/mtx-metatex.lua
 create mode 100644 scripts/context/lua/mtx-modules.lua
 create mode 100644 scripts/context/lua/mtx-mptopdf.lua
 create mode 100644 scripts/context/lua/mtx-mtxworks.lua
 create mode 100644 scripts/context/lua/mtx-package.lua
 create mode 100644 scripts/context/lua/mtx-patterns.lua
 create mode 100644 scripts/context/lua/mtx-profile.lua
 create mode 100644 scripts/context/lua/mtx-scite.lua
 create mode 100644 scripts/context/lua/mtx-server-ctx-fonttest.lua
 create mode 100644 scripts/context/lua/mtx-server-ctx-help.lua
 create mode 100644 scripts/context/lua/mtx-server-ctx-startup.lua
 create mode 100644 scripts/context/lua/mtx-server.lua
 create mode 100644 scripts/context/lua/mtx-texworks.lua
 create mode 100644 scripts/context/lua/mtx-timing.lua
 create mode 100644 scripts/context/lua/mtx-tools.lua
 create mode 100644 scripts/context/lua/mtx-unzip.lua
 create mode 100644 scripts/context/lua/mtx-update.lua
 create mode 100644 scripts/context/lua/mtx-watch.lua
 create mode 100644 scripts/context/lua/mtxrun.lua
 create mode 100644 scripts/context/lua/mtxrun.rme
 create mode 100644 scripts/context/lua/x-ldx.lua
 create mode 100644 scripts/context/perl/makempy.pl
 create mode 100644 scripts/context/perl/mptopdf.pl
 create mode 100644 scripts/context/perl/path_tre.pm
 create mode 100644 scripts/context/perl/pdftrimwhite.pl
 create mode 100644 scripts/context/perl/texfind.pl
 create mode 100644 scripts/context/perl/texfont.pl
 create mode 100644 scripts/context/ruby/base/ctx.rb
 create mode 100644 scripts/context/ruby/base/exa.rb
 create mode 100644 scripts/context/ruby/base/file.rb
 create mode 100644 scripts/context/ruby/base/kpse.rb
 create mode 100644 scripts/context/ruby/base/kpse/drb.rb
 create mode 100644 scripts/context/ruby/base/kpse/soap.rb
 create mode 100644 scripts/context/ruby/base/kpse/trees.rb
 create mode 100644 scripts/context/ruby/base/kpsedirect.rb
 create mode 100644 scripts/context/ruby/base/kpsefast.rb
 create mode 100644 scripts/context/ruby/base/kpseremote.rb
 create mode 100644 scripts/context/ruby/base/kpserunner.rb
 create mode 100644 scripts/context/ruby/base/logger.rb
 create mode 100644 scripts/context/ruby/base/merge.rb
 create mode 100644 scripts/context/ruby/base/mp.rb
 create mode 100644 scripts/context/ruby/base/pdf.rb
 create mode 100644 scripts/context/ruby/base/state.rb
 create mode 100644 scripts/context/ruby/base/switch.rb
 create mode 100644 scripts/context/ruby/base/system.rb
 create mode 100644 scripts/context/ruby/base/tex.rb
 create mode 100644 scripts/context/ruby/base/texutil.rb
 create mode 100644 scripts/context/ruby/base/tool.rb
 create mode 100644 scripts/context/ruby/base/variables.rb
 create mode 100644 scripts/context/ruby/concheck.rb
 create mode 100644 scripts/context/ruby/ctxtools.rb
 create mode 100644 scripts/context/ruby/fcd_start.rb
 create mode 100644 scripts/context/ruby/graphics/gs.rb
 create mode 100644 scripts/context/ruby/graphics/inkscape.rb
 create mode 100644 scripts/context/ruby/graphics/magick.rb
 create mode 100644 scripts/context/ruby/imgtopdf.rb
 create mode 100644 scripts/context/ruby/mpstools.rb
 create mode 100644 scripts/context/ruby/mtxtools.rb
 create mode 100644 scripts/context/ruby/pdftools.rb
 create mode 100644 scripts/context/ruby/pstopdf.rb
 create mode 100644 scripts/context/ruby/rlxtools.rb
 create mode 100644 scripts/context/ruby/rscortool.rb
 create mode 100644 scripts/context/ruby/rsfiltool.rb
 create mode 100644 scripts/context/ruby/rslibtool.rb
 create mode 100644 scripts/context/ruby/runtools.rb
 create mode 100644 scripts/context/ruby/texexec.rb
 create mode 100644 scripts/context/ruby/texmfstart.rb
 create mode 100644 scripts/context/ruby/texsync.rb
 create mode 100644 scripts/context/ruby/textools.rb
 create mode 100644 scripts/context/ruby/texutil.rb
 create mode 100644 scripts/context/ruby/tmftools.rb
 create mode 100644 scripts/context/ruby/xmltools.rb
 create mode 100644 scripts/context/stubs/mswin/context.exe
 create mode 100644 scripts/context/stubs/mswin/luatools.exe
 create mode 100644 scripts/context/stubs/mswin/luatools.lua
 create mode 100644 scripts/context/stubs/mswin/metatex.exe
 create mode 100644 scripts/context/stubs/mswin/mtxrun.dll
 create mode 100644 scripts/context/stubs/mswin/mtxrun.exe
 create mode 100644 scripts/context/stubs/mswin/mtxrun.lua
 create mode 100644 scripts/context/stubs/mswin/texexec.exe
 create mode 100644 scripts/context/stubs/mswin/texmfstart.exe
 create mode 100644 scripts/context/stubs/source/mtxrun_dll.c
 create mode 100644 scripts/context/stubs/source/mtxrun_exe.c
 create mode 100644 scripts/context/stubs/source/readme.txt
 create mode 100644 scripts/context/stubs/unix/context
 create mode 100644 scripts/context/stubs/unix/luatools
 create mode 100644 scripts/context/stubs/unix/metatex
 create mode 100644 scripts/context/stubs/unix/mtxrun
 create mode 100644 scripts/context/stubs/unix/texexec
 create mode 100644 scripts/context/stubs/unix/texmfstart

(limited to 'scripts')

diff --git a/scripts/context/lua/luatools.lua b/scripts/context/lua/luatools.lua
new file mode 100644
index 000000000..1d87322c1
--- /dev/null
+++ b/scripts/context/lua/luatools.lua
@@ -0,0 +1,8185 @@
+#!/usr/bin/env texlua
+
+if not modules then modules = { } end modules ['luatools'] = {
+    version   = 1.001,
+    comment   = "companion to context.tex",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format = string.format
+
+-- one can make a stub:
+--
+-- #!/bin/sh
+-- env LUATEXDIR=/....../texmf/scripts/context/lua texlua luatools.lua "$@"
+
+-- Although this script is part of the ConTeXt distribution it is
+-- relatively indepent of ConTeXt.  The same is true for some of
+-- the luat files. We may may make them even less dependent in
+-- the future. As long as Luatex is under development the
+-- interfaces and names of functions may change.
+
+-- For the sake of independence we optionally can merge the library
+-- code here. It's too much code, but that does not harm. Much of the
+-- library code is used elsewhere. We don't want dependencies on
+-- Lua library paths simply because these scripts are located in the
+-- texmf tree and not in some Lua path. Normally this merge is not
+-- needed when texmfstart is used, or when the proper stub is used or
+-- when (windows) suffix binding is active.
+
+texlua = true
+
+-- begin library merge
+
+
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-string'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local sub, gsub, find, match, gmatch, format, char, byte, rep, lower = string.sub, string.gsub, string.find, string.match, string.gmatch, string.format, string.char, string.byte, string.rep, string.lower
+local lpegmatch = lpeg.match
+
+-- some functions may disappear as they are not used anywhere
+
+if not string.split then
+
+    -- this will be overloaded by a faster lpeg variant
+
+    function string:split(pattern)
+        if #self > 0 then
+            local t = { }
+            for s in gmatch(self..pattern,"(.-)"..pattern) do
+                t[#t+1] = s
+            end
+            return t
+        else
+            return { }
+        end
+    end
+
+end
+
+local chr_to_esc = {
+    ["%"] = "%%",
+    ["."] = "%.",
+    ["+"] = "%+", ["-"] = "%-", ["*"] = "%*",
+    ["^"] = "%^", ["$"] = "%$",
+    ["["] = "%[", ["]"] = "%]",
+    ["("] = "%(", [")"] = "%)",
+    ["{"] = "%{", ["}"] = "%}"
+}
+
+string.chr_to_esc = chr_to_esc
+
+function string:esc() -- variant 2
+    return (gsub(self,"(.)",chr_to_esc))
+end
+
+function string:unquote()
+    return (gsub(self,"^([\"\'])(.*)%1$","%2"))
+end
+
+--~ function string:unquote()
+--~     if find(self,"^[\'\"]") then
+--~         return sub(self,2,-2)
+--~     else
+--~         return self
+--~     end
+--~ end
+
+function string:quote() -- we could use format("%q")
+    return format("%q",self)
+end
+
+function string:count(pattern) -- variant 3
+    local n = 0
+    for _ in gmatch(self,pattern) do
+        n = n + 1
+    end
+    return n
+end
+
+function string:limit(n,sentinel)
+    if #self > n then
+        sentinel = sentinel or " ..."
+        return sub(self,1,(n-#sentinel)) .. sentinel
+    else
+        return self
+    end
+end
+
+--~ function string:strip() -- the .- is quite efficient
+--~  -- return match(self,"^%s*(.-)%s*$") or ""
+--~  -- return match(self,'^%s*(.*%S)') or '' -- posted on lua list
+--~     return find(s,'^%s*$') and '' or match(s,'^%s*(.*%S)')
+--~ end
+
+do -- roberto's variant:
+    local space    = lpeg.S(" \t\v\n")
+    local nospace  = 1 - space
+    local stripper = space^0 * lpeg.C((space^0 * nospace^1)^0)
+    function string.strip(str)
+        return lpegmatch(stripper,str) or ""
+    end
+end
+
+function string:is_empty()
+    return not find(self,"%S")
+end
+
+function string:enhance(pattern,action)
+    local ok, n = true, 0
+    while ok do
+        ok = false
+        self = gsub(self,pattern, function(...)
+            ok, n = true, n + 1
+            return action(...)
+        end)
+    end
+    return self, n
+end
+
+local chr_to_hex, hex_to_chr = { }, { }
+
+for i=0,255 do
+    local c, h = char(i), format("%02X",i)
+    chr_to_hex[c], hex_to_chr[h] = h, c
+end
+
+function string:to_hex()
+    return (gsub(self or "","(.)",chr_to_hex))
+end
+
+function string:from_hex()
+    return (gsub(self or "","(..)",hex_to_chr))
+end
+
+if not string.characters then
+
+    local function nextchar(str, index)
+        index = index + 1
+        return (index <= #str) and index or nil, sub(str,index,index)
+    end
+    function string:characters()
+        return nextchar, self, 0
+    end
+    local function nextbyte(str, index)
+        index = index + 1
+        return (index <= #str) and index or nil, byte(sub(str,index,index))
+    end
+    function string:bytes()
+        return nextbyte, self, 0
+    end
+
+end
+
+-- we can use format for this (neg n)
+
+function string:rpadd(n,chr)
+    local m = n-#self
+    if m > 0 then
+        return self .. rep(chr or " ",m)
+    else
+        return self
+    end
+end
+
+function string:lpadd(n,chr)
+    local m = n-#self
+    if m > 0 then
+        return rep(chr or " ",m) .. self
+    else
+        return self
+    end
+end
+
+string.padd = string.rpadd
+
+function is_number(str) -- tonumber
+    return find(str,"^[%-%+]?[%d]-%.?[%d+]$") == 1
+end
+
+--~ print(is_number("1"))
+--~ print(is_number("1.1"))
+--~ print(is_number(".1"))
+--~ print(is_number("-0.1"))
+--~ print(is_number("+0.1"))
+--~ print(is_number("-.1"))
+--~ print(is_number("+.1"))
+
+function string:split_settings() -- no {} handling, see l-aux for lpeg variant
+    if find(self,"=") then
+        local t = { }
+        for k,v in gmatch(self,"(%a+)=([^%,]*)") do
+            t[k] = v
+        end
+        return t
+    else
+        return nil
+    end
+end
+
+local patterns_escapes = {
+    ["-"] = "%-",
+    ["."] = "%.",
+    ["+"] = "%+",
+    ["*"] = "%*",
+    ["%"] = "%%",
+    ["("] = "%)",
+    [")"] = "%)",
+    ["["] = "%[",
+    ["]"] = "%]",
+}
+
+function string:pattesc()
+    return (gsub(self,".",patterns_escapes))
+end
+
+local simple_escapes = {
+    ["-"] = "%-",
+    ["."] = "%.",
+    ["?"] = ".",
+    ["*"] = ".*",
+}
+
+function string:simpleesc()
+    return (gsub(self,".",simple_escapes))
+end
+
+function string:tohash()
+    local t = { }
+    for s in gmatch(self,"([^, ]+)") do -- lpeg
+        t[s] = true
+    end
+    return t
+end
+
+local pattern = lpeg.Ct(lpeg.C(1)^0)
+
+function string:totable()
+    return lpegmatch(pattern,self)
+end
+
+--~ local t = {
+--~     "1234567123456712345671234567",
+--~     "a\tb\tc",
+--~     "aa\tbb\tcc",
+--~     "aaa\tbbb\tccc",
+--~     "aaaa\tbbbb\tcccc",
+--~     "aaaaa\tbbbbb\tccccc",
+--~     "aaaaaa\tbbbbbb\tcccccc",
+--~ }
+--~ for k,v do
+--~     print(string.tabtospace(t[k]))
+--~ end
+
+function string.tabtospace(str,tab)
+    -- we don't handle embedded newlines
+    while true do
+        local s = find(str,"\t")
+        if s then
+            if not tab then tab = 7 end -- only when found
+            local d = tab-(s-1) % tab
+            if d > 0 then
+                str = gsub(str,"\t",rep(" ",d),1)
+            else
+                str = gsub(str,"\t","",1)
+            end
+        else
+            break
+        end
+    end
+    return str
+end
+
+function string:compactlong() -- strips newlines and leading spaces
+    self = gsub(self,"[\n\r]+ *","")
+    self = gsub(self,"^ *","")
+    return self
+end
+
+function string:striplong() -- strips newlines and leading spaces
+    self = gsub(self,"^%s*","")
+    self = gsub(self,"[\n\r]+ *","\n")
+    return self
+end
+
+function string:topattern(lowercase,strict)
+    if lowercase then
+        self = lower(self)
+    end
+    self = gsub(self,".",simple_escapes)
+    if self == "" then
+        self = ".*"
+    elseif strict then
+        self = "^" .. self .. "$"
+    end
+    return self
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-lpeg'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local lpeg = require("lpeg")
+
+lpeg.patterns  = lpeg.patterns or { } -- so that we can share
+local patterns = lpeg.patterns
+
+local P, R, S, Ct, C, Cs, Cc, V = lpeg.P, lpeg.R, lpeg.S, lpeg.Ct, lpeg.C, lpeg.Cs, lpeg.Cc, lpeg.V
+local match = lpeg.match
+
+local digit, sign      = R('09'), S('+-')
+local cr, lf, crlf     = P("\r"), P("\n"), P("\r\n")
+local utf8byte         = R("\128\191")
+
+patterns.utf8byte      = utf8byte
+patterns.utf8one       = R("\000\127")
+patterns.utf8two       = R("\194\223") * utf8byte
+patterns.utf8three     = R("\224\239") * utf8byte * utf8byte
+patterns.utf8four      = R("\240\244") * utf8byte * utf8byte * utf8byte
+
+patterns.digit         = digit
+patterns.sign          = sign
+patterns.cardinal      = sign^0 * digit^1
+patterns.integer       = sign^0 * digit^1
+patterns.float         = sign^0 * digit^0 * P('.') * digit^1
+patterns.number        = patterns.float + patterns.integer
+patterns.oct           = P("0") * R("07")^1
+patterns.octal         = patterns.oct
+patterns.HEX           = P("0x") * R("09","AF")^1
+patterns.hex           = P("0x") * R("09","af")^1
+patterns.hexadecimal   = P("0x") * R("09","AF","af")^1
+patterns.lowercase     = R("az")
+patterns.uppercase     = R("AZ")
+patterns.letter        = patterns.lowercase + patterns.uppercase
+patterns.space         = S(" ")
+patterns.eol           = S("\n\r")
+patterns.spacer        = S(" \t\f\v")  -- + string.char(0xc2, 0xa0) if we want utf (cf mail roberto)
+patterns.newline       = crlf + cr + lf
+patterns.nonspace      = 1 - patterns.space
+patterns.nonspacer     = 1 - patterns.spacer
+patterns.whitespace    = patterns.eol + patterns.spacer
+patterns.nonwhitespace = 1 - patterns.whitespace
+patterns.utf8          = patterns.utf8one + patterns.utf8two + patterns.utf8three + patterns.utf8four
+patterns.utfbom        = P('\000\000\254\255') + P('\255\254\000\000') + P('\255\254') + P('\254\255') + P('\239\187\191')
+
+function lpeg.anywhere(pattern) --slightly adapted from website
+    return P { P(pattern) + 1 * V(1) } -- why so complex?
+end
+
+function lpeg.splitter(pattern, action)
+    return (((1-P(pattern))^1)/action+1)^0
+end
+
+local spacing  = patterns.spacer^0 * patterns.newline -- sort of strip
+local empty    = spacing * Cc("")
+local nonempty = Cs((1-spacing)^1) * spacing^-1
+local content  = (empty + nonempty)^1
+
+local capture = Ct(content^0)
+
+function string:splitlines()
+    return match(capture,self)
+end
+
+patterns.textline = content
+
+--~ local p = lpeg.splitat("->",false)  print(match(p,"oeps->what->more"))  -- oeps what more
+--~ local p = lpeg.splitat("->",true)   print(match(p,"oeps->what->more"))  -- oeps what->more
+--~ local p = lpeg.splitat("->",false)  print(match(p,"oeps"))              -- oeps
+--~ local p = lpeg.splitat("->",true)   print(match(p,"oeps"))              -- oeps
+
+local splitters_s, splitters_m = { }, { }
+
+local function splitat(separator,single)
+    local splitter = (single and splitters_s[separator]) or splitters_m[separator]
+    if not splitter then
+        separator = P(separator)
+        if single then
+            local other, any = C((1 - separator)^0), P(1)
+            splitter = other * (separator * C(any^0) + "") -- ?
+            splitters_s[separator] = splitter
+        else
+            local other = C((1 - separator)^0)
+            splitter = other * (separator * other)^0
+            splitters_m[separator] = splitter
+        end
+    end
+    return splitter
+end
+
+lpeg.splitat = splitat
+
+local cache = { }
+
+function lpeg.split(separator,str)
+    local c = cache[separator]
+    if not c then
+        c = Ct(splitat(separator))
+        cache[separator] = c
+    end
+    return match(c,str)
+end
+
+function string:split(separator)
+    local c = cache[separator]
+    if not c then
+        c = Ct(splitat(separator))
+        cache[separator] = c
+    end
+    return match(c,self)
+end
+
+lpeg.splitters = cache
+
+local cache = { }
+
+function lpeg.checkedsplit(separator,str)
+    local c = cache[separator]
+    if not c then
+        separator = P(separator)
+        local other = C((1 - separator)^0)
+        c = Ct(separator^0 * other * (separator^1 * other)^0)
+        cache[separator] = c
+    end
+    return match(c,str)
+end
+
+function string:checkedsplit(separator)
+    local c = cache[separator]
+    if not c then
+        separator = P(separator)
+        local other = C((1 - separator)^0)
+        c = Ct(separator^0 * other * (separator^1 * other)^0)
+        cache[separator] = c
+    end
+    return match(c,self)
+end
+
+--~ function lpeg.append(list,pp)
+--~     local p = pp
+--~     for l=1,#list do
+--~         if p then
+--~             p = p + P(list[l])
+--~         else
+--~             p = P(list[l])
+--~         end
+--~     end
+--~     return p
+--~ end
+
+--~ from roberto's site:
+
+local f1 = string.byte
+
+local function f2(s) local c1, c2         = f1(s,1,2) return   c1 * 64 + c2                       -    12416 end
+local function f3(s) local c1, c2, c3     = f1(s,1,3) return  (c1 * 64 + c2) * 64 + c3            -   925824 end
+local function f4(s) local c1, c2, c3, c4 = f1(s,1,4) return ((c1 * 64 + c2) * 64 + c3) * 64 + c4 - 63447168 end
+
+patterns.utf8byte = patterns.utf8one/f1 + patterns.utf8two/f2 + patterns.utf8three/f3 + patterns.utf8four/f4
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-table'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+table.join = table.concat
+
+local concat, sort, insert, remove = table.concat, table.sort, table.insert, table.remove
+local format, find, gsub, lower, dump, match = string.format, string.find, string.gsub, string.lower, string.dump, string.match
+local getmetatable, setmetatable = getmetatable, setmetatable
+local type, next, tostring, tonumber, ipairs = type, next, tostring, tonumber, ipairs
+local unpack = unpack or table.unpack
+
+function table.strip(tab)
+    local lst = { }
+    for i=1,#tab do
+        local s = gsub(tab[i],"^%s*(.-)%s*$","%1")
+        if s == "" then
+            -- skip this one
+        else
+            lst[#lst+1] = s
+        end
+    end
+    return lst
+end
+
+function table.keys(t)
+    local k = { }
+    for key, _ in next, t do
+        k[#k+1] = key
+    end
+    return k
+end
+
+local function compare(a,b)
+    return (tostring(a) < tostring(b))
+end
+
+local function sortedkeys(tab)
+    local srt, kind = { }, 0 -- 0=unknown 1=string, 2=number 3=mixed
+    for key,_ in next, tab do
+        srt[#srt+1] = key
+        if kind == 3 then
+            -- no further check
+        else
+            local tkey = type(key)
+            if tkey == "string" then
+            --  if kind == 2 then kind = 3 else kind = 1 end
+                kind = (kind == 2 and 3) or 1
+            elseif tkey == "number" then
+            --  if kind == 1 then kind = 3 else kind = 2 end
+                kind = (kind == 1 and 3) or 2
+            else
+                kind = 3
+            end
+        end
+    end
+    if kind == 0 or kind == 3 then
+        sort(srt,compare)
+    else
+        sort(srt)
+    end
+    return srt
+end
+
+local function sortedhashkeys(tab) -- fast one
+    local srt = { }
+    for key,_ in next, tab do
+        srt[#srt+1] = key
+    end
+    sort(srt)
+    return srt
+end
+
+table.sortedkeys     = sortedkeys
+table.sortedhashkeys = sortedhashkeys
+
+function table.sortedhash(t)
+    local s = sortedhashkeys(t) -- maybe just sortedkeys
+    local n = 0
+    local function kv(s)
+        n = n + 1
+        local k = s[n]
+        return k, t[k]
+    end
+    return kv, s
+end
+
+table.sortedpairs = table.sortedhash
+
+function table.append(t, list)
+    for _,v in next, list do
+        insert(t,v)
+    end
+end
+
+function table.prepend(t, list)
+    for k,v in next, list do
+        insert(t,k,v)
+    end
+end
+
+function table.merge(t, ...) -- first one is target
+    t = t or {}
+    local lst = {...}
+    for i=1,#lst do
+        for k, v in next, lst[i] do
+            t[k] = v
+        end
+    end
+    return t
+end
+
+function table.merged(...)
+    local tmp, lst = { }, {...}
+    for i=1,#lst do
+        for k, v in next, lst[i] do
+            tmp[k] = v
+        end
+    end
+    return tmp
+end
+
+function table.imerge(t, ...)
+    local lst = {...}
+    for i=1,#lst do
+        local nst = lst[i]
+        for j=1,#nst do
+            t[#t+1] = nst[j]
+        end
+    end
+    return t
+end
+
+function table.imerged(...)
+    local tmp, lst = { }, {...}
+    for i=1,#lst do
+        local nst = lst[i]
+        for j=1,#nst do
+            tmp[#tmp+1] = nst[j]
+        end
+    end
+    return tmp
+end
+
+local function fastcopy(old) -- fast one
+    if old then
+        local new = { }
+        for k,v in next, old do
+            if type(v) == "table" then
+                new[k] = fastcopy(v) -- was just table.copy
+            else
+                new[k] = v
+            end
+        end
+        -- optional second arg
+        local mt = getmetatable(old)
+        if mt then
+            setmetatable(new,mt)
+        end
+        return new
+    else
+        return { }
+    end
+end
+
+local function copy(t, tables) -- taken from lua wiki, slightly adapted
+    tables = tables or { }
+    local tcopy = {}
+    if not tables[t] then
+        tables[t] = tcopy
+    end
+    for i,v in next, t do -- brrr, what happens with sparse indexed
+        if type(i) == "table" then
+            if tables[i] then
+                i = tables[i]
+            else
+                i = copy(i, tables)
+            end
+        end
+        if type(v) ~= "table" then
+            tcopy[i] = v
+        elseif tables[v] then
+            tcopy[i] = tables[v]
+        else
+            tcopy[i] = copy(v, tables)
+        end
+    end
+    local mt = getmetatable(t)
+    if mt then
+        setmetatable(tcopy,mt)
+    end
+    return tcopy
+end
+
+table.fastcopy = fastcopy
+table.copy     = copy
+
+-- rougly: copy-loop : unpack : sub == 0.9 : 0.4 : 0.45 (so in critical apps, use unpack)
+
+function table.sub(t,i,j)
+    return { unpack(t,i,j) }
+end
+
+function table.replace(a,b)
+    for k,v in next, b do
+        a[k] = v
+    end
+end
+
+-- slower than #t on indexed tables (#t only returns the size of the numerically indexed slice)
+
+function table.is_empty(t) -- obolete, use inline code instead
+    return not t or not next(t)
+end
+
+function table.one_entry(t) -- obolete, use inline code instead
+    local n = next(t)
+    return n and not next(t,n)
+end
+
+--~ function table.starts_at(t) -- obsolete, not nice
+--~     return ipairs(t,1)(t,0)
+--~ end
+
+function table.tohash(t,value)
+    local h = { }
+    if t then
+        if value == nil then value = true end
+        for _, v in next, t do -- no ipairs here
+            h[v] = value
+        end
+    end
+    return h
+end
+
+function table.fromhash(t)
+    local h = { }
+    for k, v in next, t do -- no ipairs here
+        if v then h[#h+1] = k end
+    end
+    return h
+end
+
+--~ print(table.serialize(t), "\n")
+--~ print(table.serialize(t,"name"), "\n")
+--~ print(table.serialize(t,false), "\n")
+--~ print(table.serialize(t,true), "\n")
+--~ print(table.serialize(t,"name",true), "\n")
+--~ print(table.serialize(t,"name",true,true), "\n")
+
+table.serialize_functions = true
+table.serialize_compact   = true
+table.serialize_inline    = true
+
+local noquotes, hexify, handle, reduce, compact, inline, functions
+
+local reserved = table.tohash { -- intercept a language flaw, no reserved words as key
+    'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'if',
+    'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while',
+}
+
+local function simple_table(t)
+    if #t > 0 then
+        local n = 0
+        for _,v in next, t do
+            n = n + 1
+        end
+        if n == #t then
+            local tt = { }
+            for i=1,#t do
+                local v = t[i]
+                local tv = type(v)
+                if tv == "number" then
+                    if hexify then
+                        tt[#tt+1] = format("0x%04X",v)
+                    else
+                        tt[#tt+1] = tostring(v) -- tostring not needed
+                    end
+                elseif tv == "boolean" then
+                    tt[#tt+1] = tostring(v)
+                elseif tv == "string" then
+                    tt[#tt+1] = format("%q",v)
+                else
+                    tt = nil
+                    break
+                end
+            end
+            return tt
+        end
+    end
+    return nil
+end
+
+-- Because this is a core function of mkiv I moved some function calls
+-- inline.
+--
+-- twice as fast in a test:
+--
+-- local propername = lpeg.P(lpeg.R("AZ","az","__") * lpeg.R("09","AZ","az", "__")^0 * lpeg.P(-1) )
+
+-- problem: there no good number_to_string converter with the best resolution
+
+local function do_serialize(root,name,depth,level,indexed)
+    if level > 0 then
+        depth = depth .. " "
+        if indexed then
+            handle(format("%s{",depth))
+        elseif name then
+        --~ handle(format("%s%s={",depth,key(name)))
+            if type(name) == "number" then -- or find(k,"^%d+$") then
+                if hexify then
+                    handle(format("%s[0x%04X]={",depth,name))
+                else
+                    handle(format("%s[%s]={",depth,name))
+                end
+            elseif noquotes and not reserved[name] and find(name,"^%a[%w%_]*$") then
+                handle(format("%s%s={",depth,name))
+            else
+                handle(format("%s[%q]={",depth,name))
+            end
+        else
+            handle(format("%s{",depth))
+        end
+    end
+    -- we could check for k (index) being number (cardinal)
+    if root and next(root) then
+        local first, last = nil, 0 -- #root cannot be trusted here (will be ok in 5.2 when ipairs is gone)
+        if compact then
+            -- NOT: for k=1,#root do (we need to quit at nil)
+            for k,v in ipairs(root) do -- can we use next?
+                if not first then first = k end
+                last = last + 1
+            end
+        end
+        local sk = sortedkeys(root)
+        for i=1,#sk do
+            local k = sk[i]
+            local v = root[k]
+            --~ if v == root then
+                -- circular
+            --~ else
+            local t = type(v)
+            if compact and first and type(k) == "number" and k >= first and k <= last then
+                if t == "number" then
+                    if hexify then
+                        handle(format("%s 0x%04X,",depth,v))
+                    else
+                        handle(format("%s %s,",depth,v)) -- %.99g
+                    end
+                elseif t == "string" then
+                    if reduce and tonumber(v) then
+                        handle(format("%s %s,",depth,v))
+                    else
+                        handle(format("%s %q,",depth,v))
+                    end
+                elseif t == "table" then
+                    if not next(v) then
+                        handle(format("%s {},",depth))
+                    elseif inline then -- and #t > 0
+                        local st = simple_table(v)
+                        if st then
+                            handle(format("%s { %s },",depth,concat(st,", ")))
+                        else
+                            do_serialize(v,k,depth,level+1,true)
+                        end
+                    else
+                        do_serialize(v,k,depth,level+1,true)
+                    end
+                elseif t == "boolean" then
+                    handle(format("%s %s,",depth,tostring(v)))
+                elseif t == "function" then
+                    if functions then
+                        handle(format('%s loadstring(%q),',depth,dump(v)))
+                    else
+                        handle(format('%s "function",',depth))
+                    end
+                else
+                    handle(format("%s %q,",depth,tostring(v)))
+                end
+            elseif k == "__p__" then -- parent
+                if false then
+                    handle(format("%s __p__=nil,",depth))
+                end
+            elseif t == "number" then
+            --~ if hexify then
+            --~     handle(format("%s %s=0x%04X,",depth,key(k),v))
+            --~ else
+            --~     handle(format("%s %s=%s,",depth,key(k),v)) -- %.99g
+            --~ end
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s [%s]=%s,",depth,k,v)) -- %.99g
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    if hexify then
+                        handle(format("%s %s=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s %s=%s,",depth,k,v)) -- %.99g
+                    end
+                else
+                    if hexify then
+                        handle(format("%s [%q]=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%s,",depth,k,v)) -- %.99g
+                    end
+                end
+            elseif t == "string" then
+                if reduce and tonumber(v) then
+                --~ handle(format("%s %s=%s,",depth,key(k),v))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=%s,",depth,k,v))
+                        else
+                            handle(format("%s [%s]=%s,",depth,k,v))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=%s,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%s,",depth,k,v))
+                    end
+                else
+                --~ handle(format("%s %s=%q,",depth,key(k),v))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=%q,",depth,k,v))
+                        else
+                            handle(format("%s [%s]=%q,",depth,k,v))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=%q,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%q,",depth,k,v))
+                    end
+                end
+            elseif t == "table" then
+                if not next(v) then
+                    --~ handle(format("%s %s={},",depth,key(k)))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]={},",depth,k))
+                        else
+                            handle(format("%s [%s]={},",depth,k))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s={},",depth,k))
+                    else
+                        handle(format("%s [%q]={},",depth,k))
+                    end
+                elseif inline then
+                    local st = simple_table(v)
+                    if st then
+                    --~ handle(format("%s %s={ %s },",depth,key(k),concat(st,", ")))
+                        if type(k) == "number" then -- or find(k,"^%d+$") then
+                            if hexify then
+                                handle(format("%s [0x%04X]={ %s },",depth,k,concat(st,", ")))
+                            else
+                                handle(format("%s [%s]={ %s },",depth,k,concat(st,", ")))
+                            end
+                        elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                            handle(format("%s %s={ %s },",depth,k,concat(st,", ")))
+                        else
+                            handle(format("%s [%q]={ %s },",depth,k,concat(st,", ")))
+                        end
+                    else
+                        do_serialize(v,k,depth,level+1)
+                    end
+                else
+                    do_serialize(v,k,depth,level+1)
+                end
+            elseif t == "boolean" then
+            --~ handle(format("%s %s=%s,",depth,key(k),tostring(v)))
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=%s,",depth,k,tostring(v)))
+                    else
+                        handle(format("%s [%s]=%s,",depth,k,tostring(v)))
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    handle(format("%s %s=%s,",depth,k,tostring(v)))
+                else
+                    handle(format("%s [%q]=%s,",depth,k,tostring(v)))
+                end
+            elseif t == "function" then
+                if functions then
+                    --~ handle(format('%s %s=loadstring(%q),',depth,key(k),dump(v)))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=loadstring(%q),",depth,k,dump(v)))
+                        else
+                            handle(format("%s [%s]=loadstring(%q),",depth,k,dump(v)))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=loadstring(%q),",depth,k,dump(v)))
+                    else
+                        handle(format("%s [%q]=loadstring(%q),",depth,k,dump(v)))
+                    end
+                end
+            else
+                --~ handle(format("%s %s=%q,",depth,key(k),tostring(v)))
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=%q,",depth,k,tostring(v)))
+                    else
+                        handle(format("%s [%s]=%q,",depth,k,tostring(v)))
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    handle(format("%s %s=%q,",depth,k,tostring(v)))
+                else
+                    handle(format("%s [%q]=%q,",depth,k,tostring(v)))
+                end
+            end
+            --~ end
+        end
+    end
+   if level > 0 then
+        handle(format("%s},",depth))
+    end
+end
+
+-- replacing handle by a direct t[#t+1] = ... (plus test) is not much
+-- faster (0.03 on 1.00 for zapfino.tma)
+
+local function serialize(root,name,_handle,_reduce,_noquotes,_hexify)
+    noquotes = _noquotes
+    hexify = _hexify
+    handle = _handle or print
+    reduce = _reduce or false
+    compact = table.serialize_compact
+    inline  = compact and table.serialize_inline
+    functions = table.serialize_functions
+    local tname = type(name)
+    if tname == "string" then
+        if name == "return" then
+            handle("return {")
+        else
+            handle(name .. "={")
+        end
+    elseif tname == "number" then
+        if hexify then
+            handle(format("[0x%04X]={",name))
+        else
+            handle("[" .. name .. "]={")
+        end
+    elseif tname == "boolean" then
+        if name then
+            handle("return {")
+        else
+            handle("{")
+        end
+    else
+        handle("t={")
+    end
+    if root and next(root) then
+        do_serialize(root,name,"",0,indexed)
+    end
+    handle("}")
+end
+
+--~ name:
+--~
+--~ true     : return     { }
+--~ false    :            { }
+--~ nil      : t        = { }
+--~ string   : string   = { }
+--~ 'return' : return     { }
+--~ number   : [number] = { }
+
+function table.serialize(root,name,reduce,noquotes,hexify)
+    local t = { }
+    local function flush(s)
+        t[#t+1] = s
+    end
+    serialize(root,name,flush,reduce,noquotes,hexify)
+    return concat(t,"\n")
+end
+
+function table.tohandle(handle,root,name,reduce,noquotes,hexify)
+    serialize(root,name,handle,reduce,noquotes,hexify)
+end
+
+-- sometimes tables are real use (zapfino extra pro is some 85M) in which
+-- case a stepwise serialization is nice; actually, we could consider:
+--
+-- for line in table.serializer(root,name,reduce,noquotes) do
+--    ...(line)
+-- end
+--
+-- so this is on the todo list
+
+table.tofile_maxtab = 2*1024
+
+function table.tofile(filename,root,name,reduce,noquotes,hexify)
+    local f = io.open(filename,'w')
+    if f then
+        local maxtab = table.tofile_maxtab
+        if maxtab > 1 then
+            local t = { }
+            local function flush(s)
+                t[#t+1] = s
+                if #t > maxtab then
+                    f:write(concat(t,"\n"),"\n") -- hm, write(sometable) should be nice
+                    t = { }
+                end
+            end
+            serialize(root,name,flush,reduce,noquotes,hexify)
+            f:write(concat(t,"\n"),"\n")
+        else
+            local function flush(s)
+                f:write(s,"\n")
+            end
+            serialize(root,name,flush,reduce,noquotes,hexify)
+        end
+        f:close()
+    end
+end
+
+local function flatten(t,f,complete) -- is this used? meybe a variant with next, ...
+    for i=1,#t do
+        local v = t[i]
+        if type(v) == "table" then
+            if complete or type(v[1]) == "table" then
+                flatten(v,f,complete)
+            else
+                f[#f+1] = v
+            end
+        else
+            f[#f+1] = v
+        end
+    end
+end
+
+function table.flatten(t)
+    local f = { }
+    flatten(t,f,true)
+    return f
+end
+
+function table.unnest(t) -- bad name
+    local f = { }
+    flatten(t,f,false)
+    return f
+end
+
+table.flatten_one_level = table.unnest
+
+-- a better one:
+
+local function flattened(t,f)
+    if not f then
+        f = { }
+    end
+    for k, v in next, t do
+        if type(v) == "table" then
+            flattened(v,f)
+        else
+            f[k] = v
+        end
+    end
+    return f
+end
+
+table.flattened = flattened
+
+-- the next three may disappear
+
+function table.remove_value(t,value) -- todo: n
+    if value then
+        for i=1,#t do
+            if t[i] == value then
+                remove(t,i)
+                -- remove all, so no: return
+            end
+        end
+    end
+end
+
+function table.insert_before_value(t,value,str)
+    if str then
+        if value then
+            for i=1,#t do
+                if t[i] == value then
+                    insert(t,i,str)
+                    return
+                end
+            end
+        end
+        insert(t,1,str)
+    elseif value then
+        insert(t,1,value)
+    end
+end
+
+function table.insert_after_value(t,value,str)
+    if str then
+        if value then
+            for i=1,#t do
+                if t[i] == value then
+                    insert(t,i+1,str)
+                    return
+                end
+            end
+        end
+        t[#t+1] = str
+    elseif value then
+        t[#t+1] = value
+    end
+end
+
+local function are_equal(a,b,n,m) -- indexed
+    if a and b and #a == #b then
+        n = n or 1
+        m = m or #a
+        for i=n,m do
+            local ai, bi = a[i], b[i]
+            if ai==bi then
+                -- same
+            elseif type(ai)=="table" and type(bi)=="table" then
+                if not are_equal(ai,bi) then
+                    return false
+                end
+            else
+                return false
+            end
+        end
+        return true
+    else
+        return false
+    end
+end
+
+local function identical(a,b) -- assumes same structure
+    for ka, va in next, a do
+        local vb = b[k]
+        if va == vb then
+            -- same
+        elseif type(va) == "table" and  type(vb) == "table" then
+            if not identical(va,vb) then
+                return false
+            end
+        else
+            return false
+        end
+    end
+    return true
+end
+
+table.are_equal = are_equal
+table.identical = identical
+
+-- maybe also make a combined one
+
+function table.compact(t)
+    if t then
+        for k,v in next, t do
+            if not next(v) then
+                t[k] = nil
+            end
+        end
+    end
+end
+
+function table.contains(t, v)
+    if t then
+        for i=1, #t do
+            if t[i] == v then
+                return i
+            end
+        end
+    end
+    return false
+end
+
+function table.count(t)
+    local n, e = 0, next(t)
+    while e do
+        n, e = n + 1, next(t,e)
+    end
+    return n
+end
+
+function table.swapped(t)
+    local s = { }
+    for k, v in next, t do
+        s[v] = k
+    end
+    return s
+end
+
+--~ function table.are_equal(a,b)
+--~     return table.serialize(a) == table.serialize(b)
+--~ end
+
+function table.clone(t,p) -- t is optional or nil or table
+    if not p then
+        t, p = { }, t or { }
+    elseif not t then
+        t = { }
+    end
+    setmetatable(t, { __index = function(_,key) return p[key] end }) -- why not __index = p ?
+    return t
+end
+
+function table.hexed(t,seperator)
+    local tt = { }
+    for i=1,#t do tt[i] = format("0x%04X",t[i]) end
+    return concat(tt,seperator or " ")
+end
+
+function table.reverse_hash(h)
+    local r = { }
+    for k,v in next, h do
+        r[v] = lower(gsub(k," ",""))
+    end
+    return r
+end
+
+function table.reverse(t)
+    local tt = { }
+    if #t > 0 then
+        for i=#t,1,-1 do
+            tt[#tt+1] = t[i]
+        end
+    end
+    return tt
+end
+
+function table.insert_before_value(t,value,extra)
+    for i=1,#t do
+        if t[i] == extra then
+            remove(t,i)
+        end
+    end
+    for i=1,#t do
+        if t[i] == value then
+            insert(t,i,extra)
+            return
+        end
+    end
+    insert(t,1,extra)
+end
+
+function table.insert_after_value(t,value,extra)
+    for i=1,#t do
+        if t[i] == extra then
+            remove(t,i)
+        end
+    end
+    for i=1,#t do
+        if t[i] == value then
+            insert(t,i+1,extra)
+            return
+        end
+    end
+    insert(t,#t+1,extra)
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-io'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local byte, find, gsub = string.byte, string.find, string.gsub
+
+if string.find(os.getenv("PATH"),";") then
+    io.fileseparator, io.pathseparator = "\\", ";"
+else
+    io.fileseparator, io.pathseparator = "/" , ":"
+end
+
+function io.loaddata(filename,textmode)
+    local f = io.open(filename,(textmode and 'r') or 'rb')
+    if f then
+    --  collectgarbage("step") -- sometimes makes a big difference in mem consumption
+        local data = f:read('*all')
+    --  garbagecollector.check(data)
+        f:close()
+        return data
+    else
+        return nil
+    end
+end
+
+function io.savedata(filename,data,joiner)
+    local f = io.open(filename,"wb")
+    if f then
+        if type(data) == "table" then
+            f:write(table.join(data,joiner or ""))
+        elseif type(data) == "function" then
+            data(f)
+        else
+            f:write(data or "")
+        end
+        f:close()
+        return true
+    else
+        return false
+    end
+end
+
+function io.exists(filename)
+    local f = io.open(filename)
+    if f == nil then
+        return false
+    else
+        assert(f:close())
+        return true
+    end
+end
+
+function io.size(filename)
+    local f = io.open(filename)
+    if f == nil then
+        return 0
+    else
+        local s = f:seek("end")
+        assert(f:close())
+        return s
+    end
+end
+
+function io.noflines(f)
+    local n = 0
+    for _ in f:lines() do
+        n = n + 1
+    end
+    f:seek('set',0)
+    return n
+end
+
+local nextchar = {
+    [ 4] = function(f)
+        return f:read(1,1,1,1)
+    end,
+    [ 2] = function(f)
+        return f:read(1,1)
+    end,
+    [ 1] = function(f)
+        return f:read(1)
+    end,
+    [-2] = function(f)
+        local a, b = f:read(1,1)
+        return b, a
+    end,
+    [-4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        return d, c, b, a
+    end
+}
+
+function io.characters(f,n)
+    if f then
+        return nextchar[n or 1], f
+    else
+        return nil, nil
+    end
+end
+
+local nextbyte = {
+    [4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        if d then
+            return byte(a), byte(b), byte(c), byte(d)
+        else
+            return nil, nil, nil, nil
+        end
+    end,
+    [2] = function(f)
+        local a, b = f:read(1,1)
+        if b then
+            return byte(a), byte(b)
+        else
+            return nil, nil
+        end
+    end,
+    [1] = function (f)
+        local a = f:read(1)
+        if a then
+            return byte(a)
+        else
+            return nil
+        end
+    end,
+    [-2] = function (f)
+        local a, b = f:read(1,1)
+        if b then
+            return byte(b), byte(a)
+        else
+            return nil, nil
+        end
+    end,
+    [-4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        if d then
+            return byte(d), byte(c), byte(b), byte(a)
+        else
+            return nil, nil, nil, nil
+        end
+    end
+}
+
+function io.bytes(f,n)
+    if f then
+        return nextbyte[n or 1], f
+    else
+        return nil, nil
+    end
+end
+
+function io.ask(question,default,options)
+    while true do
+        io.write(question)
+        if options then
+            io.write(string.format(" [%s]",table.concat(options,"|")))
+        end
+        if default then
+            io.write(string.format(" [%s]",default))
+        end
+        io.write(string.format(" "))
+        local answer = io.read()
+        answer = gsub(answer,"^%s*(.*)%s*$","%1")
+        if answer == "" and default then
+            return default
+        elseif not options then
+            return answer
+        else
+            for k=1,#options do
+                if options[k] == answer then
+                    return answer
+                end
+            end
+            local pattern = "^" .. answer
+            for k=1,#options do
+                local v = options[k]
+                if find(v,pattern) then
+                    return v
+                end
+            end
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-number'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local tostring = tostring
+local format, floor, insert, match = string.format, math.floor, table.insert, string.match
+local lpegmatch = lpeg.match
+
+number = number or { }
+
+-- a,b,c,d,e,f = number.toset(100101)
+
+function number.toset(n)
+    return match(tostring(n),"(.?)(.?)(.?)(.?)(.?)(.?)(.?)(.?)")
+end
+
+function number.toevenhex(n)
+    local s = format("%X",n)
+    if #s % 2 == 0 then
+        return s
+    else
+        return "0" .. s
+    end
+end
+
+-- the lpeg way is slower on 8 digits, but faster on 4 digits, some 7.5%
+-- on
+--
+-- for i=1,1000000 do
+--     local a,b,c,d,e,f,g,h = number.toset(12345678)
+--     local a,b,c,d         = number.toset(1234)
+--     local a,b,c           = number.toset(123)
+-- end
+--
+-- of course dedicated "(.)(.)(.)(.)" matches are even faster
+
+local one = lpeg.C(1-lpeg.S(''))^1
+
+function number.toset(n)
+    return lpegmatch(one,tostring(n))
+end
+
+function number.bits(n,zero)
+    local t, i = { }, (zero and 0) or 1
+    while n > 0 do
+        local m = n % 2
+        if m > 0 then
+            insert(t,1,i)
+        end
+        n = floor(n/2)
+        i = i + 1
+    end
+    return t
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-set'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+set = set or { }
+
+local nums   = { }
+local tabs   = { }
+local concat = table.concat
+local next, type = next, type
+
+set.create = table.tohash
+
+function set.tonumber(t)
+    if next(t) then
+        local s = ""
+    --  we could save mem by sorting, but it slows down
+        for k, v in next, t do
+            if v then
+            --  why bother about the leading space
+                s = s .. " " .. k
+            end
+        end
+        local n = nums[s]
+        if not n then
+            n = #tabs + 1
+            tabs[n] = t
+            nums[s] = n
+        end
+        return n
+    else
+        return 0
+    end
+end
+
+function set.totable(n)
+    if n == 0 then
+        return { }
+    else
+        return tabs[n] or { }
+    end
+end
+
+function set.tolist(n)
+    if n == 0 or not tabs[n] then
+        return ""
+    else
+        local t = { }
+        for k, v in next, tabs[n] do
+            if v then
+                t[#t+1] = k
+            end
+        end
+        return concat(t," ")
+    end
+end
+
+function set.contains(n,s)
+    if type(n) == "table" then
+        return n[s]
+    elseif n == 0 then
+        return false
+    else
+        local t = tabs[n]
+        return t and t[s]
+    end
+end
+
+--~ local c = set.create{'aap','noot','mies'}
+--~ local s = set.tonumber(c)
+--~ local t = set.totable(s)
+--~ print(t['aap'])
+--~ local c = set.create{'zus','wim','jet'}
+--~ local s = set.tonumber(c)
+--~ local t = set.totable(s)
+--~ print(t['aap'])
+--~ print(t['jet'])
+--~ print(set.contains(t,'jet'))
+--~ print(set.contains(t,'aap'))
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-os'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- maybe build io.flush in os.execute
+
+local find, format, gsub = string.find, string.format, string.gsub
+local random, ceil = math.random, math.ceil
+
+local execute, spawn, exec, ioflush = os.execute, os.spawn or os.execute, os.exec or os.execute, io.flush
+
+function os.execute(...) ioflush() return execute(...) end
+function os.spawn  (...) ioflush() return spawn  (...) end
+function os.exec   (...) ioflush() return exec   (...) end
+
+function os.resultof(command)
+    ioflush() -- else messed up logging
+    local handle = io.popen(command,"r")
+    if not handle then
+    --  print("unknown command '".. command .. "' in os.resultof")
+        return ""
+    else
+        return handle:read("*all") or ""
+    end
+end
+
+--~ os.type     : windows | unix (new, we already guessed os.platform)
+--~ os.name     : windows | msdos | linux | macosx | solaris | .. | generic (new)
+--~ os.platform : extended os.name with architecture
+
+if not io.fileseparator then
+    if find(os.getenv("PATH"),";") then
+        io.fileseparator, io.pathseparator, os.type = "\\", ";", os.type or "mswin"
+    else
+        io.fileseparator, io.pathseparator, os.type = "/" , ":", os.type or "unix"
+    end
+end
+
+os.type = os.type or (io.pathseparator == ";"       and "windows") or "unix"
+os.name = os.name or (os.type          == "windows" and "mswin"  ) or "linux"
+
+if os.type == "windows" then
+    os.libsuffix, os.binsuffix = 'dll', 'exe'
+else
+    os.libsuffix, os.binsuffix = 'so', ''
+end
+
+function os.launch(str)
+    if os.type == "windows" then
+        os.execute("start " .. str) -- os.spawn ?
+    else
+        os.execute(str .. " &")     -- os.spawn ?
+    end
+end
+
+if not os.times then
+    -- utime  = user time
+    -- stime  = system time
+    -- cutime = children user time
+    -- cstime = children system time
+    function os.times()
+        return {
+            utime  = os.gettimeofday(), -- user
+            stime  = 0,                 -- system
+            cutime = 0,                 -- children user
+            cstime = 0,                 -- children system
+        }
+    end
+end
+
+os.gettimeofday = os.gettimeofday or os.clock
+
+local startuptime = os.gettimeofday()
+
+function os.runtime()
+    return os.gettimeofday() - startuptime
+end
+
+--~ print(os.gettimeofday()-os.time())
+--~ os.sleep(1.234)
+--~ print (">>",os.runtime())
+--~ print(os.date("%H:%M:%S",os.gettimeofday()))
+--~ print(os.date("%H:%M:%S",os.time()))
+
+-- no need for function anymore as we have more clever code and helpers now
+-- this metatable trickery might as well disappear
+
+os.resolvers = os.resolvers or { }
+
+local resolvers = os.resolvers
+
+local osmt = getmetatable(os) or { __index = function(t,k) t[k] = "unset" return "unset" end } -- maybe nil
+local osix = osmt.__index
+
+osmt.__index = function(t,k)
+    return (resolvers[k] or osix)(t,k)
+end
+
+setmetatable(os,osmt)
+
+if not os.setenv then
+
+    -- we still store them but they won't be seen in
+    -- child processes although we might pass them some day
+    -- using command concatination
+
+    local env, getenv = { }, os.getenv
+
+    function os.setenv(k,v)
+        env[k] = v
+    end
+
+    function os.getenv(k)
+        return env[k] or getenv(k)
+    end
+
+end
+
+-- we can use HOSTTYPE on some platforms
+
+local name, platform = os.name or "linux", os.getenv("MTX_PLATFORM") or ""
+
+local function guess()
+    local architecture = os.resultof("uname -m") or ""
+    if architecture ~= "" then
+        return architecture
+    end
+    architecture = os.getenv("HOSTTYPE") or ""
+    if architecture ~= "" then
+        return architecture
+    end
+    return os.resultof("echo $HOSTTYPE") or ""
+end
+
+if platform ~= "" then
+
+    os.platform = platform
+
+elseif os.type == "windows" then
+
+    -- we could set the variable directly, no function needed here
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.getenv("PROCESSOR_ARCHITECTURE") or ""
+        if find(architecture,"AMD64") then
+            platform = "mswin-64"
+        else
+            platform = "mswin"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "linux" then
+
+    function os.resolvers.platform(t,k)
+        -- we sometims have HOSTTYPE set so let's check that first
+        local platform, architecture = "", os.getenv("HOSTTYPE") or os.resultof("uname -m") or ""
+        if find(architecture,"x86_64") then
+            platform = "linux-64"
+        elseif find(architecture,"ppc") then
+            platform = "linux-ppc"
+        else
+            platform = "linux"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "macosx" then
+
+    --[[
+        Identifying the architecture of OSX is quite a mess and this
+        is the best we can come up with. For some reason $HOSTTYPE is
+        a kind of pseudo environment variable, not known to the current
+        environment. And yes, uname cannot be trusted either, so there
+        is a change that you end up with a 32 bit run on a 64 bit system.
+        Also, some proper 64 bit intel macs are too cheap (low-end) and
+        therefore not permitted to run the 64 bit kernel.
+      ]]--
+
+    function os.resolvers.platform(t,k)
+     -- local platform, architecture = "", os.getenv("HOSTTYPE") or ""
+     -- if architecture == "" then
+     --     architecture = os.resultof("echo $HOSTTYPE") or ""
+     -- end
+        local platform, architecture = "", os.resultof("echo $HOSTTYPE") or ""
+        if architecture == "" then
+         -- print("\nI have no clue what kind of OSX you're running so let's assume an 32 bit intel.\n")
+            platform = "osx-intel"
+        elseif find(architecture,"i386") then
+            platform = "osx-intel"
+        elseif find(architecture,"x86_64") then
+            platform = "osx-64"
+        else
+            platform = "osx-ppc"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "sunos" then
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.resultof("uname -m") or ""
+        if find(architecture,"sparc") then
+            platform = "solaris-sparc"
+        else -- if architecture == 'i86pc'
+            platform = "solaris-intel"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "freebsd" then
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.resultof("uname -m") or ""
+        if find(architecture,"amd64") then
+            platform = "freebsd-amd64"
+        else
+            platform = "freebsd"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "kfreebsd" then
+
+    function os.resolvers.platform(t,k)
+        -- we sometims have HOSTTYPE set so let's check that first
+        local platform, architecture = "", os.getenv("HOSTTYPE") or os.resultof("uname -m") or ""
+        if find(architecture,"x86_64") then
+            platform = "kfreebsd-64"
+        else
+            platform = "kfreebsd-i386"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+else
+
+    -- platform = "linux"
+    -- os.setenv("MTX_PLATFORM",platform)
+    -- os.platform = platform
+
+    function os.resolvers.platform(t,k)
+        local platform = "linux"
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+end
+
+-- beware, we set the randomseed
+
+-- from wikipedia: Version 4 UUIDs use a scheme relying only on random numbers. This algorithm sets the
+-- version number as well as two reserved bits. All other bits are set using a random or pseudorandom
+-- data source. Version 4 UUIDs have the form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx with hexadecimal
+-- digits x and hexadecimal digits 8, 9, A, or B for y. e.g. f47ac10b-58cc-4372-a567-0e02b2c3d479.
+--
+-- as we don't call this function too often there is not so much risk on repetition
+
+local t = { 8, 9, "a", "b" }
+
+function os.uuid()
+    return format("%04x%04x-4%03x-%s%03x-%04x-%04x%04x%04x",
+        random(0xFFFF),random(0xFFFF),
+        random(0x0FFF),
+        t[ceil(random(4))] or 8,random(0x0FFF),
+        random(0xFFFF),
+        random(0xFFFF),random(0xFFFF),random(0xFFFF)
+    )
+end
+
+local d
+
+function os.timezone(delta)
+    d = d or tonumber(tonumber(os.date("%H")-os.date("!%H")))
+    if delta then
+        if d > 0 then
+            return format("+%02i:00",d)
+        else
+            return format("-%02i:00",-d)
+        end
+    else
+        return 1
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-file'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- needs a cleanup
+
+file = file or { }
+
+local concat = table.concat
+local find, gmatch, match, gsub, sub, char = string.find, string.gmatch, string.match, string.gsub, string.sub, string.char
+local lpegmatch = lpeg.match
+
+function file.removesuffix(filename)
+    return (gsub(filename,"%.[%a%d]+$",""))
+end
+
+function file.addsuffix(filename, suffix)
+    if not suffix or suffix == "" then
+        return filename
+    elseif not find(filename,"%.[%a%d]+$") then
+        return filename .. "." .. suffix
+    else
+        return filename
+    end
+end
+
+function file.replacesuffix(filename, suffix)
+    return (gsub(filename,"%.[%a%d]+$","")) .. "." .. suffix
+end
+
+function file.dirname(name,default)
+    return match(name,"^(.+)[/\\].-$") or (default or "")
+end
+
+function file.basename(name)
+    return match(name,"^.+[/\\](.-)$") or name
+end
+
+function file.nameonly(name)
+    return (gsub(match(name,"^.+[/\\](.-)$") or name,"%..*$",""))
+end
+
+function file.extname(name,default)
+    return match(name,"^.+%.([^/\\]-)$") or default or ""
+end
+
+file.suffix = file.extname
+
+--~ function file.join(...)
+--~     local pth = concat({...},"/")
+--~     pth = gsub(pth,"\\","/")
+--~     local a, b = match(pth,"^(.*://)(.*)$")
+--~     if a and b then
+--~         return a .. gsub(b,"//+","/")
+--~     end
+--~     a, b = match(pth,"^(//)(.*)$")
+--~     if a and b then
+--~         return a .. gsub(b,"//+","/")
+--~     end
+--~     return (gsub(pth,"//+","/"))
+--~ end
+
+local trick_1 = char(1)
+local trick_2 = "^" .. trick_1 .. "/+"
+
+function file.join(...)
+    local lst = { ... }
+    local a, b = lst[1], lst[2]
+    if a == "" then
+        lst[1] = trick_1
+    elseif b and find(a,"^/+$") and find(b,"^/") then
+        lst[1] = ""
+        lst[2] = gsub(b,"^/+","")
+    end
+    local pth = concat(lst,"/")
+    pth = gsub(pth,"\\","/")
+    local a, b = match(pth,"^(.*://)(.*)$")
+    if a and b then
+        return a .. gsub(b,"//+","/")
+    end
+    a, b = match(pth,"^(//)(.*)$")
+    if a and b then
+        return a .. gsub(b,"//+","/")
+    end
+    pth = gsub(pth,trick_2,"")
+    return (gsub(pth,"//+","/"))
+end
+
+--~ print(file.join("//","/y"))
+--~ print(file.join("/","/y"))
+--~ print(file.join("","/y"))
+--~ print(file.join("/x/","/y"))
+--~ print(file.join("x/","/y"))
+--~ print(file.join("http://","/y"))
+--~ print(file.join("http://a","/y"))
+--~ print(file.join("http:///a","/y"))
+--~ print(file.join("//nas-1","/y"))
+
+function file.iswritable(name)
+    local a = lfs.attributes(name) or lfs.attributes(file.dirname(name,"."))
+    return a and sub(a.permissions,2,2) == "w"
+end
+
+function file.isreadable(name)
+    local a = lfs.attributes(name)
+    return a and sub(a.permissions,1,1) == "r"
+end
+
+file.is_readable = file.isreadable
+file.is_writable = file.iswritable
+
+-- todo: lpeg
+
+--~ function file.split_path(str)
+--~     local t = { }
+--~     str = gsub(str,"\\", "/")
+--~     str = gsub(str,"(%a):([;/])", "%1\001%2")
+--~     for name in gmatch(str,"([^;:]+)") do
+--~         if name ~= "" then
+--~             t[#t+1] = gsub(name,"\001",":")
+--~         end
+--~     end
+--~     return t
+--~ end
+
+local checkedsplit = string.checkedsplit
+
+function file.split_path(str,separator)
+    str = gsub(str,"\\","/")
+    return checkedsplit(str,separator or io.pathseparator)
+end
+
+function file.join_path(tab)
+    return concat(tab,io.pathseparator) -- can have trailing //
+end
+
+-- we can hash them weakly
+
+function file.collapse_path(str)
+    str = gsub(str,"\\","/")
+    if find(str,"/") then
+        str = gsub(str,"^%./",(gsub(lfs.currentdir(),"\\","/")) .. "/") -- ./xx in qualified
+        str = gsub(str,"/%./","/")
+        local n, m = 1, 1
+        while n > 0 or m > 0 do
+            str, n = gsub(str,"[^/%.]+/%.%.$","")
+            str, m = gsub(str,"[^/%.]+/%.%./","")
+        end
+        str = gsub(str,"([^/])/$","%1")
+    --  str = gsub(str,"^%./","") -- ./xx in qualified
+        str = gsub(str,"/%.$","")
+    end
+    if str == "" then str = "." end
+    return str
+end
+
+--~ print(file.collapse_path("/a"))
+--~ print(file.collapse_path("a/./b/.."))
+--~ print(file.collapse_path("a/aa/../b/bb"))
+--~ print(file.collapse_path("a/../.."))
+--~ print(file.collapse_path("a/.././././b/.."))
+--~ print(file.collapse_path("a/./././b/.."))
+--~ print(file.collapse_path("a/b/c/../.."))
+
+function file.robustname(str)
+    return (gsub(str,"[^%a%d%/%-%.\\]+","-"))
+end
+
+file.readdata = io.loaddata
+file.savedata = io.savedata
+
+function file.copy(oldname,newname)
+    file.savedata(newname,io.loaddata(oldname))
+end
+
+-- lpeg variants, slightly faster, not always
+
+--~ local period    = lpeg.P(".")
+--~ local slashes   = lpeg.S("\\/")
+--~ local noperiod  = 1-period
+--~ local noslashes = 1-slashes
+--~ local name      = noperiod^1
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.C(noperiod^1) * -1
+
+--~ function file.extname(name)
+--~     return lpegmatch(pattern,name) or ""
+--~ end
+
+--~ local pattern = lpeg.Cs(((period * noperiod^1 * -1)/"" + 1)^1)
+
+--~ function file.removesuffix(name)
+--~     return lpegmatch(pattern,name)
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^1 * lpeg.C(noslashes^1) * -1
+
+--~ function file.basename(name)
+--~     return lpegmatch(pattern,name) or name
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^1 * lpeg.Cp() * noslashes^1 * -1
+
+--~ function file.dirname(name)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return sub(name,1,p-2)
+--~     else
+--~         return ""
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.Cp() * noperiod^1 * -1
+
+--~ function file.addsuffix(name, suffix)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return name
+--~     else
+--~         return name .. "." .. suffix
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.Cp() * noperiod^1 * -1
+
+--~ function file.replacesuffix(name,suffix)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return sub(name,1,p-2) .. "." .. suffix
+--~     else
+--~         return name .. "." .. suffix
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * lpeg.Cp() * ((noperiod^1 * period)^1 * lpeg.Cp() + lpeg.P(true)) * noperiod^1 * -1
+
+--~ function file.nameonly(name)
+--~     local a, b = lpegmatch(pattern,name)
+--~     if b then
+--~         return sub(name,a,b-2)
+--~     elseif a then
+--~         return sub(name,a)
+--~     else
+--~         return name
+--~     end
+--~ end
+
+--~ local test = file.extname
+--~ local test = file.basename
+--~ local test = file.dirname
+--~ local test = file.addsuffix
+--~ local test = file.replacesuffix
+--~ local test = file.nameonly
+
+--~ print(1,test("./a/b/c/abd.def.xxx","!!!"))
+--~ print(2,test("./../b/c/abd.def.xxx","!!!"))
+--~ print(3,test("a/b/c/abd.def.xxx","!!!"))
+--~ print(4,test("a/b/c/def.xxx","!!!"))
+--~ print(5,test("a/b/c/def","!!!"))
+--~ print(6,test("def","!!!"))
+--~ print(7,test("def.xxx","!!!"))
+
+--~ local tim = os.clock() for i=1,250000 do local ext = test("abd.def.xxx","!!!") end print(os.clock()-tim)
+
+-- also rewrite previous
+
+local letter    = lpeg.R("az","AZ") + lpeg.S("_-+")
+local separator = lpeg.P("://")
+
+local qualified = lpeg.P(".")^0 * lpeg.P("/") + letter*lpeg.P(":") + letter^1*separator + letter^1 * lpeg.P("/")
+local rootbased = lpeg.P("/") + letter*lpeg.P(":")
+
+-- ./name ../name  /name c: :// name/name
+
+function file.is_qualified_path(filename)
+    return lpegmatch(qualified,filename) ~= nil
+end
+
+function file.is_rootbased_path(filename)
+    return lpegmatch(rootbased,filename) ~= nil
+end
+
+local slash  = lpeg.S("\\/")
+local period = lpeg.P(".")
+local drive  = lpeg.C(lpeg.R("az","AZ")) * lpeg.P(":")
+local path   = lpeg.C(((1-slash)^0 * slash)^0)
+local suffix = period * lpeg.C(lpeg.P(1-period)^0 * lpeg.P(-1))
+local base   = lpeg.C((1-suffix)^0)
+
+local pattern = (drive + lpeg.Cc("")) * (path + lpeg.Cc("")) * (base + lpeg.Cc("")) * (suffix + lpeg.Cc(""))
+
+function file.splitname(str) -- returns drive, path, base, suffix
+    return lpegmatch(pattern,str)
+end
+
+-- function test(t) for k, v in next, t do print(v, "=>", file.splitname(v)) end end
+--
+-- test { "c:", "c:/aa", "c:/aa/bb", "c:/aa/bb/cc", "c:/aa/bb/cc.dd", "c:/aa/bb/cc.dd.ee" }
+-- test { "c:", "c:aa", "c:aa/bb", "c:aa/bb/cc", "c:aa/bb/cc.dd", "c:aa/bb/cc.dd.ee" }
+-- test { "/aa", "/aa/bb", "/aa/bb/cc", "/aa/bb/cc.dd", "/aa/bb/cc.dd.ee" }
+-- test { "aa", "aa/bb", "aa/bb/cc", "aa/bb/cc.dd", "aa/bb/cc.dd.ee" }
+
+--~ -- todo:
+--~
+--~ if os.type == "windows" then
+--~     local currentdir = lfs.currentdir
+--~     function lfs.currentdir()
+--~         return (gsub(currentdir(),"\\","/"))
+--~     end
+--~ end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-md5'] = {
+    version   = 1.001,
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- This also provides file checksums and checkers.
+
+local gsub, format, byte = string.gsub, string.format, string.byte
+
+local function convert(str,fmt)
+    return (gsub(md5.sum(str),".",function(chr) return format(fmt,byte(chr)) end))
+end
+
+if not md5.HEX then function md5.HEX(str) return convert(str,"%02X") end end
+if not md5.hex then function md5.hex(str) return convert(str,"%02x") end end
+if not md5.dec then function md5.dec(str) return convert(str,"%03i") end end
+
+--~ if not md5.HEX then
+--~     local function remap(chr) return format("%02X",byte(chr)) end
+--~     function md5.HEX(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+--~ if not md5.hex then
+--~     local function remap(chr) return format("%02x",byte(chr)) end
+--~     function md5.hex(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+--~ if not md5.dec then
+--~     local function remap(chr) return format("%03i",byte(chr)) end
+--~     function md5.dec(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+
+file.needs_updating_threshold = 1
+
+function file.needs_updating(oldname,newname) -- size modification access change
+    local oldtime = lfs.attributes(oldname, modification)
+    local newtime = lfs.attributes(newname, modification)
+    if newtime >= oldtime then
+        return false
+    elseif oldtime - newtime < file.needs_updating_threshold then
+        return false
+    else
+        return true
+    end
+end
+
+function file.checksum(name)
+    if md5 then
+        local data = io.loaddata(name)
+        if data then
+            return md5.HEX(data)
+        end
+    end
+    return nil
+end
+
+function file.loadchecksum(name)
+    if md5 then
+        local data = io.loaddata(name .. ".md5")
+        return data and (gsub(data,"%s",""))
+    end
+    return nil
+end
+
+function file.savechecksum(name, checksum)
+    if not checksum then checksum = file.checksum(name) end
+    if checksum then
+        io.savedata(name .. ".md5",checksum)
+        return checksum
+    end
+    return nil
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-url'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local char, gmatch, gsub = string.char, string.gmatch, string.gsub
+local tonumber, type = tonumber, type
+local lpegmatch = lpeg.match
+
+-- from the spec (on the web):
+--
+--     foo://example.com:8042/over/there?name=ferret#nose
+--     \_/   \______________/\_________/ \_________/ \__/
+--      |           |            |            |        |
+--   scheme     authority       path        query   fragment
+--      |   _____________________|__
+--     / \ /                        \
+--     urn:example:animal:ferret:nose
+
+url = url or { }
+
+local function tochar(s)
+    return char(tonumber(s,16))
+end
+
+local colon, qmark, hash, slash, percent, endofstring = lpeg.P(":"), lpeg.P("?"), lpeg.P("#"), lpeg.P("/"), lpeg.P("%"), lpeg.P(-1)
+
+local hexdigit  = lpeg.R("09","AF","af")
+local plus      = lpeg.P("+")
+local escaped   = (plus / " ") + (percent * lpeg.C(hexdigit * hexdigit) / tochar)
+
+-- we assume schemes with more than 1 character (in order to avoid problems with windows disks)
+
+local scheme    =                 lpeg.Cs((escaped+(1-colon-slash-qmark-hash))^2) * colon + lpeg.Cc("")
+local authority = slash * slash * lpeg.Cs((escaped+(1-      slash-qmark-hash))^0)         + lpeg.Cc("")
+local path      = slash *         lpeg.Cs((escaped+(1-            qmark-hash))^0)         + lpeg.Cc("")
+local query     = qmark         * lpeg.Cs((escaped+(1-                  hash))^0)         + lpeg.Cc("")
+local fragment  = hash          * lpeg.Cs((escaped+(1-           endofstring))^0)         + lpeg.Cc("")
+
+local parser = lpeg.Ct(scheme * authority * path * query * fragment)
+
+-- todo: reconsider Ct as we can as well have five return values (saves a table)
+-- so we can have two parsers, one with and one without
+
+function url.split(str)
+    return (type(str) == "string" and lpegmatch(parser,str)) or str
+end
+
+-- todo: cache them
+
+function url.hashed(str)
+    local s = url.split(str)
+    local somescheme = s[1] ~= ""
+    return {
+        scheme    = (somescheme and s[1]) or "file",
+        authority = s[2],
+        path      = s[3],
+        query     = s[4],
+        fragment  = s[5],
+        original  = str,
+        noscheme  = not somescheme,
+    }
+end
+
+function url.hasscheme(str)
+    return url.split(str)[1] ~= ""
+end
+
+function url.addscheme(str,scheme)
+    return (url.hasscheme(str) and str) or ((scheme or "file:///") .. str)
+end
+
+function url.construct(hash)
+    local fullurl = hash.sheme .. "://".. hash.authority .. hash.path
+    if hash.query then
+        fullurl = fullurl .. "?".. hash.query
+    end
+    if hash.fragment then
+        fullurl = fullurl .. "?".. hash.fragment
+    end
+    return fullurl
+end
+
+function url.filename(filename)
+    local t = url.hashed(filename)
+    return (t.scheme == "file" and (gsub(t.path,"^/([a-zA-Z])([:|])/)","%1:"))) or filename
+end
+
+function url.query(str)
+    if type(str) == "string" then
+        local t = { }
+        for k, v in gmatch(str,"([^&=]*)=([^&=]*)") do
+            t[k] = v
+        end
+        return t
+    else
+        return str
+    end
+end
+
+--~ print(url.filename("file:///c:/oeps.txt"))
+--~ print(url.filename("c:/oeps.txt"))
+--~ print(url.filename("file:///oeps.txt"))
+--~ print(url.filename("file:///etc/test.txt"))
+--~ print(url.filename("/oeps.txt"))
+
+--~ from the spec on the web (sort of):
+--~
+--~ function test(str)
+--~     print(table.serialize(url.hashed(str)))
+--~ end
+--~
+--~ test("%56pass%20words")
+--~ test("file:///c:/oeps.txt")
+--~ test("file:///c|/oeps.txt")
+--~ test("file:///etc/oeps.txt")
+--~ test("file://./etc/oeps.txt")
+--~ test("file:////etc/oeps.txt")
+--~ test("ftp://ftp.is.co.za/rfc/rfc1808.txt")
+--~ test("http://www.ietf.org/rfc/rfc2396.txt")
+--~ test("ldap://[2001:db8::7]/c=GB?objectClass?one#what")
+--~ test("mailto:John.Doe@example.com")
+--~ test("news:comp.infosystems.www.servers.unix")
+--~ test("tel:+1-816-555-1212")
+--~ test("telnet://192.0.2.16:80/")
+--~ test("urn:oasis:names:specification:docbook:dtd:xml:4.1.2")
+--~ test("/etc/passwords")
+--~ test("http://www.pragma-ade.com/spaced%20name")
+
+--~ test("zip:///oeps/oeps.zip#bla/bla.tex")
+--~ test("zip:///oeps/oeps.zip?bla/bla.tex")
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-dir'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- dir.expand_name will be merged with cleanpath and collapsepath
+
+local type = type
+local find, gmatch, match, gsub = string.find, string.gmatch, string.match, string.gsub
+local lpegmatch = lpeg.match
+
+dir = dir or { }
+
+-- handy
+
+function dir.current()
+    return (gsub(lfs.currentdir(),"\\","/"))
+end
+
+-- optimizing for no string.find (*) does not save time
+
+local attributes = lfs.attributes
+local walkdir    = lfs.dir
+
+local function glob_pattern(path,patt,recurse,action)
+    local ok, scanner
+    if path == "/" then
+        ok, scanner = xpcall(function() return walkdir(path..".") end, function() end) -- kepler safe
+    else
+        ok, scanner = xpcall(function() return walkdir(path)      end, function() end) -- kepler safe
+    end
+    if ok and type(scanner) == "function" then
+        if not find(path,"/$") then path = path .. '/' end
+        for name in scanner do
+            local full = path .. name
+            local mode = attributes(full,'mode')
+            if mode == 'file' then
+                if find(full,patt) then
+                    action(full)
+                end
+            elseif recurse and (mode == "directory") and (name ~= '.') and (name ~= "..") then
+                glob_pattern(full,patt,recurse,action)
+            end
+        end
+    end
+end
+
+dir.glob_pattern = glob_pattern
+
+local function collect_pattern(path,patt,recurse,result)
+    local ok, scanner
+    result = result or { }
+    if path == "/" then
+        ok, scanner = xpcall(function() return walkdir(path..".") end, function() end) -- kepler safe
+    else
+        ok, scanner = xpcall(function() return walkdir(path)      end, function() end) -- kepler safe
+    end
+    if ok and type(scanner) == "function" then
+        if not find(path,"/$") then path = path .. '/' end
+        for name in scanner do
+            local full = path .. name
+            local attr = attributes(full)
+            local mode = attr.mode
+            if mode == 'file' then
+                if find(full,patt) then
+                    result[name] = attr
+                end
+            elseif recurse and (mode == "directory") and (name ~= '.') and (name ~= "..") then
+                attr.list = collect_pattern(full,patt,recurse)
+                result[name] = attr
+            end
+        end
+    end
+    return result
+end
+
+dir.collect_pattern = collect_pattern
+
+local P, S, R, C, Cc, Cs, Ct, Cv, V = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.Cc, lpeg.Cs, lpeg.Ct, lpeg.Cv, lpeg.V
+
+local pattern = Ct {
+    [1] = (C(P(".") + P("/")^1) + C(R("az","AZ") * P(":") * P("/")^0) + Cc("./")) * V(2) * V(3),
+    [2] = C(((1-S("*?/"))^0 * P("/"))^0),
+    [3] = C(P(1)^0)
+}
+
+local filter = Cs ( (
+    P("**") / ".*" +
+    P("*")  / "[^/]*" +
+    P("?")  / "[^/]" +
+    P(".")  / "%%." +
+    P("+")  / "%%+" +
+    P("-")  / "%%-" +
+    P(1)
+)^0 )
+
+local function glob(str,t)
+    if type(t) == "function" then
+        if type(str) == "table" then
+            for s=1,#str do
+                glob(str[s],t)
+            end
+        elseif lfs.isfile(str) then
+            t(str)
+        else
+            local split = lpegmatch(pattern,str)
+            if split then
+                local root, path, base = split[1], split[2], split[3]
+                local recurse = find(base,"%*%*")
+                local start = root .. path
+                local result = lpegmatch(filter,start .. base)
+                glob_pattern(start,result,recurse,t)
+            end
+        end
+    else
+        if type(str) == "table" then
+            local t = t or { }
+            for s=1,#str do
+                glob(str[s],t)
+            end
+            return t
+        elseif lfs.isfile(str) then
+            local t = t or { }
+            t[#t+1] = str
+            return t
+        else
+            local split = lpegmatch(pattern,str)
+            if split then
+                local t = t or { }
+                local action = action or function(name) t[#t+1] = name end
+                local root, path, base = split[1], split[2], split[3]
+                local recurse = find(base,"%*%*")
+                local start = root .. path
+                local result = lpegmatch(filter,start .. base)
+                glob_pattern(start,result,recurse,action)
+                return t
+            else
+                return { }
+            end
+        end
+    end
+end
+
+dir.glob = glob
+
+--~ list = dir.glob("**/*.tif")
+--~ list = dir.glob("/**/*.tif")
+--~ list = dir.glob("./**/*.tif")
+--~ list = dir.glob("oeps/**/*.tif")
+--~ list = dir.glob("/oeps/**/*.tif")
+
+local function globfiles(path,recurse,func,files) -- func == pattern or function
+    if type(func) == "string" then
+        local s = func -- alas, we need this indirect way
+        func = function(name) return find(name,s) end
+    end
+    files = files or { }
+    for name in walkdir(path) do
+        if find(name,"^%.") then
+            --- skip
+        else
+            local mode = attributes(name,'mode')
+            if mode == "directory" then
+                if recurse then
+                    globfiles(path .. "/" .. name,recurse,func,files)
+                end
+            elseif mode == "file" then
+                if func then
+                    if func(name) then
+                        files[#files+1] = path .. "/" .. name
+                    end
+                else
+                    files[#files+1] = path .. "/" .. name
+                end
+            end
+        end
+    end
+    return files
+end
+
+dir.globfiles = globfiles
+
+-- t = dir.glob("c:/data/develop/context/sources/**/????-*.tex")
+-- t = dir.glob("c:/data/develop/tex/texmf/**/*.tex")
+-- t = dir.glob("c:/data/develop/context/texmf/**/*.tex")
+-- t = dir.glob("f:/minimal/tex/**/*")
+-- print(dir.ls("f:/minimal/tex/**/*"))
+-- print(dir.ls("*.tex"))
+
+function dir.ls(pattern)
+    return table.concat(glob(pattern),"\n")
+end
+
+--~ mkdirs("temp")
+--~ mkdirs("a/b/c")
+--~ mkdirs(".","/a/b/c")
+--~ mkdirs("a","b","c")
+
+local make_indeed = true -- false
+
+if string.find(os.getenv("PATH"),";") then -- os.type == "windows"
+
+    function dir.mkdirs(...)
+        local str, pth, t = "", "", { ... }
+        for i=1,#t do
+            local s = t[i]
+            if s ~= "" then
+                if str ~= "" then
+                    str = str .. "/" .. s
+                else
+                    str = s
+                end
+            end
+        end
+        local first, middle, last
+        local drive = false
+        first, middle, last = match(str,"^(//)(//*)(.*)$")
+        if first then
+            -- empty network path == local path
+        else
+            first, last = match(str,"^(//)/*(.-)$")
+            if first then
+                middle, last = match(str,"([^/]+)/+(.-)$")
+                if middle then
+                    pth = "//" .. middle
+                else
+                    pth = "//" .. last
+                    last = ""
+                end
+            else
+                first, middle, last = match(str,"^([a-zA-Z]:)(/*)(.-)$")
+                if first then
+                    pth, drive = first .. middle, true
+                else
+                    middle, last = match(str,"^(/*)(.-)$")
+                    if not middle then
+                        last = str
+                    end
+                end
+            end
+        end
+        for s in gmatch(last,"[^/]+") do
+            if pth == "" then
+                pth = s
+            elseif drive then
+                pth, drive = pth .. s, false
+            else
+                pth = pth .. "/" .. s
+            end
+            if make_indeed and not lfs.isdir(pth) then
+                lfs.mkdir(pth)
+            end
+        end
+        return pth, (lfs.isdir(pth) == true)
+    end
+
+--~         print(dir.mkdirs("","","a","c"))
+--~         print(dir.mkdirs("a"))
+--~         print(dir.mkdirs("a:"))
+--~         print(dir.mkdirs("a:/b/c"))
+--~         print(dir.mkdirs("a:b/c"))
+--~         print(dir.mkdirs("a:/bbb/c"))
+--~         print(dir.mkdirs("/a/b/c"))
+--~         print(dir.mkdirs("/aaa/b/c"))
+--~         print(dir.mkdirs("//a/b/c"))
+--~         print(dir.mkdirs("///a/b/c"))
+--~         print(dir.mkdirs("a/bbb//ccc/"))
+
+    function dir.expand_name(str) -- will be merged with cleanpath and collapsepath
+        local first, nothing, last = match(str,"^(//)(//*)(.*)$")
+        if first then
+            first = dir.current() .. "/"
+        end
+        if not first then
+            first, last = match(str,"^(//)/*(.*)$")
+        end
+        if not first then
+            first, last = match(str,"^([a-zA-Z]:)(.*)$")
+            if first and not find(last,"^/") then
+                local d = lfs.currentdir()
+                if lfs.chdir(first) then
+                    first = dir.current()
+                end
+                lfs.chdir(d)
+            end
+        end
+        if not first then
+            first, last = dir.current(), str
+        end
+        last = gsub(last,"//","/")
+        last = gsub(last,"/%./","/")
+        last = gsub(last,"^/*","")
+        first = gsub(first,"/*$","")
+        if last == "" then
+            return first
+        else
+            return first .. "/" .. last
+        end
+    end
+
+else
+
+    function dir.mkdirs(...)
+        local str, pth, t = "", "", { ... }
+        for i=1,#t do
+            local s = t[i]
+            if s ~= "" then
+                if str ~= "" then
+                    str = str .. "/" .. s
+                else
+                    str = s
+                end
+            end
+        end
+        str = gsub(str,"/+","/")
+        if find(str,"^/") then
+            pth = "/"
+            for s in gmatch(str,"[^/]+") do
+                local first = (pth == "/")
+                if first then
+                    pth = pth .. s
+                else
+                    pth = pth .. "/" .. s
+                end
+                if make_indeed and not first and not lfs.isdir(pth) then
+                    lfs.mkdir(pth)
+                end
+            end
+        else
+            pth = "."
+            for s in gmatch(str,"[^/]+") do
+                pth = pth .. "/" .. s
+                if make_indeed and not lfs.isdir(pth) then
+                    lfs.mkdir(pth)
+                end
+            end
+        end
+        return pth, (lfs.isdir(pth) == true)
+    end
+
+--~         print(dir.mkdirs("","","a","c"))
+--~         print(dir.mkdirs("a"))
+--~         print(dir.mkdirs("/a/b/c"))
+--~         print(dir.mkdirs("/aaa/b/c"))
+--~         print(dir.mkdirs("//a/b/c"))
+--~         print(dir.mkdirs("///a/b/c"))
+--~         print(dir.mkdirs("a/bbb//ccc/"))
+
+    function dir.expand_name(str) -- will be merged with cleanpath and collapsepath
+        if not find(str,"^/") then
+            str = lfs.currentdir() .. "/" .. str
+        end
+        str = gsub(str,"//","/")
+        str = gsub(str,"/%./","/")
+        return str
+    end
+
+end
+
+dir.makedirs = dir.mkdirs
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-boolean'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+boolean = boolean or { }
+
+local type, tonumber = type, tonumber
+
+function boolean.tonumber(b)
+    if b then return 1 else return 0 end
+end
+
+function toboolean(str,tolerant)
+    if tolerant then
+        local tstr = type(str)
+        if tstr == "string" then
+            return str == "true" or str == "yes" or str == "on" or str == "1" or str == "t"
+        elseif tstr == "number" then
+            return tonumber(str) ~= 0
+        elseif tstr == "nil" then
+            return false
+        else
+            return str
+        end
+    elseif str == "true" then
+        return true
+    elseif str == "false" then
+        return false
+    else
+        return str
+    end
+end
+
+function string.is_boolean(str)
+    if type(str) == "string" then
+        if str == "true" or str == "yes" or str == "on" or str == "t" then
+            return true
+        elseif str == "false" or str == "no" or str == "off" or str == "f" then
+            return false
+        end
+    end
+    return nil
+end
+
+function boolean.alwaystrue()
+    return true
+end
+
+function boolean.falsetrue()
+    return false
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-unicode'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+if not unicode then
+
+    unicode = { utf8 = { } }
+
+    local floor, char = math.floor, string.char
+
+    function unicode.utf8.utfchar(n)
+        if n < 0x80 then
+            return char(n)
+        elseif n < 0x800 then
+            return char(0xC0 + floor(n/0x40))  .. char(0x80 + (n % 0x40))
+        elseif n < 0x10000 then
+            return char(0xE0 + floor(n/0x1000)) .. char(0x80 + (floor(n/0x40) % 0x40)) .. char(0x80 + (n % 0x40))
+        elseif n < 0x40000 then
+            return char(0xF0 + floor(n/0x40000)) .. char(0x80 + floor(n/0x1000)) .. char(0x80 + (floor(n/0x40) % 0x40)) .. char(0x80 + (n % 0x40))
+        else -- wrong:
+          -- return char(0xF1 + floor(n/0x1000000)) .. char(0x80 + floor(n/0x40000)) .. char(0x80 + floor(n/0x1000)) .. char(0x80 + (floor(n/0x40) % 0x40)) .. char(0x80 + (n % 0x40))
+            return "?"
+        end
+    end
+
+end
+
+utf = utf or unicode.utf8
+
+local concat, utfchar, utfgsub = table.concat, utf.char, utf.gsub
+local char, byte, find, bytepairs = string.char, string.byte, string.find, string.bytepairs
+
+-- 0  EF BB BF      UTF-8
+-- 1  FF FE         UTF-16-little-endian
+-- 2  FE FF         UTF-16-big-endian
+-- 3  FF FE 00 00   UTF-32-little-endian
+-- 4  00 00 FE FF   UTF-32-big-endian
+
+unicode.utfname = {
+    [0] = 'utf-8',
+    [1] = 'utf-16-le',
+    [2] = 'utf-16-be',
+    [3] = 'utf-32-le',
+    [4] = 'utf-32-be'
+}
+
+-- \000 fails in <= 5.0 but is valid in >=5.1 where %z is depricated
+
+function unicode.utftype(f)
+    local str = f:read(4)
+    if not str then
+        f:seek('set')
+        return 0
+ -- elseif find(str,"^%z%z\254\255") then            -- depricated
+ -- elseif find(str,"^\000\000\254\255") then        -- not permitted and bugged
+    elseif find(str,"\000\000\254\255",1,true) then  -- seems to work okay (TH)
+        return 4
+ -- elseif find(str,"^\255\254%z%z") then            -- depricated
+ -- elseif find(str,"^\255\254\000\000") then        -- not permitted and bugged
+    elseif find(str,"\255\254\000\000",1,true) then  -- seems to work okay (TH)
+        return 3
+    elseif find(str,"^\254\255") then
+        f:seek('set',2)
+        return 2
+    elseif find(str,"^\255\254") then
+        f:seek('set',2)
+        return 1
+    elseif find(str,"^\239\187\191") then
+        f:seek('set',3)
+        return 0
+    else
+        f:seek('set')
+        return 0
+    end
+end
+
+function unicode.utf16_to_utf8(str, endian) -- maybe a gsub is faster or an lpeg
+    local result, tmp, n, m, p = { }, { }, 0, 0, 0
+    -- lf | cr | crlf / (cr:13, lf:10)
+    local function doit()
+        if n == 10 then
+            if p ~= 13 then
+                result[#result+1] = concat(tmp)
+                tmp = { }
+                p = 0
+            end
+        elseif n == 13 then
+            result[#result+1] = concat(tmp)
+            tmp = { }
+            p = n
+        else
+            tmp[#tmp+1] = utfchar(n)
+            p = 0
+        end
+    end
+    for l,r in bytepairs(str) do
+        if r then
+            if endian then
+                n = l*256 + r
+            else
+                n = r*256 + l
+            end
+            if m > 0 then
+                n = (m-0xD800)*0x400 + (n-0xDC00) + 0x10000
+                m = 0
+                doit()
+            elseif n >= 0xD800 and n <= 0xDBFF then
+                m = n
+            else
+                doit()
+            end
+        end
+    end
+    if #tmp > 0 then
+        result[#result+1] = concat(tmp)
+    end
+    return result
+end
+
+function unicode.utf32_to_utf8(str, endian)
+    local result = { }
+    local tmp, n, m, p = { }, 0, -1, 0
+    -- lf | cr | crlf / (cr:13, lf:10)
+    local function doit()
+        if n == 10 then
+            if p ~= 13 then
+                result[#result+1] = concat(tmp)
+                tmp = { }
+                p = 0
+            end
+        elseif n == 13 then
+            result[#result+1] = concat(tmp)
+            tmp = { }
+            p = n
+        else
+            tmp[#tmp+1] = utfchar(n)
+            p = 0
+        end
+    end
+    for a,b in bytepairs(str) do
+        if a and b then
+            if m < 0 then
+                if endian then
+                    m = a*256*256*256 + b*256*256
+                else
+                    m = b*256 + a
+                end
+            else
+                if endian then
+                    n = m + a*256 + b
+                else
+                    n = m + b*256*256*256 + a*256*256
+                end
+                m = -1
+                doit()
+            end
+        else
+            break
+        end
+    end
+    if #tmp > 0 then
+        result[#result+1] = concat(tmp)
+    end
+    return result
+end
+
+local function little(c)
+    local b = byte(c) -- b = c:byte()
+    if b < 0x10000 then
+        return char(b%256,b/256)
+    else
+        b = b - 0x10000
+        local b1, b2 = b/1024 + 0xD800, b%1024 + 0xDC00
+        return char(b1%256,b1/256,b2%256,b2/256)
+    end
+end
+
+local function big(c)
+    local b = byte(c)
+    if b < 0x10000 then
+        return char(b/256,b%256)
+    else
+        b = b - 0x10000
+        local b1, b2 = b/1024 + 0xD800, b%1024 + 0xDC00
+        return char(b1/256,b1%256,b2/256,b2%256)
+    end
+end
+
+function unicode.utf8_to_utf16(str,littleendian)
+    if littleendian then
+        return char(255,254) .. utfgsub(str,".",little)
+    else
+        return char(254,255) .. utfgsub(str,".",big)
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-math'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local floor, sin, cos, tan = math.floor, math.sin, math.cos, math.tan
+
+if not math.round then
+    function math.round(x)
+        return floor(x + 0.5)
+    end
+end
+
+if not math.div then
+    function math.div(n,m)
+        return floor(n/m)
+    end
+end
+
+if not math.mod then
+    function math.mod(n,m)
+        return n % m
+    end
+end
+
+local pipi = 2*math.pi/360
+
+function math.sind(d)
+    return sin(d*pipi)
+end
+
+function math.cosd(d)
+    return cos(d*pipi)
+end
+
+function math.tand(d)
+    return tan(d*pipi)
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-utils'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- hm, quite unreadable
+
+local gsub = string.gsub
+local concat = table.concat
+local type, next = type, next
+
+if not utils        then utils        = { } end
+if not utils.merger then utils.merger = { } end
+if not utils.lua    then utils.lua    = { } end
+
+utils.merger.m_begin = "begin library merge"
+utils.merger.m_end   = "end library merge"
+utils.merger.pattern =
+    "%c+" ..
+    "%-%-%s+" .. utils.merger.m_begin ..
+    "%c+(.-)%c+" ..
+    "%-%-%s+" .. utils.merger.m_end ..
+    "%c+"
+
+function utils.merger._self_fake_()
+    return
+        "-- " .. "created merged file" .. "\n\n" ..
+        "-- " .. utils.merger.m_begin  .. "\n\n" ..
+        "-- " .. utils.merger.m_end    .. "\n\n"
+end
+
+function utils.report(...)
+    print(...)
+end
+
+utils.merger.strip_comment = true
+
+function utils.merger._self_load_(name)
+    local f, data = io.open(name), ""
+    if f then
+        utils.report("reading merge from %s",name)
+        data = f:read("*all")
+        f:close()
+    else
+        utils.report("unknown file to merge %s",name)
+    end
+    if data and utils.merger.strip_comment then
+        -- saves some 20K
+        data = gsub(data,"%-%-~[^\n\r]*[\r\n]", "")
+    end
+    return data or ""
+end
+
+function utils.merger._self_save_(name, data)
+    if data ~= "" then
+        local f = io.open(name,'w')
+        if f then
+            utils.report("saving merge from %s",name)
+            f:write(data)
+            f:close()
+        end
+    end
+end
+
+function utils.merger._self_swap_(data,code)
+    if data ~= "" then
+        return (gsub(data,utils.merger.pattern, function(s)
+            return "\n\n" .. "-- "..utils.merger.m_begin .. "\n" .. code .. "\n" .. "-- "..utils.merger.m_end .. "\n\n"
+        end, 1))
+    else
+        return ""
+    end
+end
+
+--~ stripper:
+--~
+--~ data = gsub(data,"%-%-~[^\n]*\n","")
+--~ data = gsub(data,"\n\n+","\n")
+
+function utils.merger._self_libs_(libs,list)
+    local result, f, frozen = { }, nil, false
+    result[#result+1] = "\n"
+    if type(libs) == 'string' then libs = { libs } end
+    if type(list) == 'string' then list = { list } end
+    local foundpath = nil
+    for i=1,#libs do
+        local lib = libs[i]
+        for j=1,#list do
+            local pth = gsub(list[j],"\\","/") -- file.clean_path
+            utils.report("checking library path %s",pth)
+            local name = pth .. "/" .. lib
+            if lfs.isfile(name) then
+                foundpath = pth
+            end
+        end
+        if foundpath then break end
+    end
+    if foundpath then
+        utils.report("using library path %s",foundpath)
+        local right, wrong = { }, { }
+        for i=1,#libs do
+            local lib = libs[i]
+            local fullname = foundpath .. "/" .. lib
+            if lfs.isfile(fullname) then
+            --  right[#right+1] = lib
+                utils.report("merging library %s",fullname)
+                result[#result+1] = "do -- create closure to overcome 200 locals limit"
+                result[#result+1] = io.loaddata(fullname,true)
+                result[#result+1] = "end -- of closure"
+            else
+            --  wrong[#wrong+1] = lib
+                utils.report("no library %s",fullname)
+            end
+        end
+        if #right > 0 then
+            utils.report("merged libraries: %s",concat(right," "))
+        end
+        if #wrong > 0 then
+            utils.report("skipped libraries: %s",concat(wrong," "))
+        end
+    else
+        utils.report("no valid library path found")
+    end
+    return concat(result, "\n\n")
+end
+
+function utils.merger.selfcreate(libs,list,target)
+    if target then
+        utils.merger._self_save_(
+            target,
+            utils.merger._self_swap_(
+                utils.merger._self_fake_(),
+                utils.merger._self_libs_(libs,list)
+            )
+        )
+    end
+end
+
+function utils.merger.selfmerge(name,libs,list,target)
+    utils.merger._self_save_(
+        target or name,
+        utils.merger._self_swap_(
+            utils.merger._self_load_(name),
+            utils.merger._self_libs_(libs,list)
+        )
+    )
+end
+
+function utils.merger.selfclean(name)
+    utils.merger._self_save_(
+        name,
+        utils.merger._self_swap_(
+            utils.merger._self_load_(name),
+            ""
+        )
+    )
+end
+
+function utils.lua.compile(luafile, lucfile, cleanup, strip) -- defaults: cleanup=false strip=true
+ -- utils.report("compiling",luafile,"into",lucfile)
+    os.remove(lucfile)
+    local command = "-o " .. string.quote(lucfile) .. " " .. string.quote(luafile)
+    if strip ~= false then
+        command = "-s " .. command
+    end
+    local done = (os.spawn("texluac " .. command) == 0) or (os.spawn("luac " .. command) == 0)
+    if done and cleanup == true and lfs.isfile(lucfile) and lfs.isfile(luafile) then
+     -- utils.report("removing",luafile)
+        os.remove(luafile)
+    end
+    return done
+end
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-aux'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- for inline, no store split : for s in string.gmatch(str,",* *([^,]+)") do .. end
+
+aux = aux or { }
+
+local concat, format, gmatch = table.concat, string.format, string.gmatch
+local tostring, type = tostring, type
+local lpegmatch = lpeg.match
+
+local P, R, V = lpeg.P, lpeg.R, lpeg.V
+
+local escape, left, right = P("\\"), P('{'), P('}')
+
+lpeg.patterns.balanced = P {
+    [1] = ((escape * (left+right)) + (1 - (left+right)) + V(2))^0,
+    [2] = left * V(1) * right
+}
+
+local space     = lpeg.P(' ')
+local equal     = lpeg.P("=")
+local comma     = lpeg.P(",")
+local lbrace    = lpeg.P("{")
+local rbrace    = lpeg.P("}")
+local nobrace   = 1 - (lbrace+rbrace)
+local nested    = lpeg.P { lbrace * (nobrace + lpeg.V(1))^0 * rbrace }
+local spaces    = space^0
+
+local value     = lpeg.P(lbrace * lpeg.C((nobrace + nested)^0) * rbrace) + lpeg.C((nested + (1-comma))^0)
+
+local key       = lpeg.C((1-equal-comma)^1)
+local pattern_a = (space+comma)^0 * (key * equal * value + key * lpeg.C(""))
+local pattern_c = (space+comma)^0 * (key * equal * value)
+
+local key       = lpeg.C((1-space-equal-comma)^1)
+local pattern_b = spaces * comma^0 * spaces * (key * ((spaces * equal * spaces * value) + lpeg.C("")))
+
+-- "a=1, b=2, c=3, d={a{b,c}d}, e=12345, f=xx{a{b,c}d}xx, g={}" : outer {} removes, leading spaces ignored
+
+local hash = { }
+
+local function set(key,value) -- using Carg is slower here
+    hash[key] = value
+end
+
+local pattern_a_s = (pattern_a/set)^1
+local pattern_b_s = (pattern_b/set)^1
+local pattern_c_s = (pattern_c/set)^1
+
+aux.settings_to_hash_pattern_a = pattern_a_s
+aux.settings_to_hash_pattern_b = pattern_b_s
+aux.settings_to_hash_pattern_c = pattern_c_s
+
+function aux.make_settings_to_hash_pattern(set,how)
+    if how == "strict" then
+        return (pattern_c/set)^1
+    elseif how == "tolerant" then
+        return (pattern_b/set)^1
+    else
+        return (pattern_a/set)^1
+    end
+end
+
+function aux.settings_to_hash(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        if moretolerant then
+            lpegmatch(pattern_b_s,str)
+        else
+            lpegmatch(pattern_a_s,str)
+        end
+        return hash
+    else
+        return { }
+    end
+end
+
+function aux.settings_to_hash_tolerant(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        lpegmatch(pattern_b_s,str)
+        return hash
+    else
+        return { }
+    end
+end
+
+function aux.settings_to_hash_strict(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        lpegmatch(pattern_c_s,str)
+        return next(hash) and hash
+    else
+        return nil
+    end
+end
+
+local separator = comma * space^0
+local value     = lpeg.P(lbrace * lpeg.C((nobrace + nested)^0) * rbrace) + lpeg.C((nested + (1-comma))^0)
+local pattern   = lpeg.Ct(value*(separator*value)^0)
+
+-- "aap, {noot}, mies" : outer {} removes, leading spaces ignored
+
+aux.settings_to_array_pattern = pattern
+
+-- we could use a weak table as cache
+
+function aux.settings_to_array(str)
+    if not str or str == "" then
+        return { }
+    else
+        return lpegmatch(pattern,str)
+    end
+end
+
+local function set(t,v)
+    t[#t+1] = v
+end
+
+local value   = lpeg.P(lpeg.Carg(1)*value) / set
+local pattern = value*(separator*value)^0 * lpeg.Carg(1)
+
+function aux.add_settings_to_array(t,str)
+    return lpegmatch(pattern,str,nil,t)
+end
+
+function aux.hash_to_string(h,separator,yes,no,strict,omit)
+    if h then
+        local t, s = { }, table.sortedkeys(h)
+        omit = omit and table.tohash(omit)
+        for i=1,#s do
+            local key = s[i]
+            if not omit or not omit[key] then
+                local value = h[key]
+                if type(value) == "boolean" then
+                    if yes and no then
+                        if value then
+                            t[#t+1] = key .. '=' .. yes
+                        elseif not strict then
+                            t[#t+1] = key .. '=' .. no
+                        end
+                    elseif value or not strict then
+                        t[#t+1] = key .. '=' .. tostring(value)
+                    end
+                else
+                    t[#t+1] = key .. '=' .. value
+                end
+            end
+        end
+        return concat(t,separator or ",")
+    else
+        return ""
+    end
+end
+
+function aux.array_to_string(a,separator)
+    if a then
+        return concat(a,separator or ",")
+    else
+        return ""
+    end
+end
+
+function aux.settings_to_set(str,t)
+    t = t or { }
+    for s in gmatch(str,"%s*([^,]+)") do
+        t[s] = true
+    end
+    return t
+end
+
+local value     = lbrace * lpeg.C((nobrace + nested)^0) * rbrace
+local pattern   = lpeg.Ct((space + value)^0)
+
+function aux.arguments_to_table(str)
+    return lpegmatch(pattern,str)
+end
+
+-- temporary here
+
+function aux.getparameters(self,class,parentclass,settings)
+    local sc = self[class]
+    if not sc then
+        sc = table.clone(self[parent])
+        self[class] = sc
+    end
+    aux.settings_to_hash(settings,sc)
+end
+
+-- temporary here
+
+local digit         = lpeg.R("09")
+local period        = lpeg.P(".")
+local zero          = lpeg.P("0")
+local trailingzeros = zero^0 * -digit -- suggested by Roberto R
+local case_1        = period * trailingzeros / ""
+local case_2        = period * (digit - trailingzeros)^1 * (trailingzeros / "")
+local number        = digit^1 * (case_1 + case_2)
+local stripper      = lpeg.Cs((number + 1)^0)
+
+--~ local sample = "bla 11.00 bla 11 bla 0.1100 bla 1.00100 bla 0.00 bla 0.001 bla 1.1100 bla 0.100100100 bla 0.00100100100"
+--~ collectgarbage("collect")
+--~ str = string.rep(sample,10000)
+--~ local ts = os.clock()
+--~ lpegmatch(stripper,str)
+--~ print(#str, os.clock()-ts, lpegmatch(stripper,sample))
+
+lpeg.patterns.strip_zeros = stripper
+
+function aux.strip_zeros(str)
+    return lpegmatch(stripper,str)
+end
+
+function aux.definetable(target) -- defines undefined tables
+    local composed, t = nil, { }
+    for name in gmatch(target,"([^%.]+)") do
+        if composed then
+            composed = composed .. "." .. name
+        else
+            composed = name
+        end
+        t[#t+1] = format("%s = %s or { }",composed,composed)
+    end
+    return concat(t,"\n")
+end
+
+function aux.accesstable(target)
+    local t = _G
+    for name in gmatch(target,"([^%.]+)") do
+        t = t[name]
+    end
+    return t
+end
+
+-- as we use this a lot ...
+
+--~ function aux.cachefunction(action,weak)
+--~     local cache = { }
+--~     if weak then
+--~         setmetatable(cache, { __mode = "kv" } )
+--~     end
+--~     local function reminder(str)
+--~         local found = cache[str]
+--~         if not found then
+--~             found = action(str)
+--~             cache[str] = found
+--~         end
+--~         return found
+--~     end
+--~     return reminder, cache
+--~ end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-tra'] = {
+    version   = 1.001,
+    comment   = "companion to trac-tra.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- the <anonymous> tag is kind of generic and used for functions that are not
+-- bound to a variable, like node.new, node.copy etc (contrary to for instance
+-- node.has_attribute which is bound to a has_attribute local variable in mkiv)
+
+local debug = require "debug"
+
+local getinfo = debug.getinfo
+local type, next = type, next
+local concat = table.concat
+local format, find, lower, gmatch, gsub = string.format, string.find, string.lower, string.gmatch, string.gsub
+
+debugger = debugger or { }
+
+local counters = { }
+local names = { }
+
+-- one
+
+local function hook()
+    local f = getinfo(2,"f").func
+    local n = getinfo(2,"Sn")
+--  if n.what == "C" and n.name then print (n.namewhat .. ': ' .. n.name) end
+    if f then
+        local cf = counters[f]
+        if cf == nil then
+            counters[f] = 1
+            names[f] = n
+        else
+            counters[f] = cf + 1
+        end
+    end
+end
+local function getname(func)
+    local n = names[func]
+    if n then
+        if n.what == "C" then
+            return n.name or '<anonymous>'
+        else
+            -- source short_src linedefined what name namewhat nups func
+            local name = n.name or n.namewhat or n.what
+            if not name or name == "" then name = "?" end
+            return format("%s : %s : %s", n.short_src or "unknown source", n.linedefined or "--", name)
+        end
+    else
+        return "unknown"
+    end
+end
+function debugger.showstats(printer,threshold)
+    printer   = printer or texio.write or print
+    threshold = threshold or 0
+    local total, grandtotal, functions = 0, 0, 0
+    printer("\n") -- ugly but ok
+ -- table.sort(counters)
+    for func, count in next, counters do
+        if count > threshold then
+            local name = getname(func)
+            if not find(name,"for generator") then
+                printer(format("%8i  %s", count, name))
+                total = total + count
+            end
+        end
+        grandtotal = grandtotal + count
+        functions = functions + 1
+    end
+    printer(format("functions: %s, total: %s, grand total: %s, threshold: %s\n", functions, total, grandtotal, threshold))
+end
+
+-- two
+
+--~ local function hook()
+--~     local n = getinfo(2)
+--~     if n.what=="C" and not n.name then
+--~         local f = tostring(debug.traceback())
+--~         local cf = counters[f]
+--~         if cf == nil then
+--~             counters[f] = 1
+--~             names[f] = n
+--~         else
+--~             counters[f] = cf + 1
+--~         end
+--~     end
+--~ end
+--~ function debugger.showstats(printer,threshold)
+--~     printer   = printer or texio.write or print
+--~     threshold = threshold or 0
+--~     local total, grandtotal, functions = 0, 0, 0
+--~     printer("\n") -- ugly but ok
+--~  -- table.sort(counters)
+--~     for func, count in next, counters do
+--~         if count > threshold then
+--~             printer(format("%8i  %s", count, func))
+--~             total = total + count
+--~         end
+--~         grandtotal = grandtotal + count
+--~         functions = functions + 1
+--~     end
+--~     printer(format("functions: %s, total: %s, grand total: %s, threshold: %s\n", functions, total, grandtotal, threshold))
+--~ end
+
+-- rest
+
+function debugger.savestats(filename,threshold)
+    local f = io.open(filename,'w')
+    if f then
+        debugger.showstats(function(str) f:write(str) end,threshold)
+        f:close()
+    end
+end
+
+function debugger.enable()
+    debug.sethook(hook,"c")
+end
+
+function debugger.disable()
+    debug.sethook()
+--~ counters[debug.getinfo(2,"f").func] = nil
+end
+
+function debugger.tracing()
+    local n = tonumber(os.env['MTX.TRACE.CALLS']) or tonumber(os.env['MTX_TRACE_CALLS']) or 0
+    if n > 0 then
+        function debugger.tracing() return true  end ; return true
+    else
+        function debugger.tracing() return false end ; return false
+    end
+end
+
+--~ debugger.enable()
+
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+
+--~ debugger.disable()
+
+--~ print("")
+--~ debugger.showstats()
+--~ print("")
+--~ debugger.showstats(print,3)
+
+setters      = setters      or { }
+setters.data = setters.data or { }
+
+--~ local function set(t,what,value)
+--~     local data, done = t.data, t.done
+--~     if type(what) == "string" then
+--~         what = aux.settings_to_array(what) -- inefficient but ok
+--~     end
+--~     for i=1,#what do
+--~         local w = what[i]
+--~         for d, f in next, data do
+--~             if done[d] then
+--~                 -- prevent recursion due to wildcards
+--~             elseif find(d,w) then
+--~                 done[d] = true
+--~                 for i=1,#f do
+--~                     f[i](value)
+--~                 end
+--~             end
+--~         end
+--~     end
+--~ end
+
+local function set(t,what,value)
+    local data, done = t.data, t.done
+    if type(what) == "string" then
+        what = aux.settings_to_hash(what) -- inefficient but ok
+    end
+    for w, v in next, what do
+        if v == "" then
+            v = value
+        else
+            v = toboolean(v)
+        end
+        for d, f in next, data do
+            if done[d] then
+                -- prevent recursion due to wildcards
+            elseif find(d,w) then
+                done[d] = true
+                for i=1,#f do
+                    f[i](v)
+                end
+            end
+        end
+    end
+end
+
+local function reset(t)
+    for d, f in next, t.data do
+        for i=1,#f do
+            f[i](false)
+        end
+    end
+end
+
+local function enable(t,what)
+    set(t,what,true)
+end
+
+local function disable(t,what)
+    local data = t.data
+    if not what or what == "" then
+        t.done = { }
+        reset(t)
+    else
+        set(t,what,false)
+    end
+end
+
+function setters.register(t,what,...)
+    local data = t.data
+    what = lower(what)
+    local w = data[what]
+    if not w then
+        w = { }
+        data[what] = w
+    end
+    for _, fnc in next, { ... } do
+        local typ = type(fnc)
+        if typ == "function" then
+            w[#w+1] = fnc
+        elseif typ == "string" then
+            w[#w+1] = function(value) set(t,fnc,value,nesting) end
+        end
+    end
+end
+
+function setters.enable(t,what)
+    local e = t.enable
+    t.enable, t.done = enable, { }
+    enable(t,string.simpleesc(tostring(what)))
+    t.enable, t.done = e, { }
+end
+
+function setters.disable(t,what)
+    local e = t.disable
+    t.disable, t.done = disable, { }
+    disable(t,string.simpleesc(tostring(what)))
+    t.disable, t.done = e, { }
+end
+
+function setters.reset(t)
+    t.done = { }
+    reset(t)
+end
+
+function setters.list(t) -- pattern
+    local list = table.sortedkeys(t.data)
+    local user, system = { }, { }
+    for l=1,#list do
+        local what = list[l]
+        if find(what,"^%*") then
+            system[#system+1] = what
+        else
+            user[#user+1] = what
+        end
+    end
+    return user, system
+end
+
+function setters.show(t)
+    commands.writestatus("","")
+    local list = setters.list(t)
+    for k=1,#list do
+        commands.writestatus(t.name,list[k])
+    end
+    commands.writestatus("","")
+end
+
+-- we could have used a bit of oo and the trackers:enable syntax but
+-- there is already a lot of code around using the singular tracker
+
+-- we could make this into a module
+
+function setters.new(name)
+    local t
+    t = {
+        data     = { },
+        name     = name,
+        enable   = function(...) setters.enable  (t,...) end,
+        disable  = function(...) setters.disable (t,...) end,
+        register = function(...) setters.register(t,...) end,
+        list     = function(...) setters.list    (t,...) end,
+        show     = function(...) setters.show    (t,...) end,
+    }
+    setters.data[name] = t
+    return t
+end
+
+trackers    = setters.new("trackers")
+directives  = setters.new("directives")
+experiments = setters.new("experiments")
+
+-- nice trick: we overload two of the directives related functions with variants that
+-- do tracing (itself using a tracker) .. proof of concept
+
+local trace_directives  = false local trace_directives  = false  trackers.register("system.directives",  function(v) trace_directives  = v end)
+local trace_experiments = false local trace_experiments = false  trackers.register("system.experiments", function(v) trace_experiments = v end)
+
+local e = directives.enable
+local d = directives.disable
+
+function directives.enable(...)
+    commands.writestatus("directives","enabling: %s",concat({...}," "))
+    e(...)
+end
+
+function directives.disable(...)
+    commands.writestatus("directives","disabling: %s",concat({...}," "))
+    d(...)
+end
+
+local e = experiments.enable
+local d = experiments.disable
+
+function experiments.enable(...)
+    commands.writestatus("experiments","enabling: %s",concat({...}," "))
+    e(...)
+end
+
+function experiments.disable(...)
+    commands.writestatus("experiments","disabling: %s",concat({...}," "))
+    d(...)
+end
+
+-- a useful example
+
+directives.register("system.nostatistics", function(v)
+    statistics.enable = not v
+end)
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['luat-env'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- A former version provided functionality for non embeded core
+-- scripts i.e. runtime library loading. Given the amount of
+-- Lua code we use now, this no longer makes sense. Much of this
+-- evolved before bytecode arrays were available and so a lot of
+-- code has disappeared already.
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+local format, sub, match, gsub, find = string.format, string.sub, string.match, string.gsub, string.find
+local unquote, quote = string.unquote, string.quote
+
+-- precautions
+
+os.setlocale(nil,nil) -- useless feature and even dangerous in luatex
+
+function os.setlocale()
+    -- no way you can mess with it
+end
+
+-- dirty tricks
+
+if arg and (arg[0] == 'luatex' or arg[0] == 'luatex.exe') and arg[1] == "--luaonly" then
+    arg[-1]=arg[0] arg[0]=arg[2] for k=3,#arg do arg[k-2]=arg[k] end arg[#arg]=nil arg[#arg]=nil
+end
+
+if profiler and os.env["MTX_PROFILE_RUN"] == "YES" then
+    profiler.start("luatex-profile.log")
+end
+
+-- environment
+
+environment             = environment or { }
+environment.arguments   = { }
+environment.files       = { }
+environment.sortedflags = nil
+
+if not environment.jobname or environment.jobname == "" then if tex then environment.jobname = tex.jobname end end
+if not environment.version or environment.version == "" then             environment.version = "unknown"   end
+if not environment.jobname                              then             environment.jobname = "unknown"   end
+
+function environment.initialize_arguments(arg)
+    local arguments, files = { }, { }
+    environment.arguments, environment.files, environment.sortedflags = arguments, files, nil
+    for index=1,#arg do
+        local argument = arg[index]
+        if index > 0 then
+            local flag, value = match(argument,"^%-+(.-)=(.-)$")
+            if flag then
+                arguments[flag] = unquote(value or "")
+            else
+                flag = match(argument,"^%-+(.+)")
+                if flag then
+                    arguments[flag] = true
+                else
+                    files[#files+1] = argument
+                end
+            end
+        end
+    end
+    environment.ownname = environment.ownname or arg[0] or 'unknown.lua'
+end
+
+function environment.setargument(name,value)
+    environment.arguments[name] = value
+end
+
+-- todo: defaults, better checks e.g on type (boolean versus string)
+--
+-- tricky: too many hits when we support partials unless we add
+-- a registration of arguments so from now on we have 'partial'
+
+function environment.argument(name,partial)
+    local arguments, sortedflags = environment.arguments, environment.sortedflags
+    if arguments[name] then
+        return arguments[name]
+    elseif partial then
+        if not sortedflags then
+            sortedflags = table.sortedkeys(arguments)
+            for k=1,#sortedflags do
+                sortedflags[k] = "^" .. sortedflags[k]
+            end
+            environment.sortedflags = sortedflags
+        end
+        -- example of potential clash: ^mode ^modefile
+        for k=1,#sortedflags do
+            local v = sortedflags[k]
+            if find(name,v) then
+                return arguments[sub(v,2,#v)]
+            end
+        end
+    end
+    return nil
+end
+
+environment.argument("x",true)
+
+function environment.split_arguments(separator) -- rather special, cut-off before separator
+    local done, before, after = false, { }, { }
+    local original_arguments = environment.original_arguments
+    for k=1,#original_arguments do
+        local v = original_arguments[k]
+        if not done and v == separator then
+            done = true
+        elseif done then
+            after[#after+1] = v
+        else
+            before[#before+1] = v
+        end
+    end
+    return before, after
+end
+
+function environment.reconstruct_commandline(arg,noquote)
+    arg = arg or environment.original_arguments
+    if noquote and #arg == 1 then
+        local a = arg[1]
+        a = resolvers.resolve(a)
+        a = unquote(a)
+        return a
+    elseif #arg > 0 then
+        local result = { }
+        for i=1,#arg do
+            local a = arg[i]
+            a = resolvers.resolve(a)
+            a = unquote(a)
+            a = gsub(a,'"','\\"') -- tricky
+            if find(a," ") then
+                result[#result+1] = quote(a)
+            else
+                result[#result+1] = a
+            end
+        end
+        return table.join(result," ")
+    else
+        return ""
+    end
+end
+
+if arg then
+
+    -- new, reconstruct quoted snippets (maybe better just remove the " then and add them later)
+    local newarg, instring = { }, false
+
+    for index=1,#arg do
+        local argument = arg[index]
+        if find(argument,"^\"") then
+            newarg[#newarg+1] = gsub(argument,"^\"","")
+            if not find(argument,"\"$") then
+                instring = true
+            end
+        elseif find(argument,"\"$") then
+            newarg[#newarg] = newarg[#newarg] .. " " .. gsub(argument,"\"$","")
+            instring = false
+        elseif instring then
+            newarg[#newarg] = newarg[#newarg] .. " " .. argument
+        else
+            newarg[#newarg+1] = argument
+        end
+    end
+    for i=1,-5,-1 do
+        newarg[i] = arg[i]
+    end
+
+    environment.initialize_arguments(newarg)
+    environment.original_arguments = newarg
+    environment.raw_arguments = arg
+
+    arg = { } -- prevent duplicate handling
+
+end
+
+-- weird place ... depends on a not yet loaded module
+
+function environment.texfile(filename)
+    return resolvers.find_file(filename,'tex')
+end
+
+function environment.luafile(filename)
+    local resolved = resolvers.find_file(filename,'tex') or ""
+    if resolved ~= "" then
+        return resolved
+    end
+    resolved = resolvers.find_file(filename,'texmfscripts') or ""
+    if resolved ~= "" then
+        return resolved
+    end
+    return resolvers.find_file(filename,'luatexlibs') or ""
+end
+
+environment.loadedluacode = loadfile -- can be overloaded
+
+--~ function environment.loadedluacode(name)
+--~     if os.spawn("texluac -s -o texluac.luc " .. name) == 0 then
+--~         local chunk = loadstring(io.loaddata("texluac.luc"))
+--~         os.remove("texluac.luc")
+--~         return chunk
+--~     else
+--~         environment.loadedluacode = loadfile -- can be overloaded
+--~         return loadfile(name)
+--~     end
+--~ end
+
+function environment.luafilechunk(filename) -- used for loading lua bytecode in the format
+    filename = file.replacesuffix(filename, "lua")
+    local fullname = environment.luafile(filename)
+    if fullname and fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading file %s", fullname)
+        end
+        return environment.loadedluacode(fullname)
+    else
+        if trace_locating then
+            logs.report("fileio","unknown file %s", filename)
+        end
+        return nil
+    end
+end
+
+-- the next ones can use the previous ones / combine
+
+function environment.loadluafile(filename, version)
+    local lucname, luaname, chunk
+    local basename = file.removesuffix(filename)
+    if basename == filename then
+        lucname, luaname = basename .. ".luc",  basename .. ".lua"
+    else
+        lucname, luaname = nil, basename -- forced suffix
+    end
+    -- when not overloaded by explicit suffix we look for a luc file first
+    local fullname = (lucname and environment.luafile(lucname)) or ""
+    if fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading %s", fullname)
+        end
+        chunk = loadfile(fullname) -- this way we don't need a file exists check
+    end
+    if chunk then
+        assert(chunk)()
+        if version then
+            -- we check of the version number of this chunk matches
+            local v = version -- can be nil
+            if modules and modules[filename] then
+                v = modules[filename].version -- new method
+            elseif versions and versions[filename] then
+                v = versions[filename]        -- old method
+            end
+            if v == version then
+                return true
+            else
+                if trace_locating then
+                    logs.report("fileio","version mismatch for %s: lua=%s, luc=%s", filename, v, version)
+                end
+                environment.loadluafile(filename)
+            end
+        else
+            return true
+        end
+    end
+    fullname = (luaname and environment.luafile(luaname)) or ""
+    if fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading %s", fullname)
+        end
+        chunk = loadfile(fullname) -- this way we don't need a file exists check
+        if not chunk then
+            if trace_locating then
+                logs.report("fileio","unknown file %s", filename)
+            end
+        else
+            assert(chunk)()
+            return true
+        end
+    end
+    return false
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-inf'] = {
+    version   = 1.001,
+    comment   = "companion to trac-inf.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format = string.format
+
+local statusinfo, n, registered = { }, 0, { }
+
+statistics = statistics or { }
+
+statistics.enable    = true
+statistics.threshold = 0.05
+
+-- timing functions
+
+local clock = os.gettimeofday or os.clock
+
+local notimer
+
+function statistics.hastimer(instance)
+    return instance and instance.starttime
+end
+
+function statistics.resettiming(instance)
+    if not instance then
+        notimer = { timing = 0, loadtime = 0 }
+    else
+        instance.timing, instance.loadtime = 0, 0
+    end
+end
+
+function statistics.starttiming(instance)
+    if not instance then
+        notimer = { }
+        instance = notimer
+    end
+    local it = instance.timing
+    if not it then
+        it = 0
+    end
+    if it == 0 then
+        instance.starttime = clock()
+        if not instance.loadtime then
+            instance.loadtime = 0
+        end
+    else
+--~         logs.report("system","nested timing (%s)",tostring(instance))
+    end
+    instance.timing = it + 1
+end
+
+function statistics.stoptiming(instance, report)
+    if not instance then
+        instance = notimer
+    end
+    if instance then
+        local it = instance.timing
+        if it > 1 then
+            instance.timing = it - 1
+        else
+            local starttime = instance.starttime
+            if starttime then
+                local stoptime = clock()
+                local loadtime = stoptime - starttime
+                instance.stoptime = stoptime
+                instance.loadtime = instance.loadtime + loadtime
+                if report then
+                    statistics.report("load time %0.3f",loadtime)
+                end
+                instance.timing = 0
+                return loadtime
+            end
+        end
+    end
+    return 0
+end
+
+function statistics.elapsedtime(instance)
+    if not instance then
+        instance = notimer
+    end
+    return format("%0.3f",(instance and instance.loadtime) or 0)
+end
+
+function statistics.elapsedindeed(instance)
+    if not instance then
+        instance = notimer
+    end
+    local t = (instance and instance.loadtime) or 0
+    return t > statistics.threshold
+end
+
+function statistics.elapsedseconds(instance,rest) -- returns nil if 0 seconds
+    if statistics.elapsedindeed(instance) then
+        return format("%s seconds %s", statistics.elapsedtime(instance),rest or "")
+    end
+end
+
+-- general function
+
+function statistics.register(tag,fnc)
+    if statistics.enable and type(fnc) == "function" then
+        local rt = registered[tag] or (#statusinfo + 1)
+        statusinfo[rt] = { tag, fnc }
+        registered[tag] = rt
+        if #tag > n then n = #tag end
+    end
+end
+
+function statistics.show(reporter)
+    if statistics.enable then
+        if not reporter then reporter = function(tag,data,n) texio.write_nl(tag .. " " .. data) end end
+        -- this code will move
+        local register = statistics.register
+        register("luatex banner", function()
+            return string.lower(status.banner)
+        end)
+        register("control sequences", function()
+            return format("%s of %s", status.cs_count, status.hash_size+status.hash_extra)
+        end)
+        register("callbacks", function()
+            local total, indirect = status.callbacks or 0, status.indirect_callbacks or 0
+            return format("direct: %s, indirect: %s, total: %s", total-indirect, indirect, total)
+        end)
+        register("current memory usage", statistics.memused)
+        register("runtime",statistics.runtime)
+--         --
+        for i=1,#statusinfo do
+            local s = statusinfo[i]
+            local r = s[2]()
+            if r then
+                reporter(s[1],r,n)
+            end
+        end
+        texio.write_nl("") -- final newline
+        statistics.enable = false
+    end
+end
+
+function statistics.show_job_stat(tag,data,n)
+    texio.write_nl(format("%-15s: %s - %s","mkiv lua stats",tag:rpadd(n," "),data))
+end
+
+function statistics.memused() -- no math.round yet -)
+    local round = math.round or math.floor
+    return format("%s MB (ctx: %s MB)",round(collectgarbage("count")/1000), round(status.luastate_bytes/1000000))
+end
+
+if statistics.runtime then
+    -- already loaded and set
+elseif luatex and luatex.starttime then
+    statistics.starttime = luatex.starttime
+    statistics.loadtime = 0
+    statistics.timing = 0
+else
+    statistics.starttiming(statistics)
+end
+
+function statistics.runtime()
+    statistics.stoptiming(statistics)
+    return statistics.formatruntime(statistics.elapsedtime(statistics))
+end
+
+function statistics.formatruntime(runtime)
+    return format("%s seconds", statistics.elapsedtime(statistics))
+end
+
+function statistics.timed(action,report)
+    local timer = { }
+    report = report or logs.simple
+    statistics.starttiming(timer)
+    action()
+    statistics.stoptiming(timer)
+    report("total runtime: %s",statistics.elapsedtime(timer))
+end
+
+-- where, not really the best spot for this:
+
+commands = commands or { }
+
+local timer
+
+function commands.resettimer()
+    statistics.resettiming(timer)
+    statistics.starttiming(timer)
+end
+
+function commands.elapsedtime()
+    statistics.stoptiming(timer)
+    tex.sprint(statistics.elapsedtime(timer))
+end
+
+commands.resettimer()
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-log'] = {
+    version   = 1.001,
+    comment   = "companion to trac-log.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- this is old code that needs an overhaul
+
+--~ io.stdout:setvbuf("no")
+--~ io.stderr:setvbuf("no")
+
+local write_nl, write = texio.write_nl or print, texio.write or io.write
+local format, gmatch = string.format, string.gmatch
+local texcount = tex and tex.count
+
+if texlua then
+    write_nl = print
+    write    = io.write
+end
+
+--[[ldx--
+<p>This is a prelude to a more extensive logging module. For the sake
+of parsing log files, in addition to the standard logging we will
+provide an <l n='xml'/> structured file. Actually, any logging that
+is hooked into callbacks will be \XML\ by default.</p>
+--ldx]]--
+
+logs     = logs     or { }
+logs.xml = logs.xml or { }
+logs.tex = logs.tex or { }
+
+--[[ldx--
+<p>This looks pretty ugly but we need to speed things up a bit.</p>
+--ldx]]--
+
+logs.moreinfo = [[
+more information about ConTeXt and the tools that come with it can be found at:
+
+maillist : ntg-context@ntg.nl / http://www.ntg.nl/mailman/listinfo/ntg-context
+webpage  : http://www.pragma-ade.nl / http://tex.aanhet.net
+wiki     : http://contextgarden.net
+]]
+
+logs.levels = {
+    ['error']   = 1,
+    ['warning'] = 2,
+    ['info']    = 3,
+    ['debug']   = 4,
+}
+
+logs.functions = {
+    'report', 'start', 'stop', 'push', 'pop', 'line', 'direct',
+    'start_run', 'stop_run',
+    'start_page_number', 'stop_page_number',
+    'report_output_pages', 'report_output_log',
+    'report_tex_stat', 'report_job_stat',
+    'show_open', 'show_close', 'show_load',
+}
+
+logs.tracers = {
+}
+
+logs.level = 0
+logs.mode  = string.lower((os.getenv("MTX.LOG.MODE") or os.getenv("MTX_LOG_MODE") or "tex"))
+
+function logs.set_level(level)
+    logs.level = logs.levels[level] or level
+end
+
+function logs.set_method(method)
+    for _, v in next, logs.functions do
+        logs[v] = logs[method][v] or function() end
+    end
+end
+
+-- tex logging
+
+function logs.tex.report(category,fmt,...) -- new
+    if fmt then
+        write_nl(category .. " | " .. format(fmt,...))
+    else
+        write_nl(category .. " |")
+    end
+end
+
+function logs.tex.line(fmt,...) -- new
+    if fmt then
+        write_nl(format(fmt,...))
+    else
+        write_nl("")
+    end
+end
+
+--~ function logs.tex.start_page_number()
+--~     local real, user, sub = texcount.realpageno, texcount.userpageno, texcount.subpageno
+--~     if real > 0 then
+--~         if user > 0 then
+--~             if sub > 0 then
+--~                 write(format("[%s.%s.%s",real,user,sub))
+--~             else
+--~                 write(format("[%s.%s",real,user))
+--~             end
+--~         else
+--~             write(format("[%s",real))
+--~         end
+--~     else
+--~         write("[-")
+--~     end
+--~ end
+
+--~ function logs.tex.stop_page_number()
+--~     write("]")
+--~ end
+
+local real, user, sub
+
+function logs.tex.start_page_number()
+    real, user, sub = texcount.realpageno, texcount.userpageno, texcount.subpageno
+end
+
+function logs.tex.stop_page_number()
+    if real > 0 then
+        if user > 0 then
+            if sub > 0 then
+                logs.report("pages", "flushing realpage %s, userpage %s, subpage %s",real,user,sub)
+            else
+                logs.report("pages", "flushing realpage %s, userpage %s",real,user)
+            end
+        else
+            logs.report("pages", "flushing realpage %s",real)
+        end
+    else
+        logs.report("pages", "flushing page")
+    end
+    io.flush()
+end
+
+logs.tex.report_job_stat = statistics.show_job_stat
+
+-- xml logging
+
+function logs.xml.report(category,fmt,...) -- new
+    if fmt then
+        write_nl(format("<r category='%s'>%s</r>",category,format(fmt,...)))
+    else
+        write_nl(format("<r category='%s'/>",category))
+    end
+end
+function logs.xml.line(fmt,...) -- new
+    if fmt then
+        write_nl(format("<r>%s</r>",format(fmt,...)))
+    else
+        write_nl("<r/>")
+    end
+end
+
+function logs.xml.start() if logs.level > 0 then tw("<%s>" ) end end
+function logs.xml.stop () if logs.level > 0 then tw("</%s>") end end
+function logs.xml.push () if logs.level > 0 then tw("<!-- ") end end
+function logs.xml.pop  () if logs.level > 0 then tw(" -->" ) end end
+
+function logs.xml.start_run()
+    write_nl("<?xml version='1.0' standalone='yes'?>")
+    write_nl("<job>") --  xmlns='www.pragma-ade.com/luatex/schemas/context-job.rng'
+    write_nl("")
+end
+
+function logs.xml.stop_run()
+    write_nl("</job>")
+end
+
+function logs.xml.start_page_number()
+    write_nl(format("<p real='%s' page='%s' sub='%s'", texcount.realpageno, texcount.userpageno, texcount.subpageno))
+end
+
+function logs.xml.stop_page_number()
+    write("/>")
+    write_nl("")
+end
+
+function logs.xml.report_output_pages(p,b)
+    write_nl(format("<v k='pages' v='%s'/>", p))
+    write_nl(format("<v k='bytes' v='%s'/>", b))
+    write_nl("")
+end
+
+function logs.xml.report_output_log()
+end
+
+function logs.xml.report_tex_stat(k,v)
+    texiowrite_nl("log","<v k='"..k.."'>"..tostring(v).."</v>")
+end
+
+local level = 0
+
+function logs.xml.show_open(name)
+    level = level + 1
+    texiowrite_nl(format("<f l='%s' n='%s'>",level,name))
+end
+
+function logs.xml.show_close(name)
+    texiowrite("</f> ")
+    level = level - 1
+end
+
+function logs.xml.show_load(name)
+    texiowrite_nl(format("<f l='%s' n='%s'/>",level+1,name))
+end
+
+--
+
+local name, banner = 'report', 'context'
+
+local function report(category,fmt,...)
+    if fmt then
+        write_nl(format("%s | %s: %s",name,category,format(fmt,...)))
+    elseif category then
+        write_nl(format("%s | %s",name,category))
+    else
+        write_nl(format("%s |",name))
+    end
+end
+
+local function simple(fmt,...)
+    if fmt then
+        write_nl(format("%s | %s",name,format(fmt,...)))
+    else
+        write_nl(format("%s |",name))
+    end
+end
+
+function logs.setprogram(_name_,_banner_,_verbose_)
+    name, banner = _name_, _banner_
+    if _verbose_ then
+        trackers.enable("resolvers.locating")
+    end
+    logs.set_method("tex")
+    logs.report = report -- also used in libraries
+    logs.simple = simple -- only used in scripts !
+    if utils then
+        utils.report = simple
+    end
+    logs.verbose = _verbose_
+end
+
+function logs.setverbose(what)
+    if what then
+        trackers.enable("resolvers.locating")
+    else
+        trackers.disable("resolvers.locating")
+    end
+    logs.verbose = what or false
+end
+
+function logs.extendbanner(_banner_,_verbose_)
+    banner = banner .. " | ".. _banner_
+    if _verbose_ ~= nil then
+        logs.setverbose(what)
+    end
+end
+
+logs.verbose = false
+logs.report  = logs.tex.report
+logs.simple  = logs.tex.report
+
+function logs.reportlines(str) -- todo: <lines></lines>
+    for line in gmatch(str,"(.-)[\n\r]") do
+        logs.report(line)
+    end
+end
+
+function logs.reportline() -- for scripts too
+    logs.report()
+end
+
+logs.simpleline = logs.reportline
+
+function logs.reportbanner() -- for scripts too
+    logs.report(banner)
+end
+
+function logs.help(message,option)
+    logs.reportbanner()
+    logs.reportline()
+    logs.reportlines(message)
+    local moreinfo = logs.moreinfo or ""
+    if moreinfo ~= "" and option ~= "nomoreinfo" then
+        logs.reportline()
+        logs.reportlines(moreinfo)
+    end
+end
+
+logs.set_level('error')
+logs.set_method('tex')
+
+function logs.system(whereto,process,jobname,category,...)
+    for i=1,10 do
+        local f = io.open(whereto,"a")
+        if f then
+            f:write(format("%s %s => %s => %s => %s\r",os.date("%d/%m/%y %H:%m:%S"),process,jobname,category,format(...)))
+            f:close()
+            break
+        else
+            sleep(0.1)
+        end
+    end
+end
+
+--~ local syslogname = "oeps.xxx"
+--~
+--~ for i=1,10 do
+--~     logs.system(syslogname,"context","test","fonts","font %s recached due to newer version (%s)","blabla","123")
+--~ end
+
+function logs.fatal(where,...)
+    logs.report(where,"fatal error: %s, aborting now",format(...))
+    os.exit()
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-inp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files",
+}
+
+-- After a few years using the code the large luat-inp.lua file
+-- has been split up a bit. In the process some functionality was
+-- dropped:
+--
+-- * support for reading lsr files
+-- * selective scanning (subtrees)
+-- * some public auxiliary functions were made private
+--
+-- TODO: os.getenv -> os.env[]
+-- TODO: instances.[hashes,cnffiles,configurations,522]
+-- TODO: check escaping in find etc, too much, too slow
+
+-- This lib is multi-purpose and can be loaded again later on so that
+-- additional functionality becomes available. We will split thislogs.report("fileio",
+-- module in components once we're done with prototyping. This is the
+-- first code I wrote for LuaTeX, so it needs some cleanup. Before changing
+-- something in this module one can best check with Taco or Hans first; there
+-- is some nasty trickery going on that relates to traditional kpse support.
+
+-- To be considered: hash key lowercase, first entry in table filename
+-- (any case), rest paths (so no need for optimization). Or maybe a
+-- separate table that matches lowercase names to mixed case when
+-- present. In that case the lower() cases can go away. I will do that
+-- only when we run into problems with names ... well ... Iwona-Regular.
+
+-- Beware, loading and saving is overloaded in luat-tmp!
+
+local format, gsub, find, lower, upper, match, gmatch = string.format, string.gsub, string.find, string.lower, string.upper, string.match, string.gmatch
+local concat, insert, sortedkeys = table.concat, table.insert, table.sortedkeys
+local next, type = next, type
+local lpegmatch = lpeg.match
+
+local trace_locating, trace_detail, trace_expansions = false, false, false
+
+trackers.register("resolvers.locating",   function(v) trace_locating   = v end)
+trackers.register("resolvers.details",    function(v) trace_detail     = v end)
+trackers.register("resolvers.expansions", function(v) trace_expansions = v end) -- todo
+
+if not resolvers then
+    resolvers = {
+        suffixes     = { },
+        formats      = { },
+        dangerous    = { },
+        suffixmap    = { },
+        alternatives = { },
+        locators     = { },  -- locate databases
+        hashers      = { },  -- load databases
+        generators   = { },  -- generate databases
+    }
+end
+
+local resolvers = resolvers
+
+resolvers.locators  .notfound = { nil }
+resolvers.hashers   .notfound = { nil }
+resolvers.generators.notfound = { nil }
+
+resolvers.cacheversion = '1.0.1'
+resolvers.cnfname      = 'texmf.cnf'
+resolvers.luaname      = 'texmfcnf.lua'
+resolvers.homedir      = os.env[os.type == "windows" and 'USERPROFILE'] or os.env['HOME'] or '~'
+resolvers.cnfdefault   = '{$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,.local,}/web2c}'
+
+local dummy_path_expr = "^!*unset/*$"
+
+local formats      = resolvers.formats
+local suffixes     = resolvers.suffixes
+local dangerous    = resolvers.dangerous
+local suffixmap    = resolvers.suffixmap
+local alternatives = resolvers.alternatives
+
+formats['afm'] = 'AFMFONTS'       suffixes['afm'] = { 'afm' }
+formats['enc'] = 'ENCFONTS'       suffixes['enc'] = { 'enc' }
+formats['fmt'] = 'TEXFORMATS'     suffixes['fmt'] = { 'fmt' }
+formats['map'] = 'TEXFONTMAPS'    suffixes['map'] = { 'map' }
+formats['mp']  = 'MPINPUTS'       suffixes['mp']  = { 'mp' }
+formats['ocp'] = 'OCPINPUTS'      suffixes['ocp'] = { 'ocp' }
+formats['ofm'] = 'OFMFONTS'       suffixes['ofm'] = { 'ofm', 'tfm' }
+formats['otf'] = 'OPENTYPEFONTS'  suffixes['otf'] = { 'otf' } -- 'ttf'
+formats['opl'] = 'OPLFONTS'       suffixes['opl'] = { 'opl' }
+formats['otp'] = 'OTPINPUTS'      suffixes['otp'] = { 'otp' }
+formats['ovf'] = 'OVFFONTS'       suffixes['ovf'] = { 'ovf', 'vf' }
+formats['ovp'] = 'OVPFONTS'       suffixes['ovp'] = { 'ovp' }
+formats['tex'] = 'TEXINPUTS'      suffixes['tex'] = { 'tex' }
+formats['tfm'] = 'TFMFONTS'       suffixes['tfm'] = { 'tfm' }
+formats['ttf'] = 'TTFONTS'        suffixes['ttf'] = { 'ttf', 'ttc', 'dfont' }
+formats['pfb'] = 'T1FONTS'        suffixes['pfb'] = { 'pfb', 'pfa' }
+formats['vf']  = 'VFFONTS'        suffixes['vf']  = { 'vf' }
+
+formats['fea'] = 'FONTFEATURES'   suffixes['fea'] = { 'fea' }
+formats['cid'] = 'FONTCIDMAPS'    suffixes['cid'] = { 'cid', 'cidmap' }
+
+formats ['texmfscripts'] = 'TEXMFSCRIPTS' -- new
+suffixes['texmfscripts'] = { 'rb', 'pl', 'py' } -- 'lua'
+
+formats ['lua'] = 'LUAINPUTS' -- new
+suffixes['lua'] = { 'lua', 'luc', 'tma', 'tmc' }
+
+-- backward compatible ones
+
+alternatives['map files']            = 'map'
+alternatives['enc files']            = 'enc'
+alternatives['cid maps']             = 'cid' -- great, why no cid files
+alternatives['font feature files']   = 'fea' -- and fea files here
+alternatives['opentype fonts']       = 'otf'
+alternatives['truetype fonts']       = 'ttf'
+alternatives['truetype collections'] = 'ttc'
+alternatives['truetype dictionary']  = 'dfont'
+alternatives['type1 fonts']          = 'pfb'
+
+-- obscure ones
+
+formats ['misc fonts'] = ''
+suffixes['misc fonts'] = { }
+
+formats     ['sfd']                      = 'SFDFONTS'
+suffixes    ['sfd']                      = { 'sfd' }
+alternatives['subfont definition files'] = 'sfd'
+
+-- lib paths
+
+formats ['lib'] = 'CLUAINPUTS' -- new (needs checking)
+suffixes['lib'] = (os.libsuffix and { os.libsuffix }) or { 'dll', 'so' }
+
+-- In practice we will work within one tds tree, but i want to keep
+-- the option open to build tools that look at multiple trees, which is
+-- why we keep the tree specific data in a table. We used to pass the
+-- instance but for practical pusposes we now avoid this and use a
+-- instance variable.
+
+-- here we catch a few new thingies (todo: add these paths to context.tmf)
+--
+-- FONTFEATURES  = .;$TEXMF/fonts/fea//
+-- FONTCIDMAPS   = .;$TEXMF/fonts/cid//
+
+-- we always have one instance active
+
+resolvers.instance = resolvers.instance or nil -- the current one (slow access)
+local instance = resolvers.instance or nil -- the current one (fast access)
+
+function resolvers.newinstance()
+
+    -- store once, freeze and faster (once reset we can best use
+    -- instance.environment) maybe better have a register suffix
+    -- function
+
+    for k, v in next, suffixes do
+        for i=1,#v do
+            local vi = v[i]
+            if vi then
+                suffixmap[vi] = k
+            end
+        end
+    end
+
+    -- because vf searching is somewhat dangerous, we want to prevent
+    -- too liberal searching esp because we do a lookup on the current
+    -- path anyway; only tex (or any) is safe
+
+    for k, v in next, formats do
+        dangerous[k] = true
+    end
+    dangerous.tex = nil
+
+    -- the instance
+
+    local newinstance = {
+        rootpath        = '',
+        treepath        = '',
+        progname        = 'context',
+        engine          = 'luatex',
+        format          = '',
+        environment     = { },
+        variables       = { },
+        expansions      = { },
+        files           = { },
+        remap           = { },
+        configuration   = { },
+        setup           = { },
+        order           = { },
+        found           = { },
+        foundintrees    = { },
+        kpsevars        = { },
+        hashes          = { },
+        cnffiles        = { },
+        luafiles        = { },
+        lists           = { },
+        remember        = true,
+        diskcache       = true,
+        renewcache      = false,
+        scandisk        = true,
+        cachepath       = nil,
+        loaderror       = false,
+        sortdata        = false,
+        savelists       = true,
+        cleanuppaths    = true,
+        allresults      = false,
+        pattern         = nil, -- lists
+        data            = { }, -- only for loading
+        force_suffixes  = true,
+        fakepaths       = { },
+    }
+
+    local ne = newinstance.environment
+
+    for k,v in next, os.env do
+        ne[k] = resolvers.bare_variable(v)
+    end
+
+    return newinstance
+
+end
+
+function resolvers.setinstance(someinstance)
+    instance = someinstance
+    resolvers.instance = someinstance
+    return someinstance
+end
+
+function resolvers.reset()
+    return resolvers.setinstance(resolvers.newinstance())
+end
+
+local function reset_hashes()
+    instance.lists = { }
+    instance.found = { }
+end
+
+local function check_configuration() -- not yet ok, no time for debugging now
+    local ie, iv = instance.environment, instance.variables
+    local function fix(varname,default)
+        local proname = varname .. "." .. instance.progname or "crap"
+        local p, v = ie[proname], ie[varname] or iv[varname]
+        if not ((p and p ~= "") or (v and v ~= "")) then
+            iv[varname] = default -- or environment?
+        end
+    end
+    local name = os.name
+    if name == "windows" then
+        fix("OSFONTDIR", "c:/windows/fonts//")
+    elseif name == "macosx" then
+        fix("OSFONTDIR", "$HOME/Library/Fonts//;/Library/Fonts//;/System/Library/Fonts//")
+    else
+        -- bad luck
+    end
+    fix("LUAINPUTS"   , ".;$TEXINPUTS;$TEXMFSCRIPTS") -- no progname, hm
+    -- this will go away some day
+    fix("FONTFEATURES", ".;$TEXMF/fonts/{data,fea}//;$OPENTYPEFONTS;$TTFONTS;$T1FONTS;$AFMFONTS")
+    fix("FONTCIDMAPS" , ".;$TEXMF/fonts/{data,cid}//;$OPENTYPEFONTS;$TTFONTS;$T1FONTS;$AFMFONTS")
+    --
+    fix("LUATEXLIBS"  , ".;$TEXMF/luatex/lua//")
+end
+
+function resolvers.bare_variable(str) -- assumes str is a string
+    return (gsub(str,"\s*([\"\']?)(.+)%1\s*", "%2"))
+end
+
+function resolvers.settrace(n) -- no longer number but: 'locating' or 'detail'
+    if n then
+        trackers.disable("resolvers.*")
+        trackers.enable("resolvers."..n)
+    end
+end
+
+resolvers.settrace(os.getenv("MTX_INPUT_TRACE"))
+
+function resolvers.osenv(key)
+    local ie = instance.environment
+    local value = ie[key]
+    if value == nil then
+     -- local e = os.getenv(key)
+        local e = os.env[key]
+        if e == nil then
+         -- value = "" -- false
+        else
+            value = resolvers.bare_variable(e)
+        end
+        ie[key] = value
+    end
+    return value or ""
+end
+
+function resolvers.env(key)
+    return instance.environment[key] or resolvers.osenv(key)
+end
+
+--
+
+local function expand_vars(lst) -- simple vars
+    local variables, env = instance.variables, resolvers.env
+    local function resolve(a)
+        return variables[a] or env(a)
+    end
+    for k=1,#lst do
+        lst[k] = gsub(lst[k],"%$([%a%d%_%-]+)",resolve)
+    end
+end
+
+local function expanded_var(var) -- simple vars
+    local function resolve(a)
+        return instance.variables[a] or resolvers.env(a)
+    end
+    return (gsub(var,"%$([%a%d%_%-]+)",resolve))
+end
+
+local function entry(entries,name)
+    if name and (name ~= "") then
+        name = gsub(name,'%$','')
+        local result = entries[name..'.'..instance.progname] or entries[name]
+        if result then
+            return result
+        else
+            result = resolvers.env(name)
+            if result then
+                instance.variables[name] = result
+                resolvers.expand_variables()
+                return instance.expansions[name] or ""
+            end
+        end
+    end
+    return ""
+end
+
+local function is_entry(entries,name)
+    if name and name ~= "" then
+        name = gsub(name,'%$','')
+        return (entries[name..'.'..instance.progname] or entries[name]) ~= nil
+    else
+        return false
+    end
+end
+
+-- {a,b,c,d}
+-- a,b,c/{p,q,r},d
+-- a,b,c/{p,q,r}/d/{x,y,z}//
+-- a,b,c/{p,q/{x,y,z},r},d/{p,q,r}
+-- a,b,c/{p,q/{x,y,z},r},d/{p,q,r}
+-- a{b,c}{d,e}f
+-- {a,b,c,d}
+-- {a,b,c/{p,q,r},d}
+-- {a,b,c/{p,q,r}/d/{x,y,z}//}
+-- {a,b,c/{p,q/{x,y,z}},d/{p,q,r}}
+-- {a,b,c/{p,q/{x,y,z},w}v,d/{p,q,r}}
+-- {$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,.local,}/web2c}
+
+-- this one is better and faster, but it took me a while to realize
+-- that this kind of replacement is cleaner than messy parsing and
+-- fuzzy concatenating we can probably gain a bit with selectively
+-- applying lpeg, but experiments with lpeg parsing this proved not to
+-- work that well; the parsing is ok, but dealing with the resulting
+-- table is a pain because we need to work inside-out recursively
+
+local function do_first(a,b)
+    local t = { }
+    for s in gmatch(b,"[^,]+") do t[#t+1] = a .. s end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_second(a,b)
+    local t = { }
+    for s in gmatch(a,"[^,]+") do t[#t+1] = s .. b end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_both(a,b)
+    local t = { }
+    for sa in gmatch(a,"[^,]+") do
+        for sb in gmatch(b,"[^,]+") do
+            t[#t+1] = sa .. sb
+        end
+    end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_three(a,b,c)
+    return a .. b.. c
+end
+
+local function splitpathexpr(str, t, validate)
+    -- no need for further optimization as it is only called a
+    -- few times, we can use lpeg for the sub
+    if trace_expansions then
+        logs.report("fileio","expanding variable '%s'",str)
+    end
+    t = t or { }
+    str = gsub(str,",}",",@}")
+    str = gsub(str,"{,","{@,")
+ -- str = "@" .. str .. "@"
+    local ok, done
+    while true do
+        done = false
+        while true do
+            str, ok = gsub(str,"([^{},]+){([^{}]+)}",do_first)
+            if ok > 0 then done = true else break end
+        end
+        while true do
+            str, ok = gsub(str,"{([^{}]+)}([^{},]+)",do_second)
+            if ok > 0 then done = true else break end
+        end
+        while true do
+            str, ok = gsub(str,"{([^{}]+)}{([^{}]+)}",do_both)
+            if ok > 0 then done = true else break end
+        end
+        str, ok = gsub(str,"({[^{}]*){([^{}]+)}([^{}]*})",do_three)
+        if ok > 0 then done = true end
+        if not done then break end
+    end
+    str = gsub(str,"[{}]", "")
+    str = gsub(str,"@","")
+    if validate then
+        for s in gmatch(str,"[^,]+") do
+            s = validate(s)
+            if s then t[#t+1] = s end
+        end
+    else
+        for s in gmatch(str,"[^,]+") do
+            t[#t+1] = s
+        end
+    end
+    if trace_expansions then
+        for k=1,#t do
+            logs.report("fileio","% 4i: %s",k,t[k])
+        end
+    end
+    return t
+end
+
+local function expanded_path_from_list(pathlist) -- maybe not a list, just a path
+    -- a previous version fed back into pathlist
+    local newlist, ok = { }, false
+    for k=1,#pathlist do
+        if find(pathlist[k],"[{}]") then
+            ok = true
+            break
+        end
+    end
+    if ok then
+        local function validate(s)
+            s = file.collapse_path(s)
+            return s ~= "" and not find(s,dummy_path_expr) and s
+        end
+        for k=1,#pathlist do
+            splitpathexpr(pathlist[k],newlist,validate)
+        end
+    else
+        for k=1,#pathlist do
+            for p in gmatch(pathlist[k],"([^,]+)") do
+                p = file.collapse_path(p)
+                if p ~= "" then newlist[#newlist+1] = p end
+            end
+        end
+    end
+    return newlist
+end
+
+-- we follow a rather traditional approach:
+--
+-- (1) texmf.cnf given in TEXMFCNF
+-- (2) texmf.cnf searched in default variable
+--
+-- also we now follow the stupid route: if not set then just assume *one*
+-- cnf file under texmf (i.e. distribution)
+
+local args = environment and environment.original_arguments or arg -- this needs a cleanup
+
+resolvers.ownbin = resolvers.ownbin or args[-2] or arg[-2] or args[-1] or arg[-1] or arg[0] or "luatex"
+resolvers.ownbin = gsub(resolvers.ownbin,"\\","/")
+
+function resolvers.getownpath()
+    local ownpath = resolvers.ownpath or os.selfdir
+    if not ownpath or ownpath == "" or ownpath == "unset" then
+        ownpath = args[-1] or arg[-1]
+        ownpath = ownpath and file.dirname(gsub(ownpath,"\\","/"))
+        if not ownpath or ownpath == "" then
+            ownpath = args[-0] or arg[-0]
+            ownpath = ownpath and file.dirname(gsub(ownpath,"\\","/"))
+        end
+        local binary = resolvers.ownbin
+        if not ownpath or ownpath == "" then
+            ownpath = ownpath and file.dirname(binary)
+        end
+        if not ownpath or ownpath == "" then
+            if os.binsuffix ~= "" then
+                binary = file.replacesuffix(binary,os.binsuffix)
+            end
+            for p in gmatch(os.getenv("PATH"),"[^"..io.pathseparator.."]+") do
+                local b = file.join(p,binary)
+                if lfs.isfile(b) then
+                    -- we assume that after changing to the path the currentdir function
+                    -- resolves to the real location and use this side effect here; this
+                    -- trick is needed because on the mac installations use symlinks in the
+                    -- path instead of real locations
+                    local olddir = lfs.currentdir()
+                    if lfs.chdir(p) then
+                        local pp = lfs.currentdir()
+                        if trace_locating and p ~= pp then
+                            logs.report("fileio","following symlink '%s' to '%s'",p,pp)
+                        end
+                        ownpath = pp
+                        lfs.chdir(olddir)
+                    else
+                        if trace_locating then
+                            logs.report("fileio","unable to check path '%s'",p)
+                        end
+                        ownpath =  p
+                    end
+                    break
+                end
+            end
+        end
+        if not ownpath or ownpath == "" then
+            ownpath = "."
+            logs.report("fileio","forcing fallback ownpath .")
+        elseif trace_locating then
+            logs.report("fileio","using ownpath '%s'",ownpath)
+        end
+    end
+    resolvers.ownpath = ownpath
+    function resolvers.getownpath()
+        return resolvers.ownpath
+    end
+    return ownpath
+end
+
+local own_places = { "SELFAUTOLOC", "SELFAUTODIR", "SELFAUTOPARENT", "TEXMFCNF" }
+
+local function identify_own()
+    local ownpath = resolvers.getownpath() or dir.current()
+    local ie = instance.environment
+    if ownpath then
+        if resolvers.env('SELFAUTOLOC')    == "" then os.env['SELFAUTOLOC']    = file.collapse_path(ownpath) end
+        if resolvers.env('SELFAUTODIR')    == "" then os.env['SELFAUTODIR']    = file.collapse_path(ownpath .. "/..") end
+        if resolvers.env('SELFAUTOPARENT') == "" then os.env['SELFAUTOPARENT'] = file.collapse_path(ownpath .. "/../..") end
+    else
+        logs.report("fileio","error: unable to locate ownpath")
+        os.exit()
+    end
+    if resolvers.env('TEXMFCNF') == "" then os.env['TEXMFCNF'] = resolvers.cnfdefault end
+    if resolvers.env('TEXOS')    == "" then os.env['TEXOS']    = resolvers.env('SELFAUTODIR') end
+    if resolvers.env('TEXROOT')  == "" then os.env['TEXROOT']  = resolvers.env('SELFAUTOPARENT') end
+    if trace_locating then
+        for i=1,#own_places do
+            local v = own_places[i]
+            logs.report("fileio","variable '%s' set to '%s'",v,resolvers.env(v) or "unknown")
+        end
+    end
+    identify_own = function() end
+end
+
+function resolvers.identify_cnf()
+    if #instance.cnffiles == 0 then
+        -- fallback
+        identify_own()
+        -- the real search
+        resolvers.expand_variables()
+        local t = resolvers.split_path(resolvers.env('TEXMFCNF'))
+        t = expanded_path_from_list(t)
+        expand_vars(t) -- redundant
+        local function locate(filename,list)
+            for i=1,#t do
+                local ti = t[i]
+                local texmfcnf = file.collapse_path(file.join(ti,filename))
+                if lfs.isfile(texmfcnf) then
+                    list[#list+1] = texmfcnf
+                end
+            end
+        end
+        locate(resolvers.luaname,instance.luafiles)
+        locate(resolvers.cnfname,instance.cnffiles)
+    end
+end
+
+local function load_cnf_file(fname)
+    fname = resolvers.clean_path(fname)
+    local lname = file.replacesuffix(fname,'lua')
+    if lfs.isfile(lname) then
+        local dname = file.dirname(fname) -- fname ?
+        if not instance.configuration[dname] then
+            resolvers.load_data(dname,'configuration',lname and file.basename(lname))
+            instance.order[#instance.order+1] = instance.configuration[dname]
+        end
+    else
+        f = io.open(fname)
+        if f then
+            if trace_locating then
+                logs.report("fileio","loading configuration file %s", fname)
+            end
+            local line, data, n, k, v
+            local dname = file.dirname(fname)
+            if not instance.configuration[dname] then
+                instance.configuration[dname] = { }
+                instance.order[#instance.order+1] = instance.configuration[dname]
+            end
+            local data = instance.configuration[dname]
+            while true do
+                local line, n = f:read(), 0
+                if line then
+                    while true do -- join lines
+                        line, n = gsub(line,"\\%s*$", "")
+                        if n > 0 then
+                            line = line .. f:read()
+                        else
+                            break
+                        end
+                    end
+                    if not find(line,"^[%%#]") then
+                        local l = gsub(line,"%s*%%.*$","")
+                        local k, v = match(l,"%s*(.-)%s*=%s*(.-)%s*$")
+                        if k and v and not data[k] then
+                            v = gsub(v,"[%%#].*",'')
+                            data[k] = gsub(v,"~","$HOME")
+                            instance.kpsevars[k] = true
+                        end
+                    end
+                else
+                    break
+                end
+            end
+            f:close()
+        elseif trace_locating then
+            logs.report("fileio","skipping configuration file '%s'", fname)
+        end
+    end
+end
+
+local function collapse_cnf_data() -- potential optimization: pass start index (setup and configuration are shared)
+    local order = instance.order
+    for i=1,#order do
+        local c = order[i]
+        for k,v in next, c do
+            if not instance.variables[k] then
+                if instance.environment[k] then
+                    instance.variables[k] = instance.environment[k]
+                else
+                    instance.kpsevars[k] = true
+                    instance.variables[k] = resolvers.bare_variable(v)
+                end
+            end
+        end
+    end
+end
+
+function resolvers.load_cnf()
+    local function loadoldconfigdata()
+        local cnffiles = instance.cnffiles
+        for i=1,#cnffiles do
+            load_cnf_file(cnffiles[i])
+        end
+    end
+    -- instance.cnffiles contain complete names now !
+    -- we still use a funny mix of cnf and new but soon
+    -- we will switch to lua exclusively as we only use
+    -- the file to collect the tree roots
+    if #instance.cnffiles == 0 then
+        if trace_locating then
+            logs.report("fileio","no cnf files found (TEXMFCNF may not be set/known)")
+        end
+    else
+        local cnffiles = instance.cnffiles
+        instance.rootpath = cnffiles[1]
+        for k=1,#cnffiles do
+            instance.cnffiles[k] = file.collapse_path(cnffiles[k])
+        end
+        for i=1,3 do
+            instance.rootpath = file.dirname(instance.rootpath)
+        end
+        instance.rootpath = file.collapse_path(instance.rootpath)
+        if instance.diskcache and not instance.renewcache then
+            resolvers.loadoldconfig(instance.cnffiles)
+            if instance.loaderror then
+                loadoldconfigdata()
+                resolvers.saveoldconfig()
+            end
+        else
+            loadoldconfigdata()
+            if instance.renewcache then
+                resolvers.saveoldconfig()
+            end
+        end
+        collapse_cnf_data()
+    end
+    check_configuration()
+end
+
+function resolvers.load_lua()
+    if #instance.luafiles == 0 then
+        -- yet harmless
+    else
+        instance.rootpath = instance.luafiles[1]
+        local luafiles = instance.luafiles
+        for k=1,#luafiles do
+            instance.luafiles[k] = file.collapse_path(luafiles[k])
+        end
+        for i=1,3 do
+            instance.rootpath = file.dirname(instance.rootpath)
+        end
+        instance.rootpath = file.collapse_path(instance.rootpath)
+        resolvers.loadnewconfig()
+        collapse_cnf_data()
+    end
+    check_configuration()
+end
+
+-- database loading
+
+function resolvers.load_hash()
+    resolvers.locatelists()
+    if instance.diskcache and not instance.renewcache then
+        resolvers.loadfiles()
+        if instance.loaderror then
+            resolvers.loadlists()
+            resolvers.savefiles()
+        end
+    else
+        resolvers.loadlists()
+        if instance.renewcache then
+            resolvers.savefiles()
+        end
+    end
+end
+
+function resolvers.append_hash(type,tag,name)
+    if trace_locating then
+        logs.report("fileio","hash '%s' appended",tag)
+    end
+    insert(instance.hashes, { ['type']=type, ['tag']=tag, ['name']=name } )
+end
+
+function resolvers.prepend_hash(type,tag,name)
+    if trace_locating then
+        logs.report("fileio","hash '%s' prepended",tag)
+    end
+    insert(instance.hashes, 1, { ['type']=type, ['tag']=tag, ['name']=name } )
+end
+
+function resolvers.extend_texmf_var(specification) -- crap, we could better prepend the hash
+--  local t = resolvers.expanded_path_list('TEXMF') -- full expansion
+    local t = resolvers.split_path(resolvers.env('TEXMF'))
+    insert(t,1,specification)
+    local newspec = concat(t,";")
+    if instance.environment["TEXMF"] then
+        instance.environment["TEXMF"] = newspec
+    elseif instance.variables["TEXMF"] then
+        instance.variables["TEXMF"] = newspec
+    else
+        -- weird
+    end
+    resolvers.expand_variables()
+    reset_hashes()
+end
+
+-- locators
+
+function resolvers.locatelists()
+    local texmfpaths = resolvers.clean_path_list('TEXMF')
+    for i=1,#texmfpaths do
+        local path = texmfpaths[i]
+        if trace_locating then
+            logs.report("fileio","locating list of '%s'",path)
+        end
+        resolvers.locatedatabase(file.collapse_path(path))
+    end
+end
+
+function resolvers.locatedatabase(specification)
+    return resolvers.methodhandler('locators', specification)
+end
+
+function resolvers.locators.tex(specification)
+    if specification and specification ~= '' and lfs.isdir(specification) then
+        if trace_locating then
+            logs.report("fileio","tex locator '%s' found",specification)
+        end
+        resolvers.append_hash('file',specification,filename)
+    elseif trace_locating then
+        logs.report("fileio","tex locator '%s' not found",specification)
+    end
+end
+
+-- hashers
+
+function resolvers.hashdatabase(tag,name)
+    return resolvers.methodhandler('hashers',tag,name)
+end
+
+function resolvers.loadfiles()
+    instance.loaderror = false
+    instance.files = { }
+    if not instance.renewcache then
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            resolvers.hashdatabase(hash.tag,hash.name)
+            if instance.loaderror then break end
+        end
+    end
+end
+
+function resolvers.hashers.tex(tag,name)
+    resolvers.load_data(tag,'files')
+end
+
+-- generators:
+
+function resolvers.loadlists()
+    local hashes = instance.hashes
+    for i=1,#hashes do
+        resolvers.generatedatabase(hashes[i].tag)
+    end
+end
+
+function resolvers.generatedatabase(specification)
+    return resolvers.methodhandler('generators', specification)
+end
+
+-- starting with . or .. etc or funny char
+
+local weird = lpeg.P(".")^1 + lpeg.anywhere(lpeg.S("~`!#$%^&*()={}[]:;\"\'||<>,?\n\r\t"))
+
+--~ local l_forbidden = lpeg.S("~`!#$%^&*()={}[]:;\"\'||\\/<>,?\n\r\t")
+--~ local l_confusing = lpeg.P(" ")
+--~ local l_character = lpeg.patterns.utf8
+--~ local l_dangerous = lpeg.P(".")
+
+--~ local l_normal = (l_character - l_forbidden - l_confusing - l_dangerous) * (l_character - l_forbidden - l_confusing^2)^0 * lpeg.P(-1)
+--~ ----- l_normal = l_normal * lpeg.Cc(true) + lpeg.Cc(false)
+
+--~ local function test(str)
+--~     print(str,lpeg.match(l_normal,str))
+--~ end
+--~ test("ヒラギノ明朝 Pro W3")
+--~ test("..ヒラギノ明朝 Pro W3")
+--~ test(":ヒラギノ明朝 Pro W3;")
+--~ test("ヒラギノ明朝 /Pro W3;")
+--~ test("ヒラギノ明朝 Pro  W3")
+
+function resolvers.generators.tex(specification)
+    local tag = specification
+    if trace_locating then
+        logs.report("fileio","scanning path '%s'",specification)
+    end
+    instance.files[tag] = { }
+    local files = instance.files[tag]
+    local n, m, r = 0, 0, 0
+    local spec = specification .. '/'
+    local attributes = lfs.attributes
+    local directory = lfs.dir
+    local function action(path)
+        local full
+        if path then
+            full = spec .. path .. '/'
+        else
+            full = spec
+        end
+        for name in directory(full) do
+            if not lpegmatch(weird,name) then
+         -- if lpegmatch(l_normal,name) then
+                local mode = attributes(full..name,'mode')
+                if mode == 'file' then
+                    if path then
+                        n = n + 1
+                        local f = files[name]
+                        if f then
+                            if type(f) == 'string' then
+                                files[name] = { f, path }
+                            else
+                                f[#f+1] = path
+                            end
+                        else -- probably unique anyway
+                            files[name] = path
+                            local lower = lower(name)
+                            if name ~= lower then
+                                files["remap:"..lower] = name
+                                r = r + 1
+                            end
+                        end
+                    end
+                elseif mode == 'directory' then
+                    m = m + 1
+                    if path then
+                        action(path..'/'..name)
+                    else
+                        action(name)
+                    end
+                end
+            end
+        end
+    end
+    action()
+    if trace_locating then
+        logs.report("fileio","%s files found on %s directories with %s uppercase remappings",n,m,r)
+    end
+end
+
+-- savers, todo
+
+function resolvers.savefiles()
+    resolvers.save_data('files')
+end
+
+-- A config (optionally) has the paths split in tables. Internally
+-- we join them and split them after the expansion has taken place. This
+-- is more convenient.
+
+--~ local checkedsplit = string.checkedsplit
+
+local cache = { }
+
+local splitter = lpeg.Ct(lpeg.splitat(lpeg.S(os.type == "windows" and ";" or ":;")))
+
+local function split_kpse_path(str) -- beware, this can be either a path or a {specification}
+    local found = cache[str]
+    if not found then
+        if str == "" then
+            found = { }
+        else
+            str = gsub(str,"\\","/")
+--~             local split = (find(str,";") and checkedsplit(str,";")) or checkedsplit(str,io.pathseparator)
+local split = lpegmatch(splitter,str)
+            found = { }
+            for i=1,#split do
+                local s = split[i]
+                if not find(s,"^{*unset}*") then
+                    found[#found+1] = s
+                end
+            end
+            if trace_expansions then
+                logs.report("fileio","splitting path specification '%s'",str)
+                for k=1,#found do
+                    logs.report("fileio","% 4i: %s",k,found[k])
+                end
+            end
+            cache[str] = found
+        end
+    end
+    return found
+end
+
+resolvers.split_kpse_path = split_kpse_path
+
+function resolvers.splitconfig()
+    for i=1,#instance do
+        local c = instance[i]
+        for k,v in next, c do
+            if type(v) == 'string' then
+                local t = split_kpse_path(v)
+                if #t > 1 then
+                    c[k] = t
+                end
+            end
+        end
+    end
+end
+
+function resolvers.joinconfig()
+    local order = instance.order
+    for i=1,#order do
+        local c = order[i]
+        for k,v in next, c do -- indexed?
+            if type(v) == 'table' then
+                c[k] = file.join_path(v)
+            end
+        end
+    end
+end
+
+function resolvers.split_path(str)
+    if type(str) == 'table' then
+        return str
+    else
+        return split_kpse_path(str)
+    end
+end
+
+function resolvers.join_path(str)
+    if type(str) == 'table' then
+        return file.join_path(str)
+    else
+        return str
+    end
+end
+
+function resolvers.splitexpansions()
+    local ie = instance.expansions
+    for k,v in next, ie do
+        local t, h, p = { }, { }, split_kpse_path(v)
+        for kk=1,#p do
+            local vv = p[kk]
+            if vv ~= "" and not h[vv] then
+                t[#t+1] = vv
+                h[vv] = true
+            end
+        end
+        if #t > 1 then
+            ie[k] = t
+        else
+            ie[k] = t[1]
+        end
+    end
+end
+
+-- end of split/join code
+
+function resolvers.saveoldconfig()
+    resolvers.splitconfig()
+    resolvers.save_data('configuration')
+    resolvers.joinconfig()
+end
+
+resolvers.configbanner = [[
+-- This is a Luatex configuration file created by 'luatools.lua' or
+-- 'luatex.exe' directly. For comment, suggestions and questions you can
+-- contact the ConTeXt Development Team. This configuration file is
+-- not copyrighted. [HH & TH]
+]]
+
+function resolvers.serialize(files)
+    -- This version is somewhat optimized for the kind of
+    -- tables that we deal with, so it's much faster than
+    -- the generic serializer. This makes sense because
+    -- luatools and mtxtools are called frequently. Okay,
+    -- we pay a small price for properly tabbed tables.
+    local t = { }
+    local function dump(k,v,m) -- could be moved inline
+        if type(v) == 'string' then
+            return m .. "['" .. k .. "']='" .. v .. "',"
+        elseif #v == 1 then
+            return m .. "['" .. k .. "']='" .. v[1] .. "',"
+        else
+            return m .. "['" .. k .. "']={'" .. concat(v,"','").. "'},"
+        end
+    end
+    t[#t+1] = "return {"
+    if instance.sortdata then
+	local sortedfiles = sortedkeys(files)
+	for i=1,#sortedfiles do
+	    local k = sortedfiles[i]
+            local fk  = files[k]
+            if type(fk) == 'table' then
+                t[#t+1] = "\t['" .. k .. "']={"
+		local sortedfk = sortedkeys(fk)
+        	for j=1,#sortedfk do
+                    local kk = sortedfk[j]
+                    t[#t+1] = dump(kk,fk[kk],"\t\t")
+                end
+                t[#t+1] = "\t},"
+            else
+                t[#t+1] = dump(k,fk,"\t")
+            end
+        end
+    else
+        for k, v in next, files do
+            if type(v) == 'table' then
+                t[#t+1] = "\t['" .. k .. "']={"
+                for kk,vv in next, v do
+                    t[#t+1] = dump(kk,vv,"\t\t")
+                end
+                t[#t+1] = "\t},"
+            else
+                t[#t+1] = dump(k,v,"\t")
+            end
+        end
+    end
+    t[#t+1] = "}"
+    return concat(t,"\n")
+end
+
+local data_state = { }
+
+function resolvers.data_state()
+    return data_state or { }
+end
+
+function resolvers.save_data(dataname, makename) -- untested without cache overload
+    for cachename, files in next, instance[dataname] do
+        local name = (makename or file.join)(cachename,dataname)
+        local luaname, lucname = name .. ".lua", name .. ".luc"
+        if trace_locating then
+            logs.report("fileio","preparing '%s' for '%s'",dataname,cachename)
+        end
+        for k, v in next, files do
+            if type(v) == "table" and #v == 1 then
+                files[k] = v[1]
+            end
+        end
+        local data = {
+            type    = dataname,
+            root    = cachename,
+            version = resolvers.cacheversion,
+            date    = os.date("%Y-%m-%d"),
+            time    = os.date("%H:%M:%S"),
+            content = files,
+            uuid    = os.uuid(),
+        }
+        local ok = io.savedata(luaname,resolvers.serialize(data))
+        if ok then
+            if trace_locating then
+                logs.report("fileio","'%s' saved in '%s'",dataname,luaname)
+            end
+            if utils.lua.compile(luaname,lucname,false,true) then -- no cleanup but strip
+                if trace_locating then
+                    logs.report("fileio","'%s' compiled to '%s'",dataname,lucname)
+                end
+            else
+                if trace_locating then
+                    logs.report("fileio","compiling failed for '%s', deleting file '%s'",dataname,lucname)
+                end
+                os.remove(lucname)
+            end
+        elseif trace_locating then
+            logs.report("fileio","unable to save '%s' in '%s' (access error)",dataname,luaname)
+        end
+    end
+end
+
+function resolvers.load_data(pathname,dataname,filename,makename) -- untested without cache overload
+    filename = ((not filename or (filename == "")) and dataname) or filename
+    filename = (makename and makename(dataname,filename)) or file.join(pathname,filename)
+    local blob = loadfile(filename .. ".luc") or loadfile(filename .. ".lua")
+    if blob then
+        local data = blob()
+        if data and data.content and data.type == dataname and data.version == resolvers.cacheversion then
+            data_state[#data_state+1] = data.uuid
+            if trace_locating then
+                logs.report("fileio","loading '%s' for '%s' from '%s'",dataname,pathname,filename)
+            end
+            instance[dataname][pathname] = data.content
+        else
+            if trace_locating then
+                logs.report("fileio","skipping '%s' for '%s' from '%s'",dataname,pathname,filename)
+            end
+            instance[dataname][pathname] = { }
+            instance.loaderror = true
+        end
+    elseif trace_locating then
+        logs.report("fileio","skipping '%s' for '%s' from '%s'",dataname,pathname,filename)
+    end
+end
+
+-- some day i'll use the nested approach, but not yet (actually we even drop
+-- engine/progname support since we have only luatex now)
+--
+-- first texmfcnf.lua files are located, next the cached texmf.cnf files
+--
+-- return {
+--     TEXMFBOGUS = 'effe checken of dit werkt',
+-- }
+
+function resolvers.resetconfig()
+    identify_own()
+    instance.configuration, instance.setup, instance.order, instance.loaderror = { }, { }, { }, false
+end
+
+function resolvers.loadnewconfig()
+    local luafiles = instance.luafiles
+    for i=1,#luafiles do
+        local cnf = luafiles[i]
+        local pathname = file.dirname(cnf)
+        local filename = file.join(pathname,resolvers.luaname)
+        local blob = loadfile(filename)
+        if blob then
+            local data = blob()
+            if data then
+                if trace_locating then
+                    logs.report("fileio","loading configuration file '%s'",filename)
+                end
+                if true then
+                    -- flatten to variable.progname
+                    local t = { }
+                    for k, v in next, data do -- v = progname
+                        if type(v) == "string" then
+                            t[k] = v
+                        else
+                            for kk, vv in next, v do -- vv = variable
+                                if type(vv) == "string" then
+                                    t[vv.."."..v] = kk
+                                end
+                            end
+                        end
+                    end
+                    instance['setup'][pathname] = t
+                else
+                    instance['setup'][pathname] = data
+                end
+            else
+                if trace_locating then
+                    logs.report("fileio","skipping configuration file '%s'",filename)
+                end
+                instance['setup'][pathname] = { }
+                instance.loaderror = true
+            end
+        elseif trace_locating then
+            logs.report("fileio","skipping configuration file '%s'",filename)
+        end
+        instance.order[#instance.order+1] = instance.setup[pathname]
+        if instance.loaderror then break end
+    end
+end
+
+function resolvers.loadoldconfig()
+    if not instance.renewcache then
+        local cnffiles = instance.cnffiles
+        for i=1,#cnffiles do
+            local cnf = cnffiles[i]
+            local dname = file.dirname(cnf)
+            resolvers.load_data(dname,'configuration')
+            instance.order[#instance.order+1] = instance.configuration[dname]
+            if instance.loaderror then break end
+        end
+    end
+    resolvers.joinconfig()
+end
+
+function resolvers.expand_variables()
+    local expansions, environment, variables = { }, instance.environment, instance.variables
+    local env = resolvers.env
+    instance.expansions = expansions
+    if instance.engine   ~= "" then environment['engine']   = instance.engine   end
+    if instance.progname ~= "" then environment['progname'] = instance.progname end
+    for k,v in next, environment do
+        local a, b = match(k,"^(%a+)%_(.*)%s*$")
+        if a and b then
+            expansions[a..'.'..b] = v
+        else
+            expansions[k] = v
+        end
+    end
+    for k,v in next, environment do -- move environment to expansions
+        if not expansions[k] then expansions[k] = v end
+    end
+    for k,v in next, variables do -- move variables to expansions
+        if not expansions[k] then expansions[k] = v end
+    end
+    local busy = false
+    local function resolve(a)
+        busy = true
+        return expansions[a] or env(a)
+    end
+    while true do
+        busy = false
+        for k,v in next, expansions do
+            local s, n = gsub(v,"%$([%a%d%_%-]+)",resolve)
+            local s, m = gsub(s,"%$%{([%a%d%_%-]+)%}",resolve)
+            if n > 0 or m > 0 then
+                expansions[k]= s
+            end
+        end
+        if not busy then break end
+    end
+    for k,v in next, expansions do
+        expansions[k] = gsub(v,"\\", '/')
+    end
+end
+
+function resolvers.variable(name)
+    return entry(instance.variables,name)
+end
+
+function resolvers.expansion(name)
+    return entry(instance.expansions,name)
+end
+
+function resolvers.is_variable(name)
+    return is_entry(instance.variables,name)
+end
+
+function resolvers.is_expansion(name)
+    return is_entry(instance.expansions,name)
+end
+
+function resolvers.unexpanded_path_list(str)
+    local pth = resolvers.variable(str)
+    local lst = resolvers.split_path(pth)
+    return expanded_path_from_list(lst)
+end
+
+function resolvers.unexpanded_path(str)
+    return file.join_path(resolvers.unexpanded_path_list(str))
+end
+
+do -- no longer needed
+
+    local done = { }
+
+    function resolvers.reset_extra_path()
+        local ep = instance.extra_paths
+        if not ep then
+            ep, done = { }, { }
+            instance.extra_paths = ep
+        elseif #ep > 0 then
+            instance.lists, done = { }, { }
+        end
+    end
+
+    function resolvers.register_extra_path(paths,subpaths)
+        local ep = instance.extra_paths or { }
+        local n = #ep
+        if paths and paths ~= "" then
+            if subpaths and subpaths ~= "" then
+                for p in gmatch(paths,"[^,]+") do
+                    -- we gmatch each step again, not that fast, but used seldom
+                    for s in gmatch(subpaths,"[^,]+") do
+                        local ps = p .. "/" .. s
+                        if not done[ps] then
+                            ep[#ep+1] = resolvers.clean_path(ps)
+                            done[ps] = true
+                        end
+                    end
+                end
+            else
+                for p in gmatch(paths,"[^,]+") do
+                    if not done[p] then
+                        ep[#ep+1] = resolvers.clean_path(p)
+                        done[p] = true
+                    end
+                end
+            end
+        elseif subpaths and subpaths ~= "" then
+            for i=1,n do
+                -- we gmatch each step again, not that fast, but used seldom
+                for s in gmatch(subpaths,"[^,]+") do
+                    local ps = ep[i] .. "/" .. s
+                    if not done[ps] then
+                        ep[#ep+1] = resolvers.clean_path(ps)
+                        done[ps] = true
+                    end
+                end
+            end
+        end
+        if #ep > 0 then
+            instance.extra_paths = ep -- register paths
+        end
+        if #ep > n then
+            instance.lists = { } -- erase the cache
+        end
+    end
+
+end
+
+local function made_list(instance,list)
+    local ep = instance.extra_paths
+    if not ep or #ep == 0 then
+        return list
+    else
+        local done, new = { }, { }
+        -- honour . .. ../.. but only when at the start
+        for k=1,#list do
+            local v = list[k]
+            if not done[v] then
+                if find(v,"^[%.%/]$") then
+                    done[v] = true
+                    new[#new+1] = v
+                else
+                    break
+                end
+            end
+        end
+        -- first the extra paths
+        for k=1,#ep do
+            local v = ep[k]
+            if not done[v] then
+                done[v] = true
+                new[#new+1] = v
+            end
+        end
+        -- next the formal paths
+        for k=1,#list do
+            local v = list[k]
+            if not done[v] then
+                done[v] = true
+                new[#new+1] = v
+            end
+        end
+        return new
+    end
+end
+
+function resolvers.clean_path_list(str)
+    local t = resolvers.expanded_path_list(str)
+    if t then
+        for i=1,#t do
+            t[i] = file.collapse_path(resolvers.clean_path(t[i]))
+        end
+    end
+    return t
+end
+
+function resolvers.expand_path(str)
+    return file.join_path(resolvers.expanded_path_list(str))
+end
+
+function resolvers.expanded_path_list(str)
+    if not str then
+        return ep or { } -- ep ?
+    elseif instance.savelists then
+        -- engine+progname hash
+        str = gsub(str,"%$","")
+        if not instance.lists[str] then -- cached
+            local lst = made_list(instance,resolvers.split_path(resolvers.expansion(str)))
+            instance.lists[str] = expanded_path_from_list(lst)
+        end
+        return instance.lists[str]
+    else
+        local lst = resolvers.split_path(resolvers.expansion(str))
+        return made_list(instance,expanded_path_from_list(lst))
+    end
+end
+
+function resolvers.expanded_path_list_from_var(str) -- brrr
+    local tmp = resolvers.var_of_format_or_suffix(gsub(str,"%$",""))
+    if tmp ~= "" then
+        return resolvers.expanded_path_list(tmp)
+    else
+        return resolvers.expanded_path_list(str)
+    end
+end
+
+function resolvers.expand_path_from_var(str)
+    return file.join_path(resolvers.expanded_path_list_from_var(str))
+end
+
+function resolvers.format_of_var(str)
+    return formats[str] or formats[alternatives[str]] or ''
+end
+function resolvers.format_of_suffix(str)
+    return suffixmap[file.extname(str)] or 'tex'
+end
+
+function resolvers.variable_of_format(str)
+    return formats[str] or formats[alternatives[str]] or ''
+end
+
+function resolvers.var_of_format_or_suffix(str)
+    local v = formats[str]
+    if v then
+        return v
+    end
+    v = formats[alternatives[str]]
+    if v then
+        return v
+    end
+    v = suffixmap[file.extname(str)]
+    if v then
+        return formats[isf]
+    end
+    return ''
+end
+
+function resolvers.expand_braces(str) -- output variable and brace expansion of STRING
+    local ori = resolvers.variable(str)
+    local pth = expanded_path_from_list(resolvers.split_path(ori))
+    return file.join_path(pth)
+end
+
+resolvers.isreadable = { }
+
+function resolvers.isreadable.file(name)
+    local readable = lfs.isfile(name) -- brrr
+    if trace_detail then
+        if readable then
+            logs.report("fileio","file '%s' is readable",name)
+        else
+            logs.report("fileio","file '%s' is not readable", name)
+        end
+    end
+    return readable
+end
+
+resolvers.isreadable.tex = resolvers.isreadable.file
+
+-- name
+-- name/name
+
+local function collect_files(names)
+    local filelist = { }
+    for k=1,#names do
+        local fname = names[k]
+        if trace_detail then
+            logs.report("fileio","checking name '%s'",fname)
+        end
+        local bname = file.basename(fname)
+        local dname = file.dirname(fname)
+        if dname == "" or find(dname,"^%.") then
+            dname = false
+        else
+            dname = "/" .. dname .. "$"
+        end
+        local hashes = instance.hashes
+        for h=1,#hashes do
+            local hash = hashes[h]
+            local blobpath = hash.tag
+            local files = blobpath and instance.files[blobpath]
+            if files then
+                if trace_detail then
+                    logs.report("fileio","deep checking '%s' (%s)",blobpath,bname)
+                end
+                local blobfile = files[bname]
+                if not blobfile then
+                    local rname = "remap:"..bname
+                    blobfile = files[rname]
+                    if blobfile then
+                        bname = files[rname]
+                        blobfile = files[bname]
+                    end
+                end
+                if blobfile then
+                    if type(blobfile) == 'string' then
+                        if not dname or find(blobfile,dname) then
+                            filelist[#filelist+1] = {
+                                hash.type,
+                                file.join(blobpath,blobfile,bname), -- search
+                                resolvers.concatinators[hash.type](blobpath,blobfile,bname) -- result
+                            }
+                        end
+                    else
+                        for kk=1,#blobfile do
+                            local vv = blobfile[kk]
+                            if not dname or find(vv,dname) then
+                                filelist[#filelist+1] = {
+                                    hash.type,
+                                    file.join(blobpath,vv,bname), -- search
+                                    resolvers.concatinators[hash.type](blobpath,vv,bname) -- result
+                                }
+                            end
+                        end
+                    end
+                end
+            elseif trace_locating then
+                logs.report("fileio","no match in '%s' (%s)",blobpath,bname)
+            end
+        end
+    end
+    if #filelist > 0 then
+        return filelist
+    else
+        return nil
+    end
+end
+
+function resolvers.suffix_of_format(str)
+    if suffixes[str] then
+        return suffixes[str][1]
+    else
+        return ""
+    end
+end
+
+function resolvers.suffixes_of_format(str)
+    if suffixes[str] then
+        return suffixes[str]
+    else
+        return {}
+    end
+end
+
+function resolvers.register_in_trees(name)
+    if not find(name,"^%.") then
+        instance.foundintrees[name] = (instance.foundintrees[name] or 0) + 1 -- maybe only one
+    end
+end
+
+-- split the next one up for readability (bu this module needs a cleanup anyway)
+
+local function can_be_dir(name) -- can become local
+    local fakepaths = instance.fakepaths
+    if not fakepaths[name] then
+        if lfs.isdir(name) then
+            fakepaths[name] = 1 -- directory
+        else
+            fakepaths[name] = 2 -- no directory
+        end
+    end
+    return (fakepaths[name] == 1)
+end
+
+local function collect_instance_files(filename,collected) -- todo : plugin (scanners, checkers etc)
+    local result = collected or { }
+    local stamp  = nil
+    filename = file.collapse_path(filename)
+    -- speed up / beware: format problem
+    if instance.remember then
+        stamp = filename .. "--" .. instance.engine .. "--" .. instance.progname .. "--" .. instance.format
+        if instance.found[stamp] then
+            if trace_locating then
+                logs.report("fileio","remembering file '%s'",filename)
+            end
+            return instance.found[stamp]
+        end
+    end
+    if not dangerous[instance.format or "?"] then
+        if resolvers.isreadable.file(filename) then
+            if trace_detail then
+                logs.report("fileio","file '%s' found directly",filename)
+            end
+            instance.found[stamp] = { filename }
+            return { filename }
+        end
+    end
+    if find(filename,'%*') then
+        if trace_locating then
+            logs.report("fileio","checking wildcard '%s'", filename)
+        end
+        result = resolvers.find_wildcard_files(filename)
+    elseif file.is_qualified_path(filename) then
+        if resolvers.isreadable.file(filename) then
+            if trace_locating then
+                logs.report("fileio","qualified name '%s'", filename)
+            end
+            result = { filename }
+        else
+            local forcedname, ok, suffix = "", false, file.extname(filename)
+            if suffix == "" then -- why
+                if instance.format == "" then
+                    forcedname = filename .. ".tex"
+                    if resolvers.isreadable.file(forcedname) then
+                        if trace_locating then
+                            logs.report("fileio","no suffix, forcing standard filetype 'tex'")
+                        end
+                        result, ok = { forcedname }, true
+                    end
+                else
+                    local suffixes = resolvers.suffixes_of_format(instance.format)
+                    for _, s in next, suffixes do
+                        forcedname = filename .. "." .. s
+                        if resolvers.isreadable.file(forcedname) then
+                            if trace_locating then
+                                logs.report("fileio","no suffix, forcing format filetype '%s'", s)
+                            end
+                            result, ok = { forcedname }, true
+                            break
+                        end
+                    end
+                end
+            end
+            if not ok and suffix ~= "" then
+                -- try to find in tree (no suffix manipulation), here we search for the
+                -- matching last part of the name
+                local basename = file.basename(filename)
+                local pattern = gsub(filename .. "$","([%.%-])","%%%1")
+                local savedformat = instance.format
+                local format = savedformat or ""
+                if format == "" then
+                    instance.format = resolvers.format_of_suffix(suffix)
+                end
+                if not format then
+                    instance.format = "othertextfiles" -- kind of everything, maybe texinput is better
+                end
+                --
+                if basename ~= filename then
+                    local resolved = collect_instance_files(basename)
+                    if #result == 0 then
+                        local lowered = lower(basename)
+                        if filename ~= lowered then
+                            resolved = collect_instance_files(lowered)
+                        end
+                    end
+                    resolvers.format = savedformat
+                    --
+                    for r=1,#resolved do
+                        local rr = resolved[r]
+                        if find(rr,pattern) then
+                            result[#result+1], ok = rr, true
+                        end
+                    end
+                end
+                -- a real wildcard:
+                --
+                -- if not ok then
+                --     local filelist = collect_files({basename})
+                --     for f=1,#filelist do
+                --         local ff = filelist[f][3] or ""
+                --         if find(ff,pattern) then
+                --             result[#result+1], ok = ff, true
+                --         end
+                --     end
+                -- end
+            end
+            if not ok and trace_locating then
+                logs.report("fileio","qualified name '%s'", filename)
+            end
+        end
+    else
+        -- search spec
+        local filetype, extra, done, wantedfiles, ext = '', nil, false, { }, file.extname(filename)
+        if ext == "" then
+            if not instance.force_suffixes then
+                wantedfiles[#wantedfiles+1] = filename
+            end
+        else
+            wantedfiles[#wantedfiles+1] = filename
+        end
+        if instance.format == "" then
+            if ext == "" then
+                local forcedname = filename .. '.tex'
+                wantedfiles[#wantedfiles+1] = forcedname
+                filetype = resolvers.format_of_suffix(forcedname)
+                if trace_locating then
+                    logs.report("fileio","forcing filetype '%s'",filetype)
+                end
+            else
+                filetype = resolvers.format_of_suffix(filename)
+                if trace_locating then
+                    logs.report("fileio","using suffix based filetype '%s'",filetype)
+                end
+            end
+        else
+            if ext == "" then
+                local suffixes = resolvers.suffixes_of_format(instance.format)
+                for _, s in next, suffixes do
+                    wantedfiles[#wantedfiles+1] = filename .. "." .. s
+                end
+            end
+            filetype = instance.format
+            if trace_locating then
+                logs.report("fileio","using given filetype '%s'",filetype)
+            end
+        end
+        local typespec = resolvers.variable_of_format(filetype)
+        local pathlist = resolvers.expanded_path_list(typespec)
+        if not pathlist or #pathlist == 0 then
+            -- no pathlist, access check only / todo == wildcard
+            if trace_detail then
+                logs.report("fileio","checking filename '%s', filetype '%s', wanted files '%s'",filename, filetype or '?',concat(wantedfiles," | "))
+            end
+            for k=1,#wantedfiles do
+                local fname = wantedfiles[k]
+                if fname and resolvers.isreadable.file(fname) then
+                    filename, done = fname, true
+                    result[#result+1] = file.join('.',fname)
+                    break
+                end
+            end
+            -- this is actually 'other text files' or 'any' or 'whatever'
+            local filelist = collect_files(wantedfiles)
+            local fl = filelist and filelist[1]
+            if fl then
+                filename = fl[3]
+                result[#result+1] = filename
+                done = true
+            end
+        else
+            -- list search
+            local filelist = collect_files(wantedfiles)
+            local dirlist = { }
+            if filelist then
+                for i=1,#filelist do
+                    dirlist[i] = file.dirname(filelist[i][2]) .. "/"
+                end
+            end
+            if trace_detail then
+                logs.report("fileio","checking filename '%s'",filename)
+            end
+            -- a bit messy ... esp the doscan setting here
+            local doscan
+            for k=1,#pathlist do
+                local path = pathlist[k]
+                if find(path,"^!!") then doscan  = false else doscan  = true  end
+                local pathname = gsub(path,"^!+", '')
+                done = false
+                -- using file list
+                if filelist then
+                    local expression
+                    -- compare list entries with permitted pattern -- /xx /xx//
+                    if not find(pathname,"/$") then
+                        expression = pathname .. "/"
+                    else
+                        expression = pathname
+                    end
+                    expression = gsub(expression,"([%-%.])","%%%1") -- this also influences
+                    expression = gsub(expression,"//+$", '/.*')     -- later usage of pathname
+                    expression = gsub(expression,"//", '/.-/')      -- not ok for /// but harmless
+                    expression = "^" .. expression .. "$"
+                    if trace_detail then
+                        logs.report("fileio","using pattern '%s' for path '%s'",expression,pathname)
+                    end
+                    for k=1,#filelist do
+                        local fl = filelist[k]
+                        local f = fl[2]
+                        local d = dirlist[k]
+                        if find(d,expression) then
+                            --- todo, test for readable
+                            result[#result+1] = fl[3]
+                            resolvers.register_in_trees(f) -- for tracing used files
+                            done = true
+                            if instance.allresults then
+                                if trace_detail then
+                                    logs.report("fileio","match in hash for file '%s' on path '%s', continue scanning",f,d)
+                                end
+                            else
+                                if trace_detail then
+                                    logs.report("fileio","match in hash for file '%s' on path '%s', quit scanning",f,d)
+                                end
+                                break
+                            end
+                        elseif trace_detail then
+                            logs.report("fileio","no match in hash for file '%s' on path '%s'",f,d)
+                        end
+                    end
+                end
+                if not done and doscan then
+                    -- check if on disk / unchecked / does not work at all / also zips
+                    if resolvers.splitmethod(pathname).scheme == 'file' then -- ?
+                        local pname = gsub(pathname,"%.%*$",'')
+                        if not find(pname,"%*") then
+                            local ppname = gsub(pname,"/+$","")
+                            if can_be_dir(ppname) then
+                                for k=1,#wantedfiles do
+                                    local w = wantedfiles[k]
+                                    local fname = file.join(ppname,w)
+                                    if resolvers.isreadable.file(fname) then
+                                        if trace_detail then
+                                            logs.report("fileio","found '%s' by scanning",fname)
+                                        end
+                                        result[#result+1] = fname
+                                        done = true
+                                        if not instance.allresults then break end
+                                    end
+                                end
+                            else
+                                -- no access needed for non existing path, speedup (esp in large tree with lots of fake)
+                            end
+                        end
+                    end
+                end
+                if not done and doscan then
+                    -- todo: slow path scanning
+                end
+                if done and not instance.allresults then break end
+            end
+        end
+    end
+    for k=1,#result do
+        result[k] = file.collapse_path(result[k])
+    end
+    if instance.remember then
+        instance.found[stamp] = result
+    end
+    return result
+end
+
+if not resolvers.concatinators  then resolvers.concatinators = { } end
+
+resolvers.concatinators.tex  = file.join
+resolvers.concatinators.file = resolvers.concatinators.tex
+
+function resolvers.find_files(filename,filetype,mustexist)
+    if type(mustexist) == boolean then
+        -- all set
+    elseif type(filetype) == 'boolean' then
+        filetype, mustexist = nil, false
+    elseif type(filetype) ~= 'string' then
+        filetype, mustexist = nil, false
+    end
+    instance.format = filetype or ''
+    local result = collect_instance_files(filename)
+    if #result == 0 then
+        local lowered = lower(filename)
+        if filename ~= lowered then
+            return collect_instance_files(lowered)
+        end
+    end
+    instance.format = ''
+    return result
+end
+
+function resolvers.find_file(filename,filetype,mustexist)
+    return (resolvers.find_files(filename,filetype,mustexist)[1] or "")
+end
+
+function resolvers.find_given_files(filename)
+    local bname, result = file.basename(filename), { }
+    local hashes = instance.hashes
+    for k=1,#hashes do
+        local hash = hashes[k]
+        local files = instance.files[hash.tag] or { }
+        local blist = files[bname]
+        if not blist then
+            local rname = "remap:"..bname
+            blist = files[rname]
+            if blist then
+                bname = files[rname]
+                blist = files[bname]
+            end
+        end
+        if blist then
+            if type(blist) == 'string' then
+                result[#result+1] = resolvers.concatinators[hash.type](hash.tag,blist,bname) or ""
+                if not instance.allresults then break end
+            else
+                for kk=1,#blist do
+                    local vv = blist[kk]
+                    result[#result+1] = resolvers.concatinators[hash.type](hash.tag,vv,bname) or ""
+                    if not instance.allresults then break end
+                end
+            end
+        end
+    end
+    return result
+end
+
+function resolvers.find_given_file(filename)
+    return (resolvers.find_given_files(filename)[1] or "")
+end
+
+local function doit(path,blist,bname,tag,kind,result,allresults)
+    local done = false
+    if blist and kind then
+        if type(blist) == 'string' then
+            -- make function and share code
+            if find(lower(blist),path) then
+                result[#result+1] = resolvers.concatinators[kind](tag,blist,bname) or ""
+                done = true
+            end
+        else
+            for kk=1,#blist do
+                local vv = blist[kk]
+                if find(lower(vv),path) then
+                    result[#result+1] = resolvers.concatinators[kind](tag,vv,bname) or ""
+                    done = true
+                    if not allresults then break end
+                end
+            end
+        end
+    end
+    return done
+end
+
+function resolvers.find_wildcard_files(filename) -- todo: remap:
+    local result = { }
+    local bname, dname = file.basename(filename), file.dirname(filename)
+    local path = gsub(dname,"^*/","")
+    path = gsub(path,"*",".*")
+    path = gsub(path,"-","%%-")
+    if dname == "" then
+        path = ".*"
+    end
+    local name = bname
+    name = gsub(name,"*",".*")
+    name = gsub(name,"-","%%-")
+    path = lower(path)
+    name = lower(name)
+    local files, allresults, done = instance.files, instance.allresults, false
+    if find(name,"%*") then
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            local tag, kind = hash.tag, hash.type
+            for kk, hh in next, files[hash.tag] do
+                if not find(kk,"^remap:") then
+                    if find(lower(kk),name) then
+                        if doit(path,hh,kk,tag,kind,result,allresults) then done = true end
+                        if done and not allresults then break end
+                    end
+                end
+            end
+        end
+    else
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            local tag, kind = hash.tag, hash.type
+            if doit(path,files[tag][bname],bname,tag,kind,result,allresults) then done = true end
+            if done and not allresults then break end
+        end
+    end
+    -- we can consider also searching the paths not in the database, but then
+    -- we end up with a messy search (all // in all path specs)
+    return result
+end
+
+function resolvers.find_wildcard_file(filename)
+    return (resolvers.find_wildcard_files(filename)[1] or "")
+end
+
+-- main user functions
+
+function resolvers.automount()
+    -- implemented later
+end
+
+function resolvers.load(option)
+    statistics.starttiming(instance)
+    resolvers.resetconfig()
+    resolvers.identify_cnf()
+    resolvers.load_lua() -- will become the new method
+    resolvers.expand_variables()
+    resolvers.load_cnf() -- will be skipped when we have a lua file
+    resolvers.expand_variables()
+    if option ~= "nofiles" then
+        resolvers.load_hash()
+        resolvers.automount()
+    end
+    statistics.stoptiming(instance)
+end
+
+function resolvers.for_files(command, files, filetype, mustexist)
+    if files and #files > 0 then
+        local function report(str)
+            if trace_locating then
+                logs.report("fileio",str) -- has already verbose
+            else
+                print(str)
+            end
+        end
+        if trace_locating then
+            report('') -- ?
+        end
+        for f=1,#files do
+            local file = files[f]
+            local result = command(file,filetype,mustexist)
+            if type(result) == 'string' then
+                report(result)
+            else
+                for i=1,#result do
+                    report(result[i]) -- could be unpack
+                end
+            end
+        end
+    end
+end
+
+-- strtab
+
+resolvers.var_value  = resolvers.variable   -- output the value of variable $STRING.
+resolvers.expand_var = resolvers.expansion  -- output variable expansion of STRING.
+
+function resolvers.show_path(str)     -- output search path for file type NAME
+    return file.join_path(resolvers.expanded_path_list(resolvers.format_of_var(str)))
+end
+
+-- resolvers.find_file(filename)
+-- resolvers.find_file(filename, filetype, mustexist)
+-- resolvers.find_file(filename, mustexist)
+-- resolvers.find_file(filename, filetype)
+
+function resolvers.register_file(files, name, path)
+    if files[name] then
+        if type(files[name]) == 'string' then
+            files[name] = { files[name], path }
+        else
+            files[name] = path
+        end
+    else
+        files[name] = path
+    end
+end
+
+function resolvers.splitmethod(filename)
+    if not filename then
+        return { } -- safeguard
+    elseif type(filename) == "table" then
+        return filename -- already split
+    elseif not find(filename,"://") then
+        return { scheme="file", path = filename, original=filename } -- quick hack
+    else
+        return url.hashed(filename)
+    end
+end
+
+function table.sequenced(t,sep) -- temp here
+    local s = { }
+    for k, v in next, t do -- indexed?
+        s[#s+1] = k .. "=" .. tostring(v)
+    end
+    return concat(s, sep or " | ")
+end
+
+function resolvers.methodhandler(what, filename, filetype) -- ...
+    filename = file.collapse_path(filename)
+    local specification = (type(filename) == "string" and resolvers.splitmethod(filename)) or filename -- no or { }, let it bomb
+    local scheme = specification.scheme
+    if resolvers[what][scheme] then
+        if trace_locating then
+            logs.report("fileio","handler '%s' -> '%s' -> '%s'",specification.original,what,table.sequenced(specification))
+        end
+        return resolvers[what][scheme](filename,filetype) -- todo: specification
+    else
+        return resolvers[what].tex(filename,filetype) -- todo: specification
+    end
+end
+
+function resolvers.clean_path(str)
+    if str then
+        str = gsub(str,"\\","/")
+        str = gsub(str,"^!+","")
+        str = gsub(str,"^~",resolvers.homedir)
+        return str
+    else
+        return nil
+    end
+end
+
+function resolvers.do_with_path(name,func)
+    local pathlist = resolvers.expanded_path_list(name)
+    for i=1,#pathlist do
+        func("^"..resolvers.clean_path(pathlist[i]))
+    end
+end
+
+function resolvers.do_with_var(name,func)
+    func(expanded_var(name))
+end
+
+function resolvers.with_files(pattern,handle)
+    local hashes = instance.hashes
+    for i=1,#hashes do
+        local hash = hashes[i]
+        local blobpath = hash.tag
+        local blobtype = hash.type
+        if blobpath then
+            local files = instance.files[blobpath]
+            if files then
+                for k,v in next, files do
+                    if find(k,"^remap:") then
+                        k = files[k]
+                        v = files[k] -- chained
+                    end
+                    if find(k,pattern) then
+                        if type(v) == "string" then
+                            handle(blobtype,blobpath,v,k)
+                        else
+                            for _,vv in next, v do -- indexed
+                                handle(blobtype,blobpath,vv,k)
+                            end
+                        end
+                    end
+                end
+            end
+        end
+    end
+end
+
+function resolvers.locate_format(name)
+    local barename, fmtname = gsub(name,"%.%a+$",""), ""
+    if resolvers.usecache then
+        local path = file.join(caches.setpath("formats")) -- maybe platform
+        fmtname = file.join(path,barename..".fmt") or ""
+    end
+    if fmtname == "" then
+        fmtname = resolvers.find_files(barename..".fmt")[1] or ""
+    end
+    fmtname = resolvers.clean_path(fmtname)
+    if fmtname ~= "" then
+        local barename = file.removesuffix(fmtname)
+        local luaname, lucname, luiname = barename .. ".lua", barename .. ".luc", barename .. ".lui"
+        if lfs.isfile(luiname) then
+            return barename, luiname
+        elseif lfs.isfile(lucname) then
+            return barename, lucname
+        elseif lfs.isfile(luaname) then
+            return barename, luaname
+        end
+    end
+    return nil, nil
+end
+
+function resolvers.boolean_variable(str,default)
+    local b = resolvers.expansion(str)
+    if b == "" then
+        return default
+    else
+        b = toboolean(b)
+        return (b == nil and default) or b
+    end
+end
+
+texconfig.kpse_init = false
+
+kpse = { original = kpse } setmetatable(kpse, { __index = function(k,v) return resolvers[v] end } )
+
+-- for a while
+
+input = resolvers
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-tmp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+--[[ldx--
+<p>This module deals with caching data. It sets up the paths and
+implements loaders and savers for tables. Best is to set the
+following variable. When not set, the usual paths will be
+checked. Personally I prefer the (users) temporary path.</p>
+
+</code>
+TEXMFCACHE=$TMP;$TEMP;$TMPDIR;$TEMPDIR;$HOME;$TEXMFVAR;$VARTEXMF;.
+</code>
+
+<p>Currently we do no locking when we write files. This is no real
+problem because most caching involves fonts and the chance of them
+being written at the same time is small. We also need to extend
+luatools with a recache feature.</p>
+--ldx]]--
+
+local format, lower, gsub = string.format, string.lower, string.gsub
+
+local trace_cache = false  trackers.register("resolvers.cache", function(v) trace_cache = v end) -- not used yet
+
+caches = caches or { }
+
+caches.path     = caches.path or nil
+caches.base     = caches.base or "luatex-cache"
+caches.more     = caches.more or "context"
+caches.direct   = false -- true is faster but may need huge amounts of memory
+caches.tree     = false
+caches.paths    = caches.paths or nil
+caches.force    = false
+caches.defaults = { "TEXMFCACHE", "TMPDIR", "TEMPDIR", "TMP", "TEMP", "HOME", "HOMEPATH" }
+
+function caches.temp()
+    local cachepath = nil
+    local function check(list,isenv)
+        if not cachepath then
+            for k=1,#list do
+                local v = list[k]
+                cachepath = (isenv and (os.env[v] or "")) or v or ""
+                if cachepath == "" then
+                    -- next
+                else
+                    cachepath = resolvers.clean_path(cachepath)
+                    if lfs.isdir(cachepath) and file.iswritable(cachepath) then -- lfs.attributes(cachepath,"mode") == "directory"
+                        break
+                    elseif caches.force or io.ask(format("\nShould I create the cache path %s?",cachepath), "no", { "yes", "no" }) == "yes" then
+                        dir.mkdirs(cachepath)
+                        if lfs.isdir(cachepath) and file.iswritable(cachepath) then
+                            break
+                        end
+                    end
+                end
+                cachepath = nil
+            end
+        end
+    end
+    check(resolvers.clean_path_list("TEXMFCACHE") or { })
+    check(caches.defaults,true)
+    if not cachepath then
+        print("\nfatal error: there is no valid (writable) cache path defined\n")
+        os.exit()
+    elseif not lfs.isdir(cachepath) then -- lfs.attributes(cachepath,"mode") ~= "directory"
+        print(format("\nfatal error: cache path %s is not a directory\n",cachepath))
+        os.exit()
+    end
+    cachepath = file.collapse_path(cachepath)
+    function caches.temp()
+        return cachepath
+    end
+    return cachepath
+end
+
+function caches.configpath()
+    return table.concat(resolvers.instance.cnffiles,";")
+end
+
+function caches.hashed(tree)
+    return md5.hex(gsub(lower(tree),"[\\\/]+","/"))
+end
+
+function caches.treehash()
+    local tree = caches.configpath()
+    if not tree or tree == "" then
+        return false
+    else
+        return caches.hashed(tree)
+    end
+end
+
+function caches.setpath(...)
+    if not caches.path then
+        if not caches.path then
+            caches.path = caches.temp()
+        end
+        caches.path = resolvers.clean_path(caches.path) -- to be sure
+        caches.tree = caches.tree or caches.treehash()
+        if caches.tree then
+            caches.path = dir.mkdirs(caches.path,caches.base,caches.more,caches.tree)
+        else
+            caches.path = dir.mkdirs(caches.path,caches.base,caches.more)
+        end
+    end
+    if not caches.path then
+        caches.path = '.'
+    end
+    caches.path = resolvers.clean_path(caches.path)
+    local dirs = { ... }
+    if #dirs > 0 then
+        local pth = dir.mkdirs(caches.path,...)
+        return pth
+    end
+    caches.path = dir.expand_name(caches.path)
+    return caches.path
+end
+
+function caches.definepath(category,subcategory)
+    return function()
+        return caches.setpath(category,subcategory)
+    end
+end
+
+function caches.setluanames(path,name)
+    return path .. "/" .. name .. ".tma", path .. "/" .. name .. ".tmc"
+end
+
+function caches.loaddata(path,name)
+    local tmaname, tmcname = caches.setluanames(path,name)
+    local loader = loadfile(tmcname) or loadfile(tmaname)
+    if loader then
+        loader = loader()
+        collectgarbage("step")
+        return loader
+    else
+        return false
+    end
+end
+
+--~ function caches.loaddata(path,name)
+--~     local tmaname, tmcname = caches.setluanames(path,name)
+--~     return dofile(tmcname) or dofile(tmaname)
+--~ end
+
+function caches.iswritable(filepath,filename)
+    local tmaname, tmcname = caches.setluanames(filepath,filename)
+    return file.iswritable(tmaname)
+end
+
+function caches.savedata(filepath,filename,data,raw)
+    local tmaname, tmcname = caches.setluanames(filepath,filename)
+    local reduce, simplify = true, true
+    if raw then
+        reduce, simplify = false, false
+    end
+    data.cache_uuid = os.uuid()
+    if caches.direct then
+        file.savedata(tmaname, table.serialize(data,'return',false,true,false)) -- no hex
+    else
+        table.tofile(tmaname, data,'return',false,true,false) -- maybe not the last true
+    end
+    local cleanup = resolvers.boolean_variable("PURGECACHE", false)
+    local strip = resolvers.boolean_variable("LUACSTRIP", true)
+    utils.lua.compile(tmaname, tmcname, cleanup, strip)
+end
+
+-- here we use the cache for format loading (texconfig.[formatname|jobname])
+
+--~ if tex and texconfig and texconfig.formatname and texconfig.formatname == "" then
+if tex and texconfig and (not texconfig.formatname or texconfig.formatname == "") and input and resolvers.instance then
+    if not texconfig.luaname then texconfig.luaname = "cont-en.lua" end -- or luc
+    texconfig.formatname = caches.setpath("formats") .. "/" .. gsub(texconfig.luaname,"%.lu.$",".fmt")
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-inp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+resolvers.finders = resolvers.finders or { }
+resolvers.openers = resolvers.openers or { }
+resolvers.loaders = resolvers.loaders or { }
+
+resolvers.finders.notfound  = { nil }
+resolvers.openers.notfound  = { nil }
+resolvers.loaders.notfound  = { false, nil, 0 }
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-out'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+outputs = outputs or { }
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-con'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, lower, gsub = string.format, string.lower, string.gsub
+
+local trace_cache      = false  trackers.register("resolvers.cache",      function(v) trace_cache      = v end)
+local trace_containers = false  trackers.register("resolvers.containers", function(v) trace_containers = v end)
+local trace_storage    = false  trackers.register("resolvers.storage",    function(v) trace_storage    = v end)
+
+--[[ldx--
+<p>Once we found ourselves defining similar cache constructs
+several times, containers were introduced. Containers are used
+to collect tables in memory and reuse them when possible based
+on (unique) hashes (to be provided by the calling function).</p>
+
+<p>Caching to disk is disabled by default. Version numbers are
+stored in the saved table which makes it possible to change the
+table structures without bothering about the disk cache.</p>
+
+<p>Examples of usage can be found in the font related code.</p>
+--ldx]]--
+
+containers = containers or { }
+
+containers.usecache = true
+
+local function report(container,tag,name)
+    if trace_cache or trace_containers then
+        logs.report(format("%s cache",container.subcategory),"%s: %s",tag,name or 'invalid')
+    end
+end
+
+local allocated = { }
+
+-- tracing
+
+function containers.define(category, subcategory, version, enabled)
+    return function()
+        if category and subcategory then
+            local c = allocated[category]
+            if not c then
+                c  = { }
+                allocated[category] = c
+            end
+            local s = c[subcategory]
+            if not s then
+                s = {
+                    category = category,
+                    subcategory = subcategory,
+                    storage = { },
+                    enabled = enabled,
+                    version = version or 1.000,
+                    trace = false,
+                    path = caches and caches.setpath and caches.setpath(category,subcategory),
+                }
+                c[subcategory] = s
+            end
+            return s
+        else
+            return nil
+        end
+    end
+end
+
+function containers.is_usable(container, name)
+    return container.enabled and caches and caches.iswritable(container.path, name)
+end
+
+function containers.is_valid(container, name)
+    if name and name ~= "" then
+        local storage = container.storage[name]
+        return storage and storage.cache_version == container.version
+    else
+        return false
+    end
+end
+
+function containers.read(container,name)
+    if container.enabled and caches and not container.storage[name] and containers.usecache then
+        container.storage[name] = caches.loaddata(container.path,name)
+        if containers.is_valid(container,name) then
+            report(container,"loaded",name)
+        else
+            container.storage[name] = nil
+        end
+    end
+    if container.storage[name] then
+        report(container,"reusing",name)
+    end
+    return container.storage[name]
+end
+
+function containers.write(container, name, data)
+    if data then
+        data.cache_version = container.version
+        if container.enabled and caches then
+            local unique, shared = data.unique, data.shared
+            data.unique, data.shared = nil, nil
+            caches.savedata(container.path, name, data)
+            report(container,"saved",name)
+            data.unique, data.shared = unique, shared
+        end
+        report(container,"stored",name)
+        container.storage[name] = data
+    end
+    return data
+end
+
+function containers.content(container,name)
+    return container.storage[name]
+end
+
+function containers.cleanname(name)
+    return (gsub(lower(name),"[^%w%d]+","-"))
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-use'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, lower, gsub, find = string.format, string.lower, string.gsub, string.find
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+-- since we want to use the cache instead of the tree, we will now
+-- reimplement the saver.
+
+local save_data = resolvers.save_data
+local load_data = resolvers.load_data
+
+resolvers.cachepath = nil  -- public, for tracing
+resolvers.usecache  = true -- public, for tracing
+
+function resolvers.save_data(dataname)
+    save_data(dataname, function(cachename,dataname)
+        resolvers.usecache = not toboolean(resolvers.expansion("CACHEINTDS") or "false",true)
+        if resolvers.usecache then
+            resolvers.cachepath = resolvers.cachepath or caches.definepath("trees")
+            return file.join(resolvers.cachepath(),caches.hashed(cachename))
+        else
+            return file.join(cachename,dataname)
+        end
+    end)
+end
+
+function resolvers.load_data(pathname,dataname,filename)
+    load_data(pathname,dataname,filename,function(dataname,filename)
+        resolvers.usecache = not toboolean(resolvers.expansion("CACHEINTDS") or "false",true)
+        if resolvers.usecache then
+            resolvers.cachepath = resolvers.cachepath or caches.definepath("trees")
+            return file.join(resolvers.cachepath(),caches.hashed(pathname))
+        else
+            if not filename or (filename == "") then
+                filename = dataname
+            end
+            return file.join(pathname,filename)
+        end
+    end)
+end
+
+-- we will make a better format, maybe something xml or just text or lua
+
+resolvers.automounted = resolvers.automounted or { }
+
+function resolvers.automount(usecache)
+    local mountpaths = resolvers.clean_path_list(resolvers.expansion('TEXMFMOUNT'))
+    if (not mountpaths or #mountpaths == 0) and usecache then
+        mountpaths = { caches.setpath("mount") }
+    end
+    if mountpaths and #mountpaths > 0 then
+        statistics.starttiming(resolvers.instance)
+        for k=1,#mountpaths do
+            local root = mountpaths[k]
+            local f = io.open(root.."/url.tmi")
+            if f then
+                for line in f:lines() do
+                    if line then
+                        if find(line,"^[%%#%-]") then -- or %W
+                            -- skip
+                        elseif find(line,"^zip://") then
+                            if trace_locating then
+                                logs.report("fileio","mounting %s",line)
+                            end
+                            table.insert(resolvers.automounted,line)
+                            resolvers.usezipfile(line)
+                        end
+                    end
+                end
+                f:close()
+            end
+        end
+        statistics.stoptiming(resolvers.instance)
+    end
+end
+
+-- status info
+
+statistics.register("used config path", function() return caches.configpath()  end)
+statistics.register("used cache path",  function() return caches.temp() or "?" end)
+
+-- experiment (code will move)
+
+function statistics.save_fmt_status(texname,formatbanner,sourcefile) -- texname == formatname
+    local enginebanner = status.list().banner
+    if formatbanner and enginebanner and sourcefile then
+        local luvname = file.replacesuffix(texname,"luv")
+        local luvdata = {
+            enginebanner = enginebanner,
+            formatbanner = formatbanner,
+            sourcehash   = md5.hex(io.loaddata(resolvers.find_file(sourcefile)) or "unknown"),
+            sourcefile   = sourcefile,
+        }
+        io.savedata(luvname,table.serialize(luvdata,true))
+    end
+end
+
+function statistics.check_fmt_status(texname)
+    local enginebanner = status.list().banner
+    if enginebanner and texname then
+        local luvname = file.replacesuffix(texname,"luv")
+        if lfs.isfile(luvname) then
+            local luv = dofile(luvname)
+            if luv and luv.sourcefile then
+                local sourcehash = md5.hex(io.loaddata(resolvers.find_file(luv.sourcefile)) or "unknown")
+                local luvbanner = luv.enginebanner or "?"
+                if luvbanner ~= enginebanner then
+                    return string.format("engine mismatch (luv:%s <> bin:%s)",luvbanner,enginebanner)
+                end
+                local luvhash = luv.sourcehash or "?"
+                if luvhash ~= sourcehash then
+                    return string.format("source mismatch (luv:%s <> bin:%s)",luvhash,sourcehash)
+                end
+            else
+                return "invalid status file"
+            end
+        else
+            return "missing status file"
+        end
+    end
+    return true
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['luat-kps'] = {
+    version   = 1.001,
+    comment   = "companion to luatools.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+--[[ldx--
+<p>This file is used when we want the input handlers to behave like
+<type>kpsewhich</type>. What to do with the following:</p>
+
+<typing>
+{$SELFAUTOLOC,$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,}/web2c}
+$SELFAUTOLOC    : /usr/tex/bin/platform
+$SELFAUTODIR    : /usr/tex/bin
+$SELFAUTOPARENT : /usr/tex
+</typing>
+
+<p>How about just forgetting about them?</p>
+--ldx]]--
+
+local suffixes = resolvers.suffixes
+local formats  = resolvers.formats
+
+suffixes['gf']                       = { '<resolution>gf' }
+suffixes['pk']                       = { '<resolution>pk' }
+suffixes['base']                     = { 'base' }
+suffixes['bib']                      = { 'bib' }
+suffixes['bst']                      = { 'bst' }
+suffixes['cnf']                      = { 'cnf' }
+suffixes['mem']                      = { 'mem' }
+suffixes['mf']                       = { 'mf' }
+suffixes['mfpool']                   = { 'pool' }
+suffixes['mft']                      = { 'mft' }
+suffixes['mppool']                   = { 'pool' }
+suffixes['graphic/figure']           = { 'eps', 'epsi' }
+suffixes['texpool']                  = { 'pool' }
+suffixes['PostScript header']        = { 'pro' }
+suffixes['ist']                      = { 'ist' }
+suffixes['web']                      = { 'web', 'ch' }
+suffixes['cweb']                     = { 'w', 'web', 'ch' }
+suffixes['cmap files']               = { 'cmap' }
+suffixes['lig files']                = { 'lig' }
+suffixes['bitmap font']              = { }
+suffixes['MetaPost support']         = { }
+suffixes['TeX system documentation'] = { }
+suffixes['TeX system sources']       = { }
+suffixes['dvips config']             = { }
+suffixes['type42 fonts']             = { }
+suffixes['web2c files']              = { }
+suffixes['other text files']         = { }
+suffixes['other binary files']       = { }
+suffixes['opentype fonts']           = { 'otf' }
+
+suffixes['fmt']                      = { 'fmt' }
+suffixes['texmfscripts']             = { 'rb','lua','py','pl' }
+
+suffixes['pdftex config']            = { }
+suffixes['Troff fonts']              = { }
+
+suffixes['ls-R']                     = { }
+
+--[[ldx--
+<p>If you wondered abou tsome of the previous mappings, how about
+the next bunch:</p>
+--ldx]]--
+
+formats['bib']                      = ''
+formats['bst']                      = ''
+formats['mft']                      = ''
+formats['ist']                      = ''
+formats['web']                      = ''
+formats['cweb']                     = ''
+formats['MetaPost support']         = ''
+formats['TeX system documentation'] = ''
+formats['TeX system sources']       = ''
+formats['Troff fonts']              = ''
+formats['dvips config']             = ''
+formats['graphic/figure']           = ''
+formats['ls-R']                     = ''
+formats['other text files']         = ''
+formats['other binary files']       = ''
+
+formats['gf']                       = ''
+formats['pk']                       = ''
+formats['base']                     = 'MFBASES'
+formats['cnf']                      = ''
+formats['mem']                      = 'MPMEMS'
+formats['mf']                       = 'MFINPUTS'
+formats['mfpool']                   = 'MFPOOL'
+formats['mppool']                   = 'MPPOOL'
+formats['texpool']                  = 'TEXPOOL'
+formats['PostScript header']        = 'TEXPSHEADERS'
+formats['cmap files']               = 'CMAPFONTS'
+formats['type42 fonts']             = 'T42FONTS'
+formats['web2c files']              = 'WEB2C'
+formats['pdftex config']            = 'PDFTEXCONFIG'
+formats['texmfscripts']             = 'TEXMFSCRIPTS'
+formats['bitmap font']              = ''
+formats['lig files']                = 'LIGFONTS'
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-aux'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local find = string.find
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+function resolvers.update_script(oldname,newname) -- oldname -> own.name, not per se a suffix
+    local scriptpath = "scripts/context/lua"
+    newname = file.addsuffix(newname,"lua")
+    local oldscript = resolvers.clean_path(oldname)
+    if trace_locating then
+        logs.report("fileio","to be replaced old script %s", oldscript)
+    end
+    local newscripts = resolvers.find_files(newname) or { }
+    if #newscripts == 0 then
+        if trace_locating then
+            logs.report("fileio","unable to locate new script")
+        end
+    else
+        for i=1,#newscripts do
+            local newscript = resolvers.clean_path(newscripts[i])
+            if trace_locating then
+                logs.report("fileio","checking new script %s", newscript)
+            end
+            if oldscript == newscript then
+                if trace_locating then
+                    logs.report("fileio","old and new script are the same")
+                end
+            elseif not find(newscript,scriptpath) then
+                if trace_locating then
+                    logs.report("fileio","new script should come from %s",scriptpath)
+                end
+            elseif not (find(oldscript,file.removesuffix(newname).."$") or find(oldscript,newname.."$")) then
+                if trace_locating then
+                    logs.report("fileio","invalid new script name")
+                end
+            else
+                local newdata = io.loaddata(newscript)
+                if newdata then
+                    if trace_locating then
+                        logs.report("fileio","old script content replaced by new content")
+                    end
+                    io.savedata(oldscript,newdata)
+                    break
+                elseif trace_locating then
+                    logs.report("fileio","unable to load new script")
+                end
+            end
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-lst'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- used in mtxrun
+
+local find, concat, upper, format = string.find, table.concat, string.upper, string.format
+
+resolvers.listers = resolvers.listers or { }
+
+local function tabstr(str)
+    if type(str) == 'table' then
+        return concat(str," | ")
+    else
+        return str
+    end
+end
+
+local function list(list,report)
+    local instance = resolvers.instance
+    local pat = upper(pattern or "","")
+    local report = report or texio.write_nl
+    local sorted = table.sortedkeys(list)
+    for i=1,#sorted do
+        local key = sorted[i]
+        if instance.pattern == "" or find(upper(key),pat) then
+            if instance.kpseonly then
+                if instance.kpsevars[key] then
+                    report(format("%s=%s",key,tabstr(list[key])))
+                end
+            else
+                report(format('%s %s=%s',(instance.kpsevars[key] and 'K') or 'E',key,tabstr(list[key])))
+            end
+        end
+    end
+end
+
+function resolvers.listers.variables () list(resolvers.instance.variables ) end
+function resolvers.listers.expansions() list(resolvers.instance.expansions) end
+
+function resolvers.listers.configurations(report)
+    local report = report or texio.write_nl
+    local instance = resolvers.instance
+    local sorted = table.sortedkeys(instance.kpsevars)
+    for i=1,#sorted do
+        local key = sorted[i]
+        if not instance.pattern or (instance.pattern=="") or find(key,instance.pattern) then
+            report(format("%s\n",key))
+            local order = instance.order
+            for i=1,#order do
+                local str = order[i][key]
+                if str then
+                    report(format("\t%s\t%s",i,str))
+                end
+            end
+            report("")
+        end
+    end
+end
+
+
+end -- of closure
+-- end library merge
+
+-- We initialize some characteristics of this program. We need to
+-- do this before we load the libraries, else own.name will not be
+-- properly set (handy for selfcleaning the file). It's an ugly
+-- looking piece of code.
+
+own = { }
+
+own.libs = { -- todo: check which ones are really needed
+    'l-string.lua',
+    'l-lpeg.lua',
+    'l-table.lua',
+    'l-io.lua',
+    'l-number.lua',
+    'l-set.lua',
+    'l-os.lua',
+    'l-file.lua',
+    'l-md5.lua',
+    'l-url.lua',
+    'l-dir.lua',
+    'l-boolean.lua',
+    'l-unicode.lua',
+    'l-math.lua',
+    'l-utils.lua',
+    'l-aux.lua',
+    'trac-tra.lua',
+    'luat-env.lua',
+    'trac-inf.lua',
+    'trac-log.lua',
+    'data-res.lua',
+    'data-tmp.lua',
+--  'data-pre.lua',
+    'data-inp.lua',
+    'data-out.lua',
+    'data-con.lua',
+    'data-use.lua',
+--  'data-tex.lua',
+--  'data-bin.lua',
+--  'data-zip.lua',
+--  'data-crl.lua',
+--  'data-lua.lua',
+    'data-kps.lua', -- so that we can replace kpsewhich
+    'data-aux.lua', -- updater
+    'data-lst.lua', -- lister
+}
+
+-- We need this hack till luatex is fixed.
+
+if arg and arg[0] == 'luatex' and arg[1] == "--luaonly" then
+    arg[-1]=arg[0] arg[0]=arg[2] for k=3,#arg do arg[k-2]=arg[k] end arg[#arg]=nil arg[#arg]=nil
+end
+
+-- End of hack.
+
+own.name = (environment and environment.ownname) or arg[0] or 'luatools.lua'
+own.path = string.match(own.name,"^(.+)[\\/].-$") or "."
+own.list = { '.' }
+
+if own.path ~= '.' then
+    table.insert(own.list,own.path)
+end
+
+table.insert(own.list,own.path.."/../../../tex/context/base")
+table.insert(own.list,own.path.."/mtx")
+table.insert(own.list,own.path.."/../sources")
+
+function locate_libs()
+    for _, lib in pairs(own.libs) do
+        for _, pth in pairs(own.list) do
+            local filename = string.gsub(pth .. "/" .. lib,"\\","/")
+            local codeblob = loadfile(filename)
+            if codeblob then
+                codeblob()
+                own.list = { pth } -- speed up te search
+                break
+            end
+        end
+    end
+end
+
+if not resolvers then
+    locate_libs()
+end
+
+if not resolvers then
+    print("")
+    print("Luatools is unable to start up due to lack of libraries. You may")
+    print("try to run 'lua luatools.lua --selfmerge' in the path where this")
+    print("script is located (normally under ..../scripts/context/lua) which")
+    print("will make luatools library independent.")
+    os.exit()
+end
+
+logs.setprogram('LuaTools',"TDS Management Tool 1.32",environment.arguments["verbose"] or false)
+
+local instance = resolvers.reset()
+
+resolvers.defaultlibs = { -- not all are needed (this will become: context.lus (lua spec)
+    'l-string.lua',
+    'l-lpeg.lua',
+    'l-table.lua',
+    'l-boolean.lua',
+    'l-number.lua',
+    'l-unicode.lua',
+    'l-os.lua',
+    'l-io.lua',
+    'l-file.lua',
+    'l-md5.lua',
+    'l-url.lua',
+    'l-dir.lua',
+    'l-utils.lua',
+    'l-dimen.lua',
+    'trac-inf.lua',
+    'trac-tra.lua',
+    'trac-log.lua',
+    'luat-env.lua', -- here ?
+    'data-res.lua',
+    'data-inp.lua',
+    'data-out.lua',
+    'data-tmp.lua',
+    'data-con.lua',
+    'data-use.lua',
+--  'data-pre.lua',
+    'data-tex.lua',
+    'data-bin.lua',
+--  'data-zip.lua',
+--  'data-clr.lua',
+    'data-lua.lua',
+    'data-ctx.lua',
+    'luat-fio.lua',
+    'luat-cnf.lua',
+}
+
+instance.engine     =     environment.arguments["engine"]   or 'luatex'
+instance.progname   =     environment.arguments["progname"] or 'context'
+instance.luaname    =     environment.arguments["luafile"]  or "" -- environment.ownname or ""
+instance.lualibs    =     environment.arguments["lualibs"]  or table.concat(resolvers.defaultlibs,",")
+instance.allresults =     environment.arguments["all"]      or false
+instance.pattern    =     environment.arguments["pattern"]  or nil
+instance.sortdata   =     environment.arguments["sort"]     or false
+instance.kpseonly   = not environment.arguments["all"]      or false
+instance.my_format  =     environment.arguments["format"]   or instance.format
+
+if type(instance.pattern) == 'boolean' then
+    logs.simple("invalid pattern specification")
+    instance.pattern = nil
+end
+
+if environment.arguments["trace"] then resolvers.settrace(environment.arguments["trace"]) end
+
+local trackspec = environment.argument("trackers") or environment.argument("track")
+
+if trackspec then
+    trackers.enable(trackspec)
+end
+
+runners  = runners  or { }
+messages = messages or { }
+
+messages.no_ini_file = [[
+There is no lua initialization file found. This file can be forced by the
+"--progname" directive, or specified with "--luaname", or it is derived
+automatically from the formatname (aka jobname). It may be that you have
+to regenerate the file database using "luatools --generate".
+]]
+
+messages.help = [[
+--generate        generate file database
+--variables       show configuration variables
+--expansions      show expanded variables
+--configurations  show configuration order
+--expand-braces   expand complex variable
+--expand-path     expand variable (resolve paths)
+--expand-var      expand variable (resolve references)
+--show-path       show path expansion of ...
+--var-value       report value of variable
+--find-file       report file location
+--find-path       report path of file
+--make or --ini   make luatex format
+--run or --fmt=   run luatex format
+--luafile=str     lua inifile (default is <progname>.lua)
+--lualibs=list    libraries to assemble (optional when --compile)
+--compile         assemble and compile lua inifile
+--verbose         give a bit more info
+--all             show all found files
+--sort            sort cached data
+--engine=str      target engine
+--progname=str    format or backend
+--pattern=str     filter variables
+--trackers=list   enable given trackers
+]]
+
+function runners.make_format(texname)
+    local instance = resolvers.instance
+    if texname and texname ~= "" then
+        if resolvers.usecache then
+            local path = file.join(caches.setpath("formats")) -- maybe platform
+            if path and lfs then
+                lfs.chdir(path)
+            end
+        end
+        local barename = texname:gsub("%.%a+$","")
+        if barename == texname then
+            texname = texname .. ".tex"
+        end
+        local fullname = resolvers.find_files(texname)[1] or ""
+        if fullname == "" then
+            logs.simple("no tex file with name: %s",texname)
+        else
+            local luaname, lucname, luapath, lualibs = "", "", "", { }
+            -- the following is optional, since context.lua can also
+            -- handle this collect and compile business
+            if environment.arguments["compile"] then
+                if luaname == "" then luaname = barename end
+                logs.simple("creating initialization file: %s",luaname)
+                luapath = file.dirname(luaname)
+                if luapath == "" then
+                    luapath = file.dirname(texname)
+                end
+                if luapath == "" then
+                    luapath = file.dirname(resolvers.find_files(texname)[1] or "")
+                end
+                lualibs = string.split(instance.lualibs,",")
+                luaname = file.basename(barename .. ".lua")
+                lucname = file.basename(barename .. ".luc")
+                -- todo: when this fails, we can just copy the merged libraries from
+                -- luatools since they are normally the same, at least for context
+                if lualibs[1] then
+                    local firstlib = file.join(luapath,lualibs[1])
+                    if not lfs.isfile(firstlib) then
+                        local foundname = resolvers.find_files(lualibs[1])[1]
+                        if foundname then
+                            logs.simple("located library path: %s",luapath)
+                            luapath = file.dirname(foundname)
+                        end
+                    end
+                end
+                logs.simple("using library path: %s",luapath)
+                logs.simple("using lua libraries: %s",table.join(lualibs," "))
+                utils.merger.selfcreate(lualibs,luapath,luaname)
+                local strip = resolvers.boolean_variable("LUACSTRIP", true)
+                if utils.lua.compile(luaname,lucname,false,strip) and io.exists(lucname) then
+                    luaname = lucname
+                    logs.simple("using compiled initialization file: %s",lucname)
+                else
+                    logs.simple("using uncompiled initialization file: %s",luaname)
+                end
+            else
+                local what = { instance.luaname, instance.progname, barename }
+                for k=1,#what do
+                    local v = string.gsub(what[k]..".lua","%.lua%.lua$",".lua")
+                    if v and (v ~= "") then
+                        luaname = resolvers.find_files(v)[1] or ""
+                        if luaname ~= "" then
+                            break
+                        end
+                    end
+                end
+            end
+            if environment.arguments["noluc"] then
+                luaname = luaname:gsub("%.luc$",".lua") -- make this an option
+            end
+            if luaname == "" then
+                if logs.verbose then
+                    logs.simplelines(messages.no_ini_file)
+                    logs.simple("texname : %s",texname)
+                    logs.simple("luaname : %s",instance.luaname)
+                    logs.simple("progname: %s",instance.progname)
+                    logs.simple("barename: %s",barename)
+                end
+            else
+                logs.simple("using lua initialization file: %s",luaname)
+                local mp = dir.glob(file.removesuffix(file.basename(luaname)).."-*.mem")
+                if mp and #mp > 0 then
+                    for i=1,#mp do
+                        local name = mp[i]
+                        logs.simple("removing related mplib format %s", file.basename(name))
+                        os.remove(name)
+                    end
+                end
+                local flags = {
+                    "--ini",
+                    "--lua=" .. string.quote(luaname)
+                }
+                local bs = (os.platform == "unix" and "\\\\") or "\\" -- todo: make a function
+                local command = "luatex ".. table.concat(flags," ")  .. " " .. string.quote(fullname) .. " " .. bs .. "dump"
+                logs.simple("running command: %s\n",command)
+                os.spawn(command)
+                -- todo: do a dummy run that generates the related metafun and mfplain formats
+            end
+        end
+    else
+        logs.simple("no tex file given")
+    end
+end
+
+function runners.run_format(name,data,more)
+ -- hm, rather old code here; we can now use the file.whatever functions
+    if name and (name ~= "") then
+        local barename = name:gsub("%.%a+$","")
+        local fmtname = ""
+        if resolvers.usecache then
+            local path = file.join(caches.setpath("formats")) -- maybe platform
+            fmtname = file.join(path,barename..".fmt") or ""
+        end
+        if fmtname == "" then
+            fmtname = resolvers.find_files(barename..".fmt")[1] or ""
+        end
+        fmtname = resolvers.clean_path(fmtname)
+        barename = fmtname:gsub("%.%a+$","")
+        if fmtname == "" then
+            logs.simple("no format with name: %s",name)
+        else
+            local luaname = barename .. ".luc"
+            local f = io.open(luaname)
+            if not f then
+                luaname = barename .. ".lua"
+                f = io.open(luaname)
+            end
+            if f then
+                f:close()
+                local command = "luatex --fmt=" .. string.quote(barename) .. " --lua=" .. string.quote(luaname) .. " " .. string.quote(data) .. " " .. (more ~= "" and string.quote(more) or "")
+                logs.simple("running command: %s",command)
+                os.spawn(command)
+            else
+                logs.simple("using format name: %s",fmtname)
+                logs.simple("no luc/lua with name: %s",barename)
+            end
+        end
+    end
+end
+
+local ok = true
+
+-- private option --noluc for testing errors in the stub
+
+if environment.arguments["find-file"] then
+    resolvers.load()
+    instance.format  = environment.arguments["format"] or instance.format
+    if instance.pattern then
+        instance.allresults = true
+        resolvers.for_files(resolvers.find_files, { instance.pattern }, instance.my_format)
+    else
+        resolvers.for_files(resolvers.find_files, environment.files, instance.my_format)
+    end
+elseif environment.arguments["find-path"] then
+    resolvers.load()
+    local path = resolvers.find_file(environment.files[1], instance.my_format)
+    if logs.verbose then
+        logs.simple(file.dirname(path))
+    else
+        print(file.dirname(path))
+    end
+elseif environment.arguments["run"] then
+    resolvers.load("nofiles") -- ! no need for loading databases
+    logs.setverbose(true)
+    runners.run_format(environment.files[1] or "",environment.files[2] or "",environment.files[3] or "")
+elseif environment.arguments["fmt"] then
+    resolvers.load("nofiles") -- ! no need for loading databases
+    logs.setverbose(true)
+    runners.run_format(environment.arguments["fmt"], environment.files[1] or "",environment.files[2] or "")
+elseif environment.arguments["expand-braces"] then
+    resolvers.load("nofiles")
+    resolvers.for_files(resolvers.expand_braces, environment.files)
+elseif environment.arguments["expand-path"] then
+    resolvers.load("nofiles")
+    resolvers.for_files(resolvers.expand_path, environment.files)
+elseif environment.arguments["expand-var"] or environment.arguments["expand-variable"] then
+    resolvers.load("nofiles")
+    resolvers.for_files(resolvers.expand_var, environment.files)
+elseif environment.arguments["show-path"] or environment.arguments["path-value"] then
+    resolvers.load("nofiles")
+    resolvers.for_files(resolvers.show_path, environment.files)
+elseif environment.arguments["var-value"] or environment.arguments["show-value"] then
+    resolvers.load("nofiles")
+    resolvers.for_files(resolvers.var_value, environment.files)
+elseif environment.arguments["format-path"] then
+    resolvers.load()
+    logs.simple(caches.setpath("format"))
+elseif instance.pattern then -- brrr
+    resolvers.load()
+    instance.format = environment.arguments["format"] or instance.format
+    instance.allresults = true
+    resolvers.for_files(resolvers.find_files, { instance.pattern }, instance.my_format)
+elseif environment.arguments["generate"] then
+    instance.renewcache = true
+    logs.setverbose(true)
+    resolvers.load()
+elseif environment.arguments["make"] or environment.arguments["ini"] or environment.arguments["compile"] then
+    resolvers.load()
+    logs.setverbose(true)
+    runners.make_format(environment.files[1] or "")
+elseif environment.arguments["selfmerge"] then
+    utils.merger.selfmerge(own.name,own.libs,own.list)
+elseif environment.arguments["selfclean"] then
+    utils.merger.selfclean(own.name)
+elseif environment.arguments["selfupdate"] then
+    resolvers.load()
+    logs.setverbose(true)
+    resolvers.update_script(own.name,"luatools")
+elseif environment.arguments["variables"] or environment.arguments["show-variables"] then
+    resolvers.load("nofiles")
+    resolvers.listers.variables()
+elseif environment.arguments["expansions"] or environment.arguments["show-expansions"] then
+    resolvers.load("nofiles")
+    resolvers.listers.expansions()
+elseif environment.arguments["configurations"] or environment.arguments["show-configurations"] then
+    resolvers.load("nofiles")
+    resolvers.listers.configurations()
+elseif environment.arguments["help"] or (environment.files[1]=='help') or (#environment.files==0) then
+    logs.help(messages.help)
+else
+    resolvers.load()
+    resolvers.for_files(resolvers.find_files, environment.files, instance.my_format)
+end
+
+if logs.verbose then
+    logs.simpleline()
+    logs.simple("runtime: %0.3f seconds",os.runtime())
+end
+
+if os.platform == "unix" then
+    io.write("\n")
+end
diff --git a/scripts/context/lua/luatools.rme b/scripts/context/lua/luatools.rme
new file mode 100644
index 000000000..901e9a9a3
--- /dev/null
+++ b/scripts/context/lua/luatools.rme
@@ -0,0 +1,3 @@
+On MSWindows the luatools.lua script is called
+with luatools.exe. On Unix you can either rename
+luatools.lua to luatools, or use a symlink.
diff --git a/scripts/context/lua/mtx-babel.lua b/scripts/context/lua/mtx-babel.lua
new file mode 100644
index 000000000..01e2ba4b2
--- /dev/null
+++ b/scripts/context/lua/mtx-babel.lua
@@ -0,0 +1,430 @@
+if not modules then modules = { } end modules ['mtx-babel'] = {
+    version   = 1.002,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- data tables by Thomas A. Schmitz
+
+scripts       = scripts       or { }
+scripts.babel = scripts.babel or { }
+
+do
+
+    local converters = { }
+
+    -- greek
+
+    local replace_01 = { -- <' * |
+        a = "ᾅ",
+        h = "ᾕ",
+        w = "ᾥ",
+    }
+
+    local replace_02 = { -- >' * |
+        a = "ᾄ",
+        h = "ᾔ",
+        w = "ᾤ",
+    }
+
+    local replace_03 = { -- <` * |
+        a = "ᾃ",
+        h = "ᾓ",
+        w = "ᾣ",
+    }
+
+    local replace_04 = { -- >` * |
+        a = "ᾂ",
+        h = "ᾒ",
+        w = "ᾢ",
+    }
+
+    local replace_05 = { -- <~ * |
+        a = "ᾇ",
+        h = "ᾗ",
+        w = "ᾧ",
+    }
+
+    local replace_06 = { -- >~ * |
+        a = "ᾆ",
+        h = "ᾖ",
+        w = "ᾦ"
+    }
+
+    local replace_07 = { -- "' *
+        i = "ΐ",
+        u = "ΰ",
+    }
+
+    local replace_08 = { -- "` *
+        i = "ῒ",
+        u = "ῢ",
+    }
+
+    local replace_09 = { -- "~ *
+        i = "ῗ",
+        u = "ῧ",
+    }
+
+    local replace_10 = { -- <' *
+        a = "ἅ",
+        e = "ἕ",
+        h = "ἥ",
+        i = "ἵ",
+        o = "ὅ",
+        u = "ὕ",
+        w = "ὥ",
+        A = "Ἅ",
+        E = "Ἕ",
+        H = "Ἥ",
+        I = "Ἵ",
+        O = "Ὅ",
+        U = "Ὕ",
+        W = "Ὥ",
+    }
+
+    local replace_11 = { -- >' *
+        a = "ἄ",
+        e = "ἔ",
+        h = "ἤ",
+        i = "ἴ",
+        o = "ὄ",
+        u = "ὔ",
+        w = "ὤ",
+        A = "Ἄ",
+        E = "Ἔ",
+        H = "Ἤ",
+        I = "Ἴ",
+        O = "Ὄ",
+        U = "῎Υ",
+        W = "Ὤ",
+    }
+
+    local replace_12 = { -- <` *
+        a = "ἃ",
+        e = "ἓ",
+        h = "ἣ",
+        i = "ἳ",
+        o = "ὃ",
+        u = "ὓ",
+        w = "ὣ",
+        A = "Ἃ",
+        E = "Ἒ",
+        H = "Ἣ",
+        I = "Ἳ",
+        O = "Ὃ",
+        U = "Ὓ",
+        W = "Ὣ",
+    }
+
+    local replace_13 = { -- >` *
+        a = "ἂ",
+        e = "ἒ",
+        h = "ἢ",
+        i = "ἲ",
+        o = "ὂ",
+        u = "ὒ",
+        w = "ὢ",
+        A = "Ἂ",
+        E = "Ἒ",
+        H = "Ἢ",
+        I = "Ἲ",
+        O = "Ὂ",
+        U = "῍Υ",
+        W = "Ὢ",
+    }
+
+    local replace_14 = { -- <~ *
+        a = "ἇ",
+        h = "ἧ",
+        i = "ἷ",
+        u = "ὗ",
+        w = "ὧ",
+        A = "Ἇ",
+        H = "Ἧ",
+        I = "Ἷ",
+        U = "Ὗ",
+        W = "Ὧ",
+    }
+
+    local replace_15 = { -- >~ *
+        a = "ἆ",
+        h = "ἦ",
+        i = "ἶ",
+        u = "ὖ",
+        w = "ὦ",
+        A = "Ἆ",
+        H = "Ἦ",
+        I = "Ἶ",
+        U = "῏Υ",
+        W = "Ὦ",
+    }
+
+    local replace_16 = { -- ' * |
+        a = "ᾴ",
+        h = "ῄ",
+        w = "ῴ",
+    }
+
+    local replace_17 = { -- ` * |
+        a = "ᾲ",
+        h = "ῂ",
+        w = "ῲ",
+    }
+
+    local replace_18 = { -- ~ * |
+        a = "ᾷ",
+        h = "ῇ",
+        w = "ῷ"
+    }
+
+    local replace_19 = { -- ' *
+        a = "ά",
+        e = "έ",
+        h = "ή",
+        i = "ί",
+        o = "ό",
+        u = "ύ",
+        w = "ώ",
+    ["'"] = "’",
+    }
+
+    local replace_20 = { -- ` *
+        a = "ὰ",
+        e = "ὲ",
+        h = "ὴ",
+        i = "ὶ",
+        o = "ὸ",
+        u = "ὺ",
+        w = "ὼ",
+    }
+
+    local replace_21 = { -- ~ *
+        a = "ᾶ",
+        h = "ῆ",
+        i = "ῖ",
+        u = "ῦ",
+        w = "ῶ",
+    }
+
+    local replace_22 = { -- < *
+        a = "ἁ",
+        e = "ἑ",
+        h = "ἡ",
+        i = "ἱ",
+        o = "ὁ",
+        u = "ὑ",
+        w = "ὡ",
+        r = "ῥ",
+        A = "Ἁ",
+        E = "Ἑ",
+        H = "Ἡ",
+        I = "Ἱ",
+        O = "Ὁ",
+        U = "Ὑ",
+        W = "Ὡ",
+        R = "Ῥ",
+    }
+
+    local replace_23 = { -- > *
+        a = "ἀ",
+        e = "ἐ",
+        h = "ἠ",
+        i = "ἰ",
+        o = "ὀ",
+        u = "ὐ",
+        w = "ὠ",
+        A = "Ἀ",
+        E = "Ἐ",
+        H = "Ἠ",
+        I = "Ἰ",
+        O = "Ὀ",
+        U = "᾿Υ",
+        W = "Ὠ",
+    }
+
+    local replace_24 = { -- * |
+        a = "ᾳ",
+        h = "ῃ",
+        w = "ῳ",
+    }
+
+    local replace_25 = { -- " *
+        i = "ϊ",
+        u = "ϋ",
+    }
+
+    local replace_26 = { -- *
+        a = "α",
+        b = "β",
+        g = "γ",
+        d = "δ",
+        e = "ε",
+        z = "ζ",
+        h = "η",
+        j = "θ",
+        i = "ι",
+        k = "κ",
+        l = "λ",
+        m = "μ",
+        n = "ν",
+        x = "ξ",
+        o = "ο",
+        p = "π",
+        r = "ρ",
+        s = "σ",
+        c = "ς",
+        t = "τ",
+        u = "υ",
+        f = "φ",
+        q = "χ",
+        y = "ψ",
+        w = "ω",
+        A = "Α",
+        B = "Β",
+        G = "Γ",
+        D = "Δ",
+        E = "Ε",
+        Z = "Ζ",
+        H = "Η",
+        J = "Θ",
+        I = "Ι",
+        K = "Κ",
+        L = "Λ",
+        M = "Μ",
+        N = "Ν",
+        X = "Ξ",
+        O = "Ο",
+        P = "Π",
+        R = "Ρ",
+        S = "Σ",
+        T = "Τ",
+        U = "Υ",
+        F = "Φ",
+        Q = "Χ",
+        Y = "Ψ",
+        W = "Ω",
+    [";"] = "·",
+    ["?"] = ";",
+    }
+
+    local P, R, S, V, Cs = lpeg.P, lpeg.R, lpeg.S, lpeg.V, lpeg.Cs
+
+    local skips_01 = P("\\")   * R("az", "AZ")^1
+    local skips_02 = P("[")    * (1- S("[]"))^1  * P("]")
+
+    local greek_01 = (P("<'")  * Cs(1) * P('|')) / replace_01
+    local greek_02 = (P(">'")  * Cs(1) * P('|')) / replace_02
+    local greek_03 = (P("<`")  * Cs(1) * P('|')) / replace_03
+    local greek_04 = (P(">`")  * Cs(1) * P('|')) / replace_04
+    local greek_05 = (P("<~")  * Cs(1) * P('|')) / replace_05
+    local greek_06 = (P(">~")  * Cs(1) * P('|')) / replace_06
+    local greek_07 = (P('"\'') * Cs(1)         ) / replace_07
+    local greek_08 = (P('"`')  * Cs(1)         ) / replace_08
+    local greek_09 = (P('"~')  * Cs(1)         ) / replace_09
+    local greek_10 = (P("<'")  * Cs(1)         ) / replace_10
+    local greek_11 = (P(">'")  * Cs(1)         ) / replace_11
+    local greek_12 = (P("<`")  * Cs(1)         ) / replace_12
+    local greek_13 = (P(">`")  * Cs(1)         ) / replace_13
+    local greek_14 = (P("<~")  * Cs(1)         ) / replace_14
+    local greek_15 = (P(">~")  * Cs(1)         ) / replace_15
+    local greek_16 = (P("'")   * Cs(1) * P('|')) / replace_16
+    local greek_17 = (P("`")   * Cs(1) * P('|')) / replace_17
+    local greek_18 = (P("~")   * Cs(1) * P('|')) / replace_18
+    local greek_19 = (P("'")   * Cs(1)         ) / replace_19
+    local greek_20 = (P("`")   * Cs(1)         ) / replace_20
+    local greek_21 = (P("~")   * Cs(1)         ) / replace_21
+    local greek_22 = (P("<")   * Cs(1)         ) / replace_22
+    local greek_23 = (P(">")   * Cs(1)         ) / replace_23
+    local greek_24 = (Cs(1)    * P('|')        ) / replace_24
+    local greek_25 = (P('"')   * Cs(1)         ) / replace_25
+    local greek_26 = (Cs(1)                    ) / replace_26
+
+    local skips =
+        skips_01 + skips_02
+
+    local greek =
+        greek_01 + greek_02 + greek_03 + greek_04 + greek_05 +
+        greek_06 + greek_07 + greek_08 + greek_09 + greek_10 +
+        greek_11 + greek_12 + greek_13 + greek_14 + greek_15 +
+        greek_16 + greek_17 + greek_18 + greek_19 + greek_20 +
+        greek_21 + greek_22 + greek_23 + greek_24 + greek_25 +
+        greek_26
+
+    local spacing      = S(" \n\r\t")
+    local startgreek   = P("\\startgreek")
+    local stopgreek    = P("\\stopgreek")
+    local localgreek   = P("\\localgreek")
+    local lbrace       = P("{")
+    local rbrace       = P("}")
+
+    local documentparser = Cs((skips + greek + 1)^0)
+
+    local contextgrammar = Cs ( P { "scan",
+        ["scan"]     = (V("global") + V("local") + skips + 1)^0,
+        ["global"]   = startgreek * ((skips + greek + 1)-stopgreek )^0 ,
+        ["local"]    = localgreek * V("grouped"),
+        ["grouped"]  = spacing^0 * lbrace * (V("grouped") + skips + (greek - rbrace))^0 * rbrace,
+    } )
+
+    converters['greek'] = {
+        document = documentparser,
+        context  = contextgrammar,
+    }
+
+    -- lpeg.print(parser): 254 lines
+
+    function scripts.babel.convert(filename)
+        if filename and filename ~= empty then
+            local data = io.loaddata(filename) or ""
+            if data ~= "" then
+                local language  = environment.argument("language")  or ""
+                if language ~= "" then
+                    local converter = converters[language]
+                    if converter then
+                        local structure = environment.argument("structure") or "document"
+                        converter = converter[structure]
+                        if converter then
+                            logs.simple("converting '%s' using language '%s' with structure '%s'", filename, language, structure)
+                            data = converter:match(data)
+                            local newfilename = filename .. ".utf"
+                            io.savedata(newfilename, data)
+                            logs.simple("converted data saved in '%s'", newfilename)
+                        else
+                            logs.simple("unknown structure '%s' language '%s'", structure, language)
+                        end
+                    else
+                        logs.simple("no converter for language '%s'", language)
+                    end
+                else
+                    logs.simple("provide language")
+                end
+            else
+                logs.simple("no data in '%s'",filename)
+            end
+        end
+    end
+
+    --~ print(contextgrammar:match [[
+    --~ oeps abg \localgreek{a}
+    --~ \startgreek abg \stopgreek \oeps
+    --~ oeps abg \localgreek{a{b}\oeps g}
+    --~ ]])
+
+end
+
+logs.extendbanner("Babel Input To UTF Conversion 1.20",true)
+
+messages.help = [[
+--language=string     conversion language (e.g. greek)
+--structure=string    obey given structure (e.g. 'document', default: 'context')
+--convert             convert babel codes into utf
+]]
+
+if environment.argument("convert") then
+    scripts.babel.convert(environment.files[1] or "")
+else
+    logs.help(messages.help)
+end
diff --git a/scripts/context/lua/mtx-cache.lua b/scripts/context/lua/mtx-cache.lua
new file mode 100644
index 000000000..c2a0db00d
--- /dev/null
+++ b/scripts/context/lua/mtx-cache.lua
@@ -0,0 +1,96 @@
+if not modules then modules = { } end modules ['mtx-cache'] = {
+    version   = 1.001,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+scripts       = scripts       or { }
+scripts.cache = scripts.cache or { }
+
+function scripts.cache.collect_one(...)
+    local path = caches.setpath(...)
+    local tmas = dir.glob(path .. "/*.tma")
+    local tmcs = dir.glob(path .. "/*.tmc")
+    return path, tmas, tmcs
+end
+
+function scripts.cache.collect_two(...)
+    local path = caches.setpath(...)
+    local rest = dir.glob(path .. "/**/*")
+    return path, rest
+end
+
+local suffixes = { "afm", "tfm", "def", "enc", "otf", "mp", "data" }
+
+function scripts.cache.process_one(action)
+    for i=1,#suffixes do
+        action("fonts", suffixes[i])
+    end
+end
+
+function scripts.cache.process_two(action)
+    action("curl")
+end
+
+-- todo: recursive delete of paths
+
+function scripts.cache.remove(list,keep)
+    local n, keepsuffixes = 0, table.tohash(keep or { })
+    for i=1,#list do
+        local filename = list[i]
+        if string.find(filename,"luatex%-cache") then -- safeguard
+            if not keepsuffixes[file.extname(filename) or ""] then
+                os.remove(filename)
+                n = n + 1
+            end
+        end
+    end
+    return n
+end
+
+function scripts.cache.delete(all,keep)
+    scripts.cache.process_one(function(...)
+        local path, rest = scripts.cache.collect_one(...)
+        local n = scripts.cache.remove(rest,keep)
+        logs.report("cache path",string.format("%4i files out of %4i deleted on %s",n,#rest,path))
+    end)
+    scripts.cache.process_two(function(...)
+        local path, rest = scripts.cache.collect_two(...)
+        local n = scripts.cache.remove(rest,keep)
+        logs.report("cache path",string.format("%4i files out of %4i deleted on %s",n,#rest,path))
+    end)
+end
+
+function scripts.cache.list(all)
+    scripts.cache.process_one(function(...)
+        local path, tmas, tmcs = scripts.cache.collect_one(...)
+        logs.report("cache path",string.format("%4i (tma:%4i, tmc:%4i)  %s",#tmas+#tmcs,#tmas,#tmcs,path))
+        logs.report("cache path",string.format("%4i (tma:%4i, tmc:%4i)  %s",#tmas+#tmcs,#tmas,#tmcs,path))
+    end)
+    scripts.cache.process_two(function(...)
+        local path, rest = scripts.cache.collect_two("curl")
+        logs.report("cache path",string.format("%4i                       %s",#rest,path))
+    end)
+end
+
+logs.extendbanner("ConTeXt & MetaTeX Cache Management 0.10")
+
+messages.help = [[
+--purge               remove not used files
+--erase               completely remove cache
+--list                show cache
+
+--all                 all (not yet implemented)
+]]
+
+if environment.argument("purge") then
+    scripts.cache.delete(environment.argument("all"),{"tmc"})
+elseif environment.argument("erase") then
+    scripts.cache.delete(environment.argument("all"))
+elseif environment.argument("list") then
+    scripts.cache.list(environment.argument("all"))
+else
+    logs.help(messages.help)
+end
diff --git a/scripts/context/lua/mtx-chars.lua b/scripts/context/lua/mtx-chars.lua
new file mode 100644
index 000000000..6acacfbd2
--- /dev/null
+++ b/scripts/context/lua/mtx-chars.lua
@@ -0,0 +1,322 @@
+if not modules then modules = { } end modules ['mtx-chars'] = {
+    version   = 1.001,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, concat, utfchar, upper = string.format, table.concat, unicode.utf8.char, string.upper
+
+scripts       = scripts       or { }
+scripts.chars = scripts.chars or { }
+
+--~ local banner = [[
+--~ -- filename : char-mth.lua
+--~ -- comment  : companion to char-mth.tex (in ConTeXt)
+--~ -- author   : Hans Hagen, PRAGMA-ADE, Hasselt NL
+--~ -- license  : see context related readme files
+--~ -- comment  : generated from data file downloaded from STIX website
+--~
+--~ if not versions   then versions   = { } end versions['char-mth'] = 1.001
+--~ if not characters then characters = { } end
+--~ ]]
+--~
+--~ function scripts.chars.stixtomkiv(inname,outname)
+--~     if inname == "" then
+--~         logs.report("aquiring math data","invalid datafilename")
+--~     end
+--~     local f = io.open(inname)
+--~     if not f then
+--~         logs.report("aquiring math data","invalid datafile")
+--~     else
+--~         logs.report("aquiring math data","processing " .. inname)
+--~         if not outname or outname == "" then
+--~             outname = "char-mth.lua"
+--~         end
+--~         local classes = {
+--~             N = "normal",
+--~             A = "alphabetic",
+--~             D = "diacritic",
+--~             P = "punctuation",
+--~             B = "binary",
+--~             R = "relation",
+--~             L = "large",
+--~             O = "opening",
+--~             C = "closing",
+--~             F = "fence"
+--~         }
+--~         local valid, done = false, { }
+--~         local g = io.open(outname,'w')
+--~         g:write(banner)
+--~         g:write(format("\ncharacters.math = {\n"))
+--~         for l in f:lines() do
+--~             if not valid then
+--~                 valid = l:find("AMS/TeX name")
+--~             end
+--~             if valid then
+--~                 local unicode = l:sub(2,6)
+--~                 if unicode:sub(1,1) ~= " " and unicode ~= "" and not done[unicode] then
+--~                     local mathclass, adobename, texname = l:sub(57,57) or "", l:sub(13,36) or "", l:sub(84,109) or ""
+--~                     texname, adobename = texname:gsub("[\\ ]",""), adobename:gsub("[\\ ]","")
+--~                     local t = { }
+--~                     if mathclass ~= "" then t[#t+1] = format("mathclass='%s'", classes[mathclass] or "unknown") end
+--~                     if adobename ~= "" then t[#t+1] = format("adobename='%s'", adobename                      ) end
+--~                     if texname   ~= "" then t[#t+1] = format("texname='%s'"  , texname                        ) end
+--~                     if #t > 0 then
+--~                         g:write(format("\t[0x%s] = { %s },\n",unicode, concat(t,", ")))
+--~                     end
+--~                     done[unicode] = true
+--~                 end
+--~             end
+--~         end
+--~         if not valid then
+--~             g:write("\t-- The data file is corrupt, invalid or maybe the format has changed.\n")
+--~             logs.report("aquiring math data","problems with data table")
+--~         else
+--~             logs.report("aquiring math data","table saved in " .. outname)
+--~         end
+--~         g:write("}\n")
+--~         g:close()
+--~         f:close()
+--~     end
+--~ end
+
+function scripts.chars.stixtomkiv(inname,outname)
+    logs.report("we no longer use this options but use our own tables instead")
+end
+
+local banner_pdf_1 = [[
+% filename : pdfr-def.tex
+% comment  : generated by mtxrun --script chars --pdf
+% author   : Hans Hagen, PRAGMA-ADE, Hasselt NL
+% copyright: PRAGMA ADE / ConTeXt Development Team
+% license  : see context related readme files
+%
+]]
+
+local banner_pdf_2 = [[
+%
+\endinput
+]]
+
+function scripts.chars.makepdfr()
+    local chartable = resolvers.find_file("char-def.lua") or ""
+    if chartable ~= "" then
+        dofile(chartable)
+        if characters and characters.data then
+            local f = io.open("pdfr-def.tex", 'w')
+            if f then
+                f:write(banner_pdf_1)
+                local cd = characters.data
+                local sd = table.sortedkeys(cd)
+                for i=1,#sd do
+                    local char = cd[sd[i]]
+                    if char.adobename then
+                        f:write(format("\\pdfglyphtounicode{%s}{%04X}%%\n",char.adobename,char.unicodeslot))
+                    end
+                end
+                f:write(banner_pdf_2)
+                f:close()
+            end
+        end
+    end
+end
+
+local banner_utf_module = [[
+%% filename : %s
+%% comment  : generated by mtxrun --script chars --xtx
+%% author   : Hans Hagen, PRAGMA-ADE, Hasselt NL
+%% copyright: PRAGMA ADE / ConTeXt Development Team
+%% license  : see context related readme files
+]]
+
+local banner_utf_mappings = [[
+
+% lc/uc/catcode mappings
+
+]]
+
+local banner_utf_patch = [[
+
+% patch needed for turkish
+
+\setXTXcharcodes "201C "201C "201C
+\setXTXcharcodes "201D "201D "201D
+]]
+
+local banner_utf_names = [[
+
+% named characters mapped onto utf (\\char is needed for accents)
+
+]]
+
+local banner_utf_classes = [[
+
+% some character classes for xetex; seems to be rather hard coded, these numbers
+% and also a mix of several classes; here we do linebreaks
+
+]]
+
+local banner_utf_finish = [[
+
+\endinput
+]]
+
+local xtxclasses = {
+    id =   1,
+    ex =   3,
+    is =   3,
+    cm = 256,
+    op =   2,
+    ns =   3,
+    cl =   3,
+}
+
+function scripts.chars.makeencoutf()
+    local chartable = resolvers.find_file("char-def.lua") or ""
+    if chartable ~= "" then
+        dofile(chartable)
+        local function open(name,banner)
+            local f = io.open(name,'w')
+            if f then
+                logs.simple("writing '%s'",name)
+                f:write(format(banner_utf_module,name))
+                f:write(banner)
+                f:write()
+                return f
+            end
+        end
+        local function close(f)
+            f:write(banner_utf_finish)
+            f:close()
+        end
+        local data = characters and characters.data
+        if data then
+            local list = table.sortedkeys(characters.data)
+            local f = open("xetx-utf.tex",banner_utf_mappings)
+            if f then
+                for i=1,#list do
+                    local code = list[i]
+                    if code <= 0xFFFF then
+                        local chr = data[code]
+                        local cc = chr.category
+                        if cc == 'll' or cc == 'lu' or cc == 'lt' then
+                            if not chr.lccode then chr.lccode = code end
+                            if not chr.uccode then chr.uccode = code end
+                            f:write(format('\\setXTXcharcodes "%05X "%05X "%05X %% %s\n',code,chr.lccode,chr.uccode,chr.description))
+                        end
+                    end
+                end
+                f:write("\n")
+                for i=1,#list do
+                    local code = list[i]
+                    local chr = data[code]
+                    if chr and chr.range then
+                        local cc = chr.category
+                        if cc == 'lo' then
+                            f:write(format('\\dofastrecurse{"%05X}{"%05X}{1}{\\dosetXTXcharcodes\\recurselevel\\recurselevel\\recurselevel}\n',code,chr.range))
+                        end
+                    end
+                end
+                f:write(banner_utf_patch)
+                close(f)
+            end
+            local f = open("xetx-chr.tex",banner_utf_names)
+            if f then
+                local length = 0
+                for i=1,#list do
+                    local code = list[i]
+                    if code > 0x5B and code <= 0xFFFF then
+                        local chr = data[code]
+                        if chr and #(chr.contextname or "") > length then
+                            length = #chr.contextname
+                        end
+                    end
+                end
+                for i=1,#list do
+                    local code = list[i]
+                    if code > 0x5B and code <= 0xFFFF then
+                        local chr = data[code]
+                        if chr and chr.contextname then
+                            local ch = utfchar(code)
+                            f:write(format("\\def\\%s{\\char\"%05X } %% %s: %s\n", chr.contextname:rpadd(length," "), code, chr.description, ch))
+                        end
+                    end
+                end
+                close(f)
+            end
+            local f = open("xetx-cls.tex",banner_utf_classes)
+            if f then
+                for k, v in next, xtxclasses do
+                    f:write(format("\\defineXTXcharinjectionclass[lb:%s]\n",k))
+                end
+                f:write("\n")
+                local i_first, i_last, i_clb = nil, nil, nil
+                local function flush()
+                    if i_first then
+                        if i_first == i_last then
+                            f:write(format('\\dosetXTXcharacterclass{"%05X}{lb:%s}\n',i_first,i_clb))
+                        else
+                            f:write(format('\\dofastrecurse{"%05X}{"%05X}{1}{\\dosetXTXcharacterclass\\fastrecursecounter{lb:%s}}\n',i_first,i_last,i_clb))
+                        end
+                    end
+                    i_first, i_last, i_clb = nil, nil, nil
+                end
+                for i=1,#list do
+                    local code      = list[i]
+                    local code_next = list[i+1]
+                    local chr       = data[code]
+                    local chr_next  = data[code_next]
+                    local clb       = chr and chr.linebreak
+                    local lbc       = xtxclasses[clb]
+                    if not lbc then
+                        flush()
+                    elseif clb == i_clb then
+                        if i_first then
+                            i_last = code
+                        else
+                            i_first, i_last, i_clb = code, code, clb
+                        end
+                    else
+                        flush()
+                        i_first, i_last, i_clb = code, code, clb
+                    end
+                end
+                flush()
+                f:write("\n")
+                for i=1,#list do
+                    local code = list[i]
+                    local chr = data[code]
+                    if chr and chr.range then
+                        local lbc = chr.linebreak
+                        if xtxclasses[lbc] then
+                            f:write(format('\\dofastrecurse{"%05X}{"%05X}{1}{\\dosetXTXcharacterclass\\fastrecursecounter{lb:%s}}\n',code,chr.range,lbc))
+                        end
+                    end
+                end
+                close(f)
+            end
+        end
+    end
+end
+
+logs.extendbanner("MkII Character Table Generators 0.10")
+
+messages.help = [[
+--stix                convert stix table to math table
+--xtx                 generate xetx-*.tex (used by xetex)
+--pdf                 generate pdfr-def.tex (used by pdftex)
+]]
+
+if environment.argument("stix") then
+    local inname  = environment.files[1] or ""
+    local outname = environment.files[2] or ""
+    scripts.chars.stixtomkiv(inname,outname)
+elseif environment.argument("xtx") then
+    scripts.chars.makeencoutf()
+elseif environment.argument("pdf") then
+    scripts.chars.makepdfr()
+else
+    logs.help(messages.help)
+end
diff --git a/scripts/context/lua/mtx-check.lua b/scripts/context/lua/mtx-check.lua
new file mode 100644
index 000000000..4266ddf0d
--- /dev/null
+++ b/scripts/context/lua/mtx-check.lua
@@ -0,0 +1,143 @@
+if not modules then modules = { } end modules ['mtx-check'] = {
+    version   = 1.001,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+scripts         = scripts         or { }
+scripts.checker = scripts.checker or { }
+
+local validator = { }
+
+do
+
+    validator.n      = 1
+    validator.errors = { }
+    validator.trace  = false
+    validator.direct = false
+
+    validator.printer = print
+    validator.tracer  = print
+
+    local message = function(position, kind)
+        local ve = validator.errors
+        ve[#ve+1] = { kind, position, validator.n }
+        if validator.direct then
+            validator.printer(string.format("%s error at position %s (line %s)", kind, position, validator.n))
+        end
+    end
+    local progress = function(position, data, kind)
+        if validator.trace then
+            validator.tracer(string.format("%s at position %s: %s", kind, position, data or ""))
+        end
+    end
+
+    local P, R, S, V, C, CP, CC = lpeg.P, lpeg.R, lpeg.S, lpeg.V, lpeg.C, lpeg.Cp, lpeg.Cc
+
+    local i_m, d_m = P("$"), P("$$")
+    local l_s, r_s = P("["), P("]")
+    local l_g, r_g = P("{"), P("}")
+
+    local okay = lpeg.P("{[}") + lpeg.P("{]}")
+
+    local esc     = P("\\")
+    local cr      = P("\r")
+    local lf      = P("\n")
+    local crlf    = P("\r\n")
+    local space   = S(" \t\f\v")
+    local newline = crlf + cr + lf
+
+    local line = newline / function() validator.n = validator.n + 1 end
+
+    --  local grammar = P { "tokens",
+    --      ["tokens"]   = (V("whatever") + V("grouped") +  V("setup") + V("display") + V("inline") + V("errors") + 1)^0,
+    --      ["whatever"] = line + esc * 1 + C(P("%") * (1-line)^0),
+    --      ["grouped"]  = CP() * C(l_g * (V("whatever") + V("grouped") + V("setup") + V("display") + V("inline") + (1 - l_g - r_g))^0 * r_g) * CC("group") / progress,
+    --      ["setup"]    = CP() * C(l_s * (V("whatever") + V("grouped") + V("setup") + V("display") + V("inline") + (1 - l_s - r_s))^0 * r_s) * CC("setup") / progress,
+    --      ["display"]  = CP() * C(d_m * (V("whatever") + V("grouped") + (1 - d_m))^0 * d_m) * CC("display") / progress,
+    --      ["inline"]   = CP() * C(i_m * (V("whatever") + V("grouped") + (1 - i_m))^0 * i_m) * CC("inline") / progress,
+    --      ["errors"]   = (V("gerror") + V("serror") + V("derror") + V("ierror")) * true,
+    --      ["gerror"]   = CP() * (l_g + r_g) * CC("grouping") / message,
+    --      ["serror"]   = CP() * (l_s + r_g) * CC("setup error") / message,
+    --      ["derror"]   = CP() * d_m * CC("display math error") / message,
+    --      ["ierror"]   = CP() * i_m * CC("inline math error") / message,
+    --  }
+
+    local startluacode = P("\\startluacode")
+    local stopluacode  = P("\\stopluacode")
+
+    local somecode  = startluacode * (1-stopluacode)^1 * stopluacode
+
+    local grammar = P { "tokens",
+        ["tokens"]   = (V("ignore") + V("whatever") + V("grouped") +  V("setup") + V("display") + V("inline") + V("errors") + 1)^0,
+        ["whatever"] = line + esc * 1 + C(P("%") * (1-line)^0),
+        ["grouped"]  = l_g * (V("whatever") + V("grouped") + V("setup") + V("display") + V("inline") + (1 - l_g - r_g))^0 * r_g,
+        ["setup"]    = l_s * (okay + V("whatever") + V("grouped") + V("setup") + V("display") + V("inline") + (1 - l_s - r_s))^0 * r_s,
+        ["display"]  = d_m * (V("whatever") + V("grouped") + (1 - d_m))^0 * d_m,
+        ["inline"]   = i_m * (V("whatever") + V("grouped") + (1 - i_m))^0 * i_m,
+        ["errors"]   = (V("gerror")+ V("serror") + V("derror") + V("ierror")),
+        ["gerror"]   = CP() * (l_g + r_g) * CC("grouping") / message,
+        ["serror"]   = CP() * (l_s + r_g) * CC("setup error") / message,
+        ["derror"]   = CP() * d_m * CC("display math error") / message,
+        ["ierror"]   = CP() * i_m * CC("inline math error") / message,
+        ["ignore"]   = somecode,
+    }
+
+    function validator.check(str)
+        validator.n = 1
+        validator.errors = { }
+        grammar:match(str)
+    end
+
+end
+
+--~ str = [[
+--~ a{oeps {oe\{\}ps} }
+--~ test { oeps \} \[\] oeps \setupxxx[oeps=bla]}
+--~ test $$ \hbox{$ oeps \} \[\] oeps $} $$
+--~ {$x\$xx$ $
+--~ ]]
+--~ str = string.rep(str,10)
+
+function scripts.checker.check(filename)
+    local str = io.loaddata(filename)
+    if str then
+        validator.check(str)
+        local errors = validator.errors
+        if #errors > 0 then
+            for k=1,#errors do
+                local v = errors[k]
+                local kind, position, line = v[1], v[2], v[3]
+                local data = str:sub(position-30,position+30)
+                data = data:gsub("(.)", {
+                    ["\n"] = " <lf> ",
+                    ["\r"] = " <cr> ",
+                    ["\t"] = " <tab> ",
+                })
+                data = data:gsub("^ *","")
+                print(string.format("% 5i  %s  %s", line,string.rpadd(kind,10," "),data))
+            end
+        else
+            print("no error")
+        end
+    else
+        print("no file")
+    end
+end
+
+logs.extendbanner("Basic ConTeXt Syntax Checking 0.10",true)
+
+messages.help = [[
+--convert             check tex file for errors
+]]
+
+if environment.argument("check") then
+    scripts.checker.check(environment.files[1])
+elseif environment.argument("help") then
+    logs.help(messages.help)
+elseif environment.files[1] then
+    scripts.checker.check(environment.files[1])
+end
+
diff --git a/scripts/context/lua/mtx-context.lua b/scripts/context/lua/mtx-context.lua
new file mode 100644
index 000000000..79e74e407
--- /dev/null
+++ b/scripts/context/lua/mtx-context.lua
@@ -0,0 +1,1554 @@
+if not modules then modules = { } end modules ['mtx-context'] = {
+    version   = 1.001,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+scripts         = scripts         or { }
+scripts.context = scripts.context or { }
+
+-- a demo cld file:
+--
+-- context.starttext()
+-- context.chapter("Hello There")
+-- context.readfile("tufte","","not found")
+-- context.stoptext()
+
+-- l-file / todo
+
+function file.needsupdate(oldfile,newfile)
+    return true
+end
+function file.syncmtimes(oldfile,newfile)
+end
+
+-- l-io
+
+function io.copydata(fromfile,tofile)
+    io.savedata(tofile,io.loaddata(fromfile) or "")
+end
+
+-- ctx
+
+ctxrunner = { }
+
+do
+
+    function ctxrunner.filtered(str,method)
+        str = tostring(str)
+        if     method == 'name'     then str = file.removesuffix(file.basename(str))
+        elseif method == 'path'     then str = file.dirname(str)
+        elseif method == 'suffix'   then str = file.extname(str)
+        elseif method == 'nosuffix' then str = file.removesuffix(str)
+        elseif method == 'nopath'   then str = file.basename(str)
+        elseif method == 'base'     then str = file.basename(str)
+    --  elseif method == 'full'     then
+    --  elseif method == 'complete' then
+    --  elseif method == 'expand'   then -- str = file.expand_path(str)
+        end
+        return str:gsub("\\","/")
+    end
+
+    function ctxrunner.substitute(e,str)
+        local attributes = e.at
+        if str and attributes then
+            if attributes['method'] then
+                str = ctxrunner.filtered(str,attributes['method'])
+            end
+            if str == "" and attributes['default'] then
+                str = attributes['default']
+            end
+        end
+        return str
+    end
+
+    function ctxrunner.reflag(flags)
+        local t = { }
+        for _, flag in next, flags do
+            local key, value = flag:match("^(.-)=(.+)$")
+            if key and value then
+                t[key] = value
+            else
+                t[flag] = true
+            end
+        end
+        return t
+    end
+
+    function ctxrunner.substitute(str)
+        return str
+    end
+
+    function ctxrunner.justtext(str)
+        str = xml.unescaped(tostring(str))
+        str = xml.cleansed(str)
+        str = str:gsub("\\+",'/')
+        str = str:gsub("%s+",' ')
+        return str
+    end
+
+    function ctxrunner.new()
+        return {
+            ctxname      = "",
+            jobname      = "",
+            xmldata      = nil,
+            suffix       = "prep",
+            locations    = { '..', '../..' },
+            variables    = { },
+            messages     = { },
+            environments = { },
+            modules      = { },
+            filters      = { },
+            flags        = { },
+            modes        = { },
+            prepfiles    = { },
+            paths        = { },
+        }
+    end
+
+    function ctxrunner.savelog(ctxdata,ctlname)
+        local function yn(b)
+            if b then return 'yes' else return 'no' end
+        end
+        if not ctlname or ctlname == "" or ctlname == ctxdata.jobname then
+            if ctxdata.jobname then
+                ctlname = file.replacesuffix(ctxdata.jobname,'ctl')
+            elseif ctxdata.ctxname then
+                ctlname = file.replacesuffix(ctxdata.ctxname,'ctl')
+            else
+                logs.simple("invalid ctl name: %s",ctlname or "?")
+                return
+            end
+        end
+        local prepfiles = ctxdata.prepfiles
+        if prepfiles and next(prepfiles) then
+            logs.simple("saving logdata in: %s",ctlname)
+            f = io.open(ctlname,'w')
+            if f then
+                f:write("<?xml version='1.0' standalone='yes'?>\n\n")
+                f:write(string.format("<ctx:preplist local='%s'>\n",yn(ctxdata.runlocal)))
+                local sorted = table.sortedkeys(prepfiles)
+                for i=1,#sorted do
+                    local name = sorted[i]
+                    f:write(string.format("\t<ctx:prepfile done='%s'>%s</ctx:prepfile>\n",yn(prepfiles[name]),name))
+                end
+                f:write("</ctx:preplist>\n")
+                f:close()
+            end
+        else
+            logs.simple("nothing prepared, no ctl file saved")
+            os.remove(ctlname)
+        end
+    end
+
+    function ctxrunner.register_path(ctxdata,path)
+        -- test if exists
+        ctxdata.paths[ctxdata.paths+1] = path
+    end
+
+    function ctxrunner.trace(ctxdata)
+        print(table.serialize(ctxdata.messages))
+        print(table.serialize(ctxdata.flags))
+        print(table.serialize(ctxdata.environments))
+        print(table.serialize(ctxdata.modules))
+        print(table.serialize(ctxdata.filters))
+        print(table.serialize(ctxdata.modes))
+        print(xml.tostring(ctxdata.xmldata))
+    end
+
+    function ctxrunner.manipulate(ctxdata,ctxname,defaultname)
+
+        if not ctxdata.jobname or ctxdata.jobname == "" then
+            return
+        end
+
+        ctxdata.ctxname = ctxname or file.removesuffix(ctxdata.jobname) or ""
+
+        if ctxdata.ctxname == "" then
+            return
+        end
+
+        ctxdata.jobname = file.addsuffix(ctxdata.jobname,'tex')
+        ctxdata.ctxname = file.addsuffix(ctxdata.ctxname,'ctx')
+
+        logs.simple("jobname: %s",ctxdata.jobname)
+        logs.simple("ctxname: %s",ctxdata.ctxname)
+
+        -- mtxrun should resolve kpse: and file:
+
+        local usedname = ctxdata.ctxname
+        local found    = lfs.isfile(usedname)
+
+        if not found then
+            for _, path in next, ctxdata.locations do
+                local fullname = file.join(path,ctxdata.ctxname)
+                if lfs.isfile(fullname) then
+                    usedname, found = fullname, true
+                    break
+                end
+            end
+        end
+
+        usedname = resolvers.find_file(ctxdata.ctxname,"tex")
+        found = usedname ~= ""
+
+        if not found and defaultname and defaultname ~= "" and lfs.isfile(defaultname) then
+            usedname, found = defaultname, true
+        end
+
+        if not found then
+            return
+        end
+
+        ctxdata.xmldata = xml.load(usedname)
+
+        if not ctxdata.xmldata then
+            return
+        else
+            -- test for valid, can be text file
+        end
+
+        xml.include(ctxdata.xmldata,'ctx:include','name', table.append({'.', file.dirname(ctxdata.ctxname)},ctxdata.locations))
+
+        ctxdata.variables['job'] = ctxdata.jobname
+
+        ctxdata.flags        = xml.collect_texts(ctxdata.xmldata,"/ctx:job/ctx:flags/ctx:flag",true)
+        ctxdata.environments = xml.collect_texts(ctxdata.xmldata,"/ctx:job/ctx:process/ctx:resources/ctx:environment",true)
+        ctxdata.modules      = xml.collect_texts(ctxdata.xmldata,"/ctx:job/ctx:process/ctx:resources/ctx:module",true)
+        ctxdata.filters      = xml.collect_texts(ctxdata.xmldata,"/ctx:job/ctx:process/ctx:resources/ctx:filter",true)
+        ctxdata.modes        = xml.collect_texts(ctxdata.xmldata,"/ctx:job/ctx:process/ctx:resources/ctx:mode",true)
+        ctxdata.messages     = xml.collect_texts(ctxdata.xmldata,"ctx:message",true)
+
+        ctxdata.flags = ctxrunner.reflag(ctxdata.flags)
+
+        local messages = ctxdata.messages
+        for i=1,#messages do
+            logs.simple("ctx comment: %s", xml.tostring(messages[i]))
+        end
+
+        for r, d, k in xml.elements(ctxdata.xmldata,"ctx:value[@name='job']") do
+            d[k] = ctxdata.variables['job'] or ""
+        end
+
+        local commands = { }
+        for e in xml.collected(ctxdata.xmldata,"/ctx:job/ctx:preprocess/ctx:processors/ctx:processor") do
+            commands[e.at and e.at['name'] or "unknown"] = e
+        end
+
+        local suffix   = xml.filter(ctxdata.xmldata,"/ctx:job/ctx:preprocess/attribute('suffix')") or ctxdata.suffix
+        local runlocal = xml.filter(ctxdata.xmldata,"/ctx:job/ctx:preprocess/ctx:processors/attribute('local')")
+
+        runlocal = toboolean(runlocal)
+
+        for files in xml.collected(ctxdata.xmldata,"/ctx:job/ctx:preprocess/ctx:files") do
+            for pattern in xml.collected(files,"ctx:file") do
+
+                preprocessor = pattern.at['processor'] or ""
+
+                if preprocessor ~= "" then
+
+                    ctxdata.variables['old'] = ctxdata.jobname
+                    for r, d, k in xml.elements(ctxdata.xmldata,"ctx:value") do
+                        local ek = d[k]
+                        local ekat = ek.at['name']
+                        if ekat == 'old' then
+                            d[k] = ctxrunner.substitute(ctxdata.variables[ekat] or "")
+                        end
+                    end
+
+                    pattern = ctxrunner.justtext(xml.tostring(pattern))
+
+                    local oldfiles = dir.glob(pattern)
+
+                    local pluspath = false
+                    if #oldfiles == 0 then
+                        -- message: no files match pattern
+                        local paths = ctxdata.paths
+                        for i=1,#paths do
+                            local p = paths[i]
+                            local oldfiles = dir.glob(path.join(p,pattern))
+                            if #oldfiles > 0 then
+                                pluspath = true
+                                break
+                            end
+                        end
+                    end
+                    if #oldfiles == 0 then
+                        -- message: no old files
+                    else
+                        for i=1,#oldfiles do
+                            local oldfile = oldfiles[i]
+                            local newfile = oldfile .. "." .. suffix -- addsuffix will add one only
+                            if ctxdata.runlocal then
+                                newfile = file.basename(newfile)
+                            end
+                            if oldfile ~= newfile and file.needsupdate(oldfile,newfile) then
+                            --  message: oldfile needs preprocessing
+                            --  os.remove(newfile)
+                                local splitted = preprocessor:split(',')
+                                for i=1,#splitted do
+                                    local pp = splitted[i]
+                                    local command = commands[pp]
+                                    if command then
+                                        command = xml.copy(command)
+                                        local suf = (command.at and command.at['suffix']) or ctxdata.suffix
+                                        if suf then
+                                            newfile = oldfile .. "." .. suf
+                                        end
+                                        if ctxdata.runlocal then
+                                            newfile = file.basename(newfile)
+                                        end
+                                        for r, d, k in xml.elements(command,"ctx:old") do
+                                            d[k] = ctxrunner.substitute(oldfile)
+                                        end
+                                        for r, d, k in xml.elements(command,"ctx:new") do
+                                            d[k] = ctxrunner.substitute(newfile)
+                                        end
+                                        ctxdata.variables['old'] = oldfile
+                                        ctxdata.variables['new'] = newfile
+                                        for r, d, k in xml.elements(command,"ctx:value") do
+                                            local ek = d[k]
+                                            local ekat = ek.at and ek.at['name']
+                                            if ekat then
+                                                d[k] = ctxrunner.substitute(ctxdata.variables[ekat] or "")
+                                            end
+                                        end
+                                        -- potential optimization: when mtxrun run internal
+                                        command = xml.content(command)
+                                        command = ctxrunner.justtext(command)
+                                        logs.simple("command: %s",command)
+                                        local result = os.spawn(command) or 0
+                                        -- somehow we get the wrong return value
+                                        if result > 0 then
+                                            logs.simple("error, return code: %s",result)
+                                        end
+                                        if ctxdata.runlocal then
+                                            oldfile = file.basename(oldfile)
+                                        end
+                                    end
+                                end
+                                if lfs.isfile(newfile) then
+                                    file.syncmtimes(oldfile,newfile)
+                                    ctxdata.prepfiles[oldfile] = true
+                                else
+                                    logs.simple("error, check target location of new file: %s", newfile)
+                                    ctxdata.prepfiles[oldfile] = false
+                                end
+                            else
+                                logs.simple("old file needs no preprocessing")
+                                ctxdata.prepfiles[oldfile] = lfs.isfile(newfile)
+                            end
+                        end
+                    end
+                end
+            end
+        end
+
+        ctxrunner.savelog(ctxdata)
+
+    end
+
+    function ctxrunner.preppedfile(ctxdata,filename)
+        if ctxdata.prepfiles[file.basename(filename)] then
+            return filename .. ".prep"
+        else
+            return filename
+        end
+    end
+
+end
+
+-- rest
+
+scripts.context.multipass = {
+--  suffixes = { ".tuo", ".tuc" },
+    suffixes = { ".tuc" },
+    nofruns = 8,
+}
+
+function scripts.context.multipass.hashfiles(jobname)
+    local hash = { }
+    local suffixes = scripts.context.multipass.suffixes
+    for i=1,#suffixes do
+        local suffix = suffixes[i]
+        local full = jobname .. suffix
+        hash[full] = md5.hex(io.loaddata(full) or "unknown")
+    end
+    return hash
+end
+
+function scripts.context.multipass.changed(oldhash, newhash)
+    for k,v in next, oldhash do
+        if v ~= newhash[k] then
+            return true
+        end
+    end
+    return false
+end
+
+scripts.context.backends = {
+    pdftex = 'pdftex',
+    luatex = 'pdftex',
+    pdf    = 'pdftex',
+    dvi    = 'dvipdfmx',
+    dvips  = 'dvips'
+}
+
+function scripts.context.multipass.makeoptionfile(jobname,ctxdata,kindofrun,currentrun,finalrun)
+    -- take jobname from ctx
+    jobname = file.removesuffix(jobname)
+    local f = io.open(jobname..".top","w")
+    if f then
+        local function someflag(flag)
+            return (ctxdata and ctxdata.flags[flag]) or environment.argument(flag)
+        end
+        local function setvalue(flag,format,hash,default)
+            local a = someflag(flag) or default
+            if a and a ~= "" then
+                if hash then
+                    if hash[a] then
+                        f:write(format:format(a),"\n")
+                    end
+                else
+                    f:write(format:format(a),"\n")
+                end
+            end
+        end
+        local function setvalues(flag,format,plural)
+            if type(flag) == "table"  then
+                for k, v in next, flag do
+                    f:write(format:format(v),"\n")
+                end
+            else
+                local a = someflag(flag) or (plural and someflag(flag.."s"))
+                if a and a ~= "" then
+                    for v in a:gmatch("%s*([^,]+)") do
+                        f:write(format:format(v),"\n")
+                    end
+                end
+            end
+        end
+        local function setfixed(flag,format,...)
+            if someflag(flag) then
+                f:write(format:format(...),"\n")
+            end
+        end
+        local function setalways(format,...)
+            f:write(format:format(...),"\n")
+        end
+        --
+        setalways("%% runtime options files (command line driven)")
+        --
+        setalways("\\unprotect")
+        --
+        setalways("%% special commands, mostly for the ctx development team")
+        --
+        if environment.argument("dumpdelta") then
+            setalways("\\tracersdumpdelta")
+        elseif environment.argument("dumphash") then
+            setalways("\\tracersdumphash")
+        end
+        setalways("%% feedback and basic job control")
+        if type(environment.argument("track")) == "string" then
+            setvalue ("track"    , "\\enabletrackers[%s]")
+        end
+        if type(environment.argument("trackers")) == "string" then
+            setvalue ("trackers" , "\\enabletrackers[%s]")
+        end
+        if type(environment.argument("directives")) == "string" then
+            setvalue ("directives", "\\enabledirectives[%s]")
+        end
+        setfixed ("timing"       , "\\usemodule[timing]")
+        setfixed ("batchmode"    , "\\batchmode")
+        setfixed ("batch"        , "\\batchmode")
+        setfixed ("nonstopmode"  , "\\nonstopmode")
+        setfixed ("nonstop"      , "\\nonstopmode")
+        setfixed ("tracefiles"   , "\\tracefilestrue")
+        setfixed ("nostats"      , "\\nomkivstatistics")
+        setfixed ("paranoid"     , "\\def\\maxreadlevel{1}")
+        --
+        setalways("%% handy for special styles")
+        --
+        setalways("\\startluacode")
+        setalways("document = document or { }")
+        setalways(table.serialize(environment.arguments, "document.arguments"))
+        setalways(table.serialize(environment.files,     "document.files"))
+        setalways("\\stopluacode")
+        --
+        setalways("%% process info")
+        --
+    --  setvalue ("inputfile"    , "\\setupsystem[inputfile=%s]")
+        setalways(                 "\\setupsystem[inputfile=%s]",environment.argument("input") or environment.files[1] or "\\jobname")
+        setvalue ("result"       , "\\setupsystem[file=%s]")
+        setalways(                 "\\setupsystem[\\c!n=%s,\\c!m=%s]", kindofrun or 0, currentrun or 0)
+    --  setalways(                 "\\setupsystem[\\c!type=%s]",os.type) -- windows or unix
+        setvalues("path"         , "\\usepath[%s]")
+        setvalue ("setuppath"    , "\\setupsystem[\\c!directory={%s}]")
+        setvalue ("randomseed"   , "\\setupsystem[\\c!random=%s]")
+        setvalue ("arguments"    , "\\setupenv[%s]")
+        setalways("%% modes")
+        setvalues("modefile"     , "\\readlocfile{%s}{}{}")
+        setvalues("mode"         , "\\enablemode[%s]", true)
+        if ctxdata then
+            setvalues(ctxdata.modes, "\\enablemode[%s]")
+        end
+        --
+        setalways("%% options (not that important)")
+        --
+        setalways("\\startsetups *runtime:options")
+        setvalue ('output'       , "\\setupoutput[%s]", scripts.context.backends, 'pdftex')
+        setfixed ("color"        , "\\setupcolors[\\c!state=\\v!start]")
+        setvalue ("separation"   , "\\setupcolors[\\c!split=%s]")
+        setfixed ("noarrange"    , "\\setuparranging[\\v!disable]")
+        if environment.argument('arrange') and not finalrun then
+            setalways(             "\\setuparranging[\\v!disable]")
+        end
+        setalways("\\stopsetups")
+        --
+        setalways("%% styles and modules")
+        --
+        setalways("\\startsetups *runtime:modules")
+        setvalues("filter"       , "\\useXMLfilter[%s]", true)
+        setvalues("usemodule"    , "\\usemodule[%s]", true)
+        setvalues("environment"  , "\\environment %s ", true)
+        if ctxdata then
+            setvalues(ctxdata.modules,      "\\usemodule[%s]")
+            setvalues(ctxdata.environments, "\\environment %s ")
+        end
+        setalways("\\stopsetups")
+        --
+        setalways("%% done")
+        --
+        setalways("\\protect \\endinput")
+        f:close()
+    end
+end
+
+function scripts.context.multipass.copyluafile(jobname)
+--  io.savedata(jobname..".tuc",io.loaddata(jobname..".tua") or "")
+    local tuaname, tucname = jobname..".tua", jobname..".tuc"
+    if lfs.isfile(tuaname) then
+        os.remove(tucname)
+        os.rename(tuaname,tucname)
+    end
+end
+
+-- obsolete:
+--
+-- function scripts.context.multipass.copytuifile(jobname)
+--     local tuiname, tuoname = jobname .. ".tui", jobname .. ".tuo"
+--     if lfs.isfile(tuiname) then
+--         local f, g = io.open(tuiname), io.open(tuoname,'w')
+--         if f and g then
+--             g:write("% traditional utility file, only commands written by mtxrun/context\n%\n")
+--             for line in f:lines() do
+--                 if line:find("^c ") then
+--                     g:write((line:gsub("^c ","")),"%\n")
+--                 end
+--             end
+--             g:write("\\endinput\n")
+--             f:close()
+--             g:close()
+--         end
+--     else
+--     --  os.remove(tuoname)
+--     end
+-- end
+
+scripts.context.cldsuffixes = table.tohash {
+    "cld",
+}
+
+scripts.context.xmlsuffixes = table.tohash {
+    "xml",
+}
+
+scripts.context.luasuffixes = table.tohash {
+    "lua",
+}
+
+scripts.context.beforesuffixes = {
+    "tuo", "tuc"
+}
+scripts.context.aftersuffixes = {
+    "pdf", "tuo", "tuc", "log"
+}
+
+scripts.context.interfaces = {
+    en = "cont-en",
+    uk = "cont-uk",
+    de = "cont-de",
+    fr = "cont-fr",
+    nl = "cont-nl",
+    cz = "cont-cz",
+    it = "cont-it",
+    ro = "cont-ro",
+    pe = "cont-pe",
+}
+
+scripts.context.defaultformats  = {
+    "cont-en",
+    "cont-nl",
+    "mptopdf",
+--  "metatex",
+    "metafun",
+--  "plain"
+}
+
+local function analyze(filename)
+    local f = io.open(file.addsuffix(filename,"tex"))
+    if f then
+        local t = { }
+        local line = f:read("*line") or ""
+        local preamble = line:match("[\254\255]*%%%s+(.+)$") -- there can be an utf bomb in front
+        if preamble then
+            for key, value in preamble:gmatch("(%S+)=(%S+)") do
+                t[key] = value
+            end
+            t.type = "tex"
+        elseif line:find("^<?xml ") then
+            t.type = "xml"
+        end
+        if t.nofruns then
+            scripts.context.multipass.nofruns = t.nofruns
+        end
+        if not t.engine then
+            t.engine = 'luatex'
+        end
+        f:close()
+        return t
+    end
+    return nil
+end
+
+local function makestub(format,filename,prepname)
+    local stubname = file.replacesuffix(file.basename(filename),'run')
+    local f = io.open(stubname,'w')
+    if f then
+        f:write("\\starttext\n")
+        f:write(string.format(format,prepname or filename),"\n")
+        f:write("\\stoptext\n")
+        f:close()
+        filename = stubname
+    end
+    return filename
+end
+
+--~ function scripts.context.openpdf(name)
+--~     os.spawn(string.format('pdfopen --file "%s" 2>&1', file.replacesuffix(name,"pdf")))
+--~ end
+--~ function scripts.context.closepdf(name)
+--~     os.spawn(string.format('pdfclose --file "%s" 2>&1', file.replacesuffix(name,"pdf")))
+--~ end
+
+local pdfview -- delayed loading
+
+function scripts.context.openpdf(name)
+    pdfview = pdfview or dofile(resolvers.find_file("l-pdfview.lua","tex"))
+    logs.simple("pdfview methods: %s, current method: %s, MTX_PDFVIEW_METHOD=%s",pdfview.methods(),pdfview.method,os.getenv(pdfview.METHOD) or "<unset>")
+    pdfview.open(file.replacesuffix(name,"pdf"))
+end
+
+function scripts.context.closepdf(name)
+    pdfview = pdfview or dofile(resolvers.find_file("l-pdfview.lua","tex"))
+    pdfview.close(file.replacesuffix(name,"pdf"))
+end
+
+function scripts.context.run(ctxdata,filename)
+    -- filename overloads environment.files
+    local files = (filename and { filename }) or environment.files
+    if ctxdata then
+        -- todo: interface
+        for k,v in next, ctxdata.flags do
+            environment.setargument(k,v)
+        end
+    end
+    if #files > 0 then
+        --
+        local interface = environment.argument("interface")
+        -- todo: environment.argument("interface","en")
+        interface = (type(interface) == "string" and interface) or "en"
+        --
+        local formatname = scripts.context.interfaces[interface] or "cont-en"
+        local formatfile, scriptfile = resolvers.locate_format(formatname)
+        -- this catches the command line
+        if not formatfile or not scriptfile then
+            logs.simple("warning: no format found, forcing remake (commandline driven)")
+            scripts.context.generate()
+            scripts.context.make(formatname)
+            formatfile, scriptfile = resolvers.locate_format(formatname)
+        end
+        --
+        if formatfile and scriptfile then
+            for i=1,#files do
+                local filename = files[i]
+                local basename, pathname = file.basename(filename), file.dirname(filename)
+                local jobname = file.removesuffix(basename)
+                if pathname == "" then
+                    filename = "./" .. filename
+                end
+                -- look at the first line
+                local a = analyze(filename)
+                if a and (a.engine == 'pdftex' or a.engine == 'xetex' or environment.argument("pdftex") or environment.argument("xetex")) then
+                    if false then
+                        -- we need to write a top etc too and run mp etc so it's not worth the
+                        -- trouble, so it will take a while before the next is finished
+                        --
+                        -- require "mtx-texutil.lua"
+                    else
+                        local texexec = resolvers.find_file("texexec.rb") or ""
+                        if texexec ~= "" then
+                            os.setenv("RUBYOPT","")
+                            local command = string.format("ruby %s %s",texexec,environment.reconstruct_commandline(environment.arguments_after))
+                            os.exec(command)
+                        end
+                    end
+                else
+                    if a and a.interface and a.interface ~= interface then
+                        formatname = scripts.context.interfaces[a.interface] or formatname
+                        formatfile, scriptfile = resolvers.locate_format(formatname)
+                    end
+                    -- this catches the command line
+                    if not formatfile or not scriptfile then
+                        logs.simple("warning: no format found, forcing remake (source driven)")
+                        scripts.context.generate()
+                        scripts.context.make(formatname)
+                        formatfile, scriptfile = resolvers.locate_format(formatname)
+                    end
+                    if formatfile and scriptfile then
+                        -- we default to mkiv xml !
+                        -- the --prep argument might become automatic (and noprep)
+                        local suffix = file.extname(filename) or "?"
+                        if scripts.context.xmlsuffixes[suffix] or environment.argument("forcexml") then
+                            if environment.argument("mkii") then
+                                filename = makestub("\\processXMLfilegrouped{%s}",filename)
+                            else
+                                filename = makestub("\\xmlprocess{\\xmldocument}{%s}{}",filename)
+                            end
+                        elseif scripts.context.cldsuffixes[suffix] or environment.argument("forcecld") then
+                            filename = makestub("\\ctxlua{context.runfile('%s')}",filename)
+                        elseif scripts.context.luasuffixes[suffix] or environment.argument("forcelua") then
+                            filename = makestub("\\ctxlua{dofile('%s')}",filename)
+                        elseif environment.argument("prep") then
+                            -- we need to keep the original jobname
+                            filename = makestub("\\readfile{%s}{}{}",filename,ctxrunner.preppedfile(ctxdata,filename))
+                        end
+                        --
+                        -- todo: also other stubs
+                        --
+                        local suffix, resultname = environment.argument("suffix"), environment.argument("result")
+                        if type(suffix) == "string" then
+                            resultname = file.removesuffix(jobname) .. suffix
+                        end
+                        local oldbase, newbase = "", ""
+                        if type(resultname) == "string" then
+                            oldbase = file.removesuffix(jobname)
+                            newbase = file.removesuffix(resultname)
+                            if oldbase ~= newbase then
+                                for _, suffix in next, scripts.context.beforesuffixes do
+                                    local oldname = file.addsuffix(oldbase,suffix)
+                                    local newname = file.addsuffix(newbase,suffix)
+                                    local tmpname = "keep-"..oldname
+                                    os.remove(tmpname)
+                                    os.rename(oldname,tmpname)
+                                    os.remove(oldname)
+                                    os.rename(newname,oldname)
+                                end
+                            else
+                                resultname = nil
+                            end
+                        else
+                            resultname = nil
+                        end
+                        --
+                        if environment.argument("autopdf") then
+                            scripts.context.closepdf(filename)
+                            if resultname then
+                                scripts.context.closepdf(resultname)
+                            end
+                        end
+                        --
+                        local okay = statistics.check_fmt_status(formatfile)
+                        if okay ~= true then
+                            logs.simple("warning: %s, forcing remake",tostring(okay))
+                            scripts.context.generate()
+                            scripts.context.make(formatname)
+                        end
+                        --
+                        local flags = { }
+                        if environment.argument("batchmode") or environment.argument("batch") then
+                            flags[#flags+1] = "--interaction=batchmode"
+                        end
+                        if environment.argument("synctex") then
+                            logs.simple("warning: syntex is enabled") -- can add upto 5% runtime
+                            flags[#flags+1] = "--synctex=1"
+                        end
+                        flags[#flags+1] = "--fmt=" .. string.quote(formatfile)
+                        flags[#flags+1] = "--lua=" .. string.quote(scriptfile)
+                        flags[#flags+1] = "--backend=pdf"
+                        local command = string.format("luatex %s %s", table.concat(flags," "), string.quote(filename))
+                        local oldhash, newhash = scripts.context.multipass.hashfiles(jobname), { }
+                        local once = environment.argument("once")
+                        local maxnofruns = (once and 1) or scripts.context.multipass.nofruns
+                        local arrange = environment.argument("arrange")
+                        for i=1,maxnofruns do
+                            -- 1:first run, 2:successive run, 3:once, 4:last of maxruns
+                            local kindofrun = (once and 3) or (i==1 and 1) or (i==maxnofruns and 4) or 2
+                            scripts.context.multipass.makeoptionfile(jobname,ctxdata,kindofrun,i,false) -- kindofrun, currentrun, final
+                            logs.simple("run %s: %s",i,command)
+                            local returncode, errorstring = os.spawn(command)
+                        --~ if returncode == 3 then
+                        --~     scripts.context.generate()
+                        --~     scripts.context.make(formatname)
+                        --~     returncode, errorstring = os.spawn(command)
+                        --~     if returncode == 3 then
+                        --~         logs.simple("ks: return code 3, message: %s",errorstring or "?")
+                        --~         os.exit(1)
+                        --~     end
+                        --~ end
+                            if not returncode then
+                                logs.simple("fatal error: no return code, message: %s",errorstring or "?")
+                                os.exit(1)
+                                break
+                            elseif returncode > 0 then
+                                logs.simple("fatal error: return code: %s",returncode or "?")
+                                os.exit(returncode)
+                                break
+                            else
+                                scripts.context.multipass.copyluafile(jobname)
+                            --  scripts.context.multipass.copytuifile(jobname)
+                                newhash = scripts.context.multipass.hashfiles(jobname)
+                                if scripts.context.multipass.changed(oldhash,newhash) then
+                                    oldhash = newhash
+                                else
+                                    break
+                                end
+                            end
+                        end
+                        --
+                        if arrange then
+                            local kindofrun = 3
+                            scripts.context.multipass.makeoptionfile(jobname,ctxdata,kindofrun,i,true) -- kindofrun, currentrun, final
+                            logs.simple("arrange run: %s",command)
+                            local returncode, errorstring = os.spawn(command)
+                            if not returncode then
+                                logs.simple("fatal error: no return code, message: %s",errorstring or "?")
+                                os.exit(1)
+                            elseif returncode > 0 then
+                                logs.simple("fatal error: return code: %s",returncode or "?")
+                                os.exit(returncode)
+                            end
+                        end
+                        --
+                        if environment.argument("purge") then
+                            scripts.context.purge_job(jobname)
+                        elseif environment.argument("purgeall") then
+                            scripts.context.purge_job(jobname,true)
+                        end
+                        --
+                        os.remove(jobname..".top")
+                        --
+                        if resultname then
+                            for _, suffix in next, scripts.context.aftersuffixes do
+                                local oldname = file.addsuffix(oldbase,suffix)
+                                local newname = file.addsuffix(newbase,suffix)
+                                local tmpname = "keep-"..oldname
+                                os.remove(newname)
+                                os.rename(oldname,newname)
+                                os.rename(tmpname,oldname)
+                            end
+                            logs.simple("result renamed to: %s",newbase)
+                        end
+                        --
+                        if environment.argument("purge") then
+                            scripts.context.purge_job(resultname)
+                        elseif environment.argument("purgeall") then
+                            scripts.context.purge_job(resultname,true)
+                        end
+                        --
+                        if environment.argument("autopdf") then
+                            scripts.context.openpdf(resultname or filename)
+                        end
+                        --
+                        if environment.argument("timing") then
+                            logs.line()
+                            logs.simple("you can process (timing) statistics with:",jobname)
+                            logs.line()
+                            logs.simple("context --extra=timing '%s'",jobname)
+                            logs.simple("mtxrun --script timing --xhtml [--launch --remove] '%s'",jobname)
+                            logs.line()
+                        end
+                    else
+                        if formatname then
+                            logs.simple("error, no format found with name: %s, skipping",formatname)
+                        else
+                            logs.simple("error, no format found (provide formatname or interface)")
+                        end
+                        break
+                    end
+                end
+            end
+        else
+            if formatname then
+                logs.simple("error, no format found with name: %s, aborting",formatname)
+            else
+                logs.simple("error, no format found (provide formatname or interface)")
+            end
+        end
+    end
+end
+
+function scripts.context.pipe()
+    -- context --pipe
+    -- context --pipe --purge --dummyfile=whatever.tmp
+    local interface = environment.argument("interface")
+    interface = (type(interface) == "string" and interface) or "en"
+    local formatname = scripts.context.interfaces[interface] or "cont-en"
+    local formatfile, scriptfile = resolvers.locate_format(formatname)
+    if not formatfile or not scriptfile then
+        logs.simple("warning: no format found, forcing remake (commandline driven)")
+        scripts.context.generate()
+        scripts.context.make(formatname)
+        formatfile, scriptfile = resolvers.locate_format(formatname)
+    end
+    if formatfile and scriptfile then
+        local okay = statistics.check_fmt_status(formatfile)
+        if okay ~= true then
+            logs.simple("warning: %s, forcing remake",tostring(okay))
+            scripts.context.generate()
+            scripts.context.make(formatname)
+        end
+        local flags = {
+            "--interaction=scrollmode",
+            "--fmt=" .. string.quote(formatfile),
+            "--lua=" .. string.quote(scriptfile),
+            "--backend=pdf",
+        }
+        local filename = environment.argument("dummyfile") or ""
+        if filename == "" then
+            filename = "\\relax"
+            logs.simple("entering scrollmode, end job with \\end")
+        else
+            filename = file.addsuffix(filename,"tmp")
+            io.savedata(filename,"\\relax")
+            scripts.context.multipass.makeoptionfile(filename,{ flags = flags },3,1,false) -- kindofrun, currentrun, final
+            logs.simple("entering scrollmode using '%s' with optionfile, end job with \\end",filename)
+        end
+        local command = string.format("luatex %s %s", table.concat(flags," "), string.quote(filename))
+        os.spawn(command)
+        if environment.argument("purge") then
+            scripts.context.purge_job(filename)
+        elseif environment.argument("purgeall") then
+            scripts.context.purge_job(filename,true)
+            os.remove(filename)
+        end
+    else
+        if formatname then
+            logs.simple("error, no format found with name: %s, aborting",formatname)
+        else
+            logs.simple("error, no format found (provide formatname or interface)")
+        end
+    end
+end
+
+function scripts.context.make(name)
+    local runners = {
+        "luatools --make --compile ",
+        (environment.argument("pdftex") and "mtxrun texexec.rb --make --pdftex ") or false,
+        (environment.argument("xetex")  and "mtxrun texexec.rb --make --xetex " ) or false,
+    }
+    local list = (name and { name }) or (environment.files[1] and environment.files) or scripts.context.defaultformats
+    for i=1,#list do
+        local name = list[i]
+        name = scripts.context.interfaces[name] or name
+        for i=1,#runners do
+            local runner = runners[i]
+            if runner then
+                local command = runner .. name
+                logs.simple("running command: %s",command)
+                os.spawn(command)
+            end
+        end
+    end
+end
+
+function scripts.context.generate()
+    -- hack, should also be a shared function
+    local command = "luatools --generate "
+    logs.simple("running command: %s",command)
+    os.spawn(command)
+end
+
+function scripts.context.ctx()
+    local ctxdata = ctxrunner.new()
+    ctxdata.jobname = environment.files[1]
+    ctxrunner.manipulate(ctxdata,environment.argument("ctx"))
+    scripts.context.run(ctxdata)
+end
+
+function scripts.context.autoctx()
+    local ctxdata = nil
+    local files = (filename and { filename }) or environment.files
+    local firstfile = #files > 0 and files[1]
+    if firstfile and file.extname(firstfile) == "xml" then
+        local f = io.open(firstfile)
+        if f then
+            local chunk = f:read(512) or ""
+            f:close()
+            local ctxname = string.match(chunk,"<%?context%-directive%s+job%s+ctxfile%s+([^ ]-)%s*?>")
+            if ctxname then
+                ctxdata = ctxrunner.new()
+                ctxdata.jobname = firstfile
+                ctxrunner.manipulate(ctxdata,ctxname)
+            end
+        end
+    end
+    scripts.context.run(ctxdata)
+end
+
+-- todo: quite after first image
+
+local template = [[
+    \starttext
+        \startMPpage %% %s
+            input "%s" ;
+        \stopMPpage
+    \stoptext
+]]
+
+local loaded = false
+
+function scripts.context.metapost()
+    local filename = environment.files[1] or ""
+    if not loaded then
+        dofile(resolvers.find_file("mlib-run.lua"))
+        loaded = true
+        commands = commands or { }
+        commands.writestatus = logs.report
+    end
+    local formatname = environment.argument("format") or "metafun"
+    if formatname == "" or type(format) == "boolean" then
+        formatname = "metafun"
+    end
+    if environment.argument("pdf") then
+        local basename = file.removesuffix(filename)
+        local resultname = environment.argument("result") or basename
+        local jobname = "mtx-context-metapost"
+        local tempname = file.addsuffix(jobname,"tex")
+        io.savedata(tempname,string.format(template,"metafun",filename))
+        environment.files[1] = tempname
+        environment.setargument("result",resultname)
+        environment.setargument("once",true)
+        scripts.context.run()
+        scripts.context.purge_job(jobname,true)
+        scripts.context.purge_job(resultname,true)
+    elseif environment.argument("svg") then
+        metapost.directrun(formatname,filename,"svg")
+    else
+        metapost.directrun(formatname,filename,"mps")
+    end
+end
+
+function scripts.context.version()
+    local name = resolvers.find_file("context.tex")
+    if name ~= "" then
+        logs.simple("main context file: %s",name)
+        local data = io.loaddata(name)
+        if data then
+            local version = data:match("\\edef\\contextversion{(.-)}")
+            if version then
+                logs.simple("current version: %s",version)
+            else
+                logs.simple("context version: unknown, no timestamp found")
+            end
+        else
+            logs.simple("context version: unknown, load error")
+        end
+    else
+        logs.simple("main context file: unknown, 'context.tex' not found")
+    end
+end
+
+local generic_files = {
+    "texexec.tex", "texexec.tui", "texexec.tuo",
+    "texexec.tuc", "texexec.tua",
+    "texexec.ps", "texexec.pdf", "texexec.dvi",
+    "cont-opt.tex", "cont-opt.bak"
+}
+
+local obsolete_results = {
+    "dvi",
+}
+
+local temporary_runfiles = {
+    "tui", "tua", "tup", "ted", "tes", "top",
+    "log", "tmp", "run", "bck", "rlg",
+    "mpt", "mpx", "mpd", "mpo", "mpb", "ctl",
+    "synctex.gz", "pgf"
+}
+
+local persistent_runfiles = {
+    "tuo", "tub", "top", "tuc"
+}
+
+local function purge_file(dfile,cfile)
+    if cfile and lfs.isfile(cfile) then
+        if os.remove(dfile) then
+            return file.basename(dfile)
+        end
+    elseif dfile then
+        if os.remove(dfile) then
+            return file.basename(dfile)
+        end
+    end
+end
+
+function scripts.context.purge_job(jobname,all)
+    if jobname and jobname ~= "" then
+        jobname = file.basename(jobname)
+        local filebase = file.removesuffix(jobname)
+        local deleted = { }
+        for i=1,#obsolete_results do
+            deleted[#deleted+1] = purge_file(filebase.."."..obsolete_results[i],filebase..".pdf")
+        end
+        for i=1,#temporary_runfiles do
+            deleted[#deleted+1] = purge_file(filebase.."."..temporary_runfiles[i])
+        end
+        if all then
+            for i=1,#persistent_runfiles do
+                deleted[#deleted+1] = purge_file(filebase.."."..persistent_runfiles[i])
+            end
+        end
+        if #deleted > 0 then
+            logs.simple("purged files: %s", table.join(deleted,", "))
+        end
+    end
+end
+
+function scripts.context.purge(all)
+    local all = all or environment.argument("all")
+    local pattern = environment.argument("pattern") or "*.*"
+    local files = dir.glob(pattern)
+    local obsolete = table.tohash(obsolete_results)
+    local temporary = table.tohash(temporary_runfiles)
+    local persistent = table.tohash(persistent_runfiles)
+    local generic = table.tohash(generic_files)
+    local deleted = { }
+    for i=1,#files do
+        local name = files[i]
+        local suffix = file.extname(name)
+        local basename = file.basename(name)
+        if obsolete[suffix] or temporary[suffix] or persistent[suffix] or generic[basename] then
+            deleted[#deleted+1] = purge_file(name)
+        end
+    end
+    if #deleted > 0 then
+        logs.simple("purged files: %s", table.join(deleted,", "))
+    end
+end
+
+--~ purge_for_files("test",true)
+--~ purge_all_files()
+
+local function touch(name,pattern)
+    local name = resolvers.find_file(name)
+    local olddata = io.loaddata(name)
+    if olddata then
+        local oldversion, newversion = "", os.date("%Y.%m.%d %H:%M")
+        local newdata, ok = olddata:gsub(pattern,function(pre,mid,post)
+            oldversion = mid
+            return pre .. newversion .. post
+        end)
+        if ok > 0 then
+            local backup = file.replacesuffix(name,"tmp")
+            os.remove(backup)
+            os.rename(name,backup)
+            io.savedata(name,newdata)
+            return true, oldversion, newversion, name
+        else
+            return false
+        end
+    end
+end
+
+function scripts.context.touch()
+    if environment.argument("expert") then
+        local done, oldversion, newversion, foundname = touch("context.tex", "(\\edef\\contextversion{)(.-)(})")
+        if done then
+            logs.simple("old version : %s", oldversion)
+            logs.simple("new version : %s", newversion)
+            logs.simple("touched file: %s", foundname)
+            local ok, _, _, foundname = touch("cont-new.tex", "(\\newcontextversion{)(.-)(})")
+            if ok then
+                logs.simple("touched file: %s", foundname)
+            end
+            local ok, _, _, foundname = touch("cont-xp.tex", "(\\edef\\contextversion{)(.-)(})")
+            if ok then
+                logs.simple("touched file: %s", foundname)
+            end
+        end
+    end
+end
+
+-- extras
+
+function scripts.context.extras(pattern)
+    local found = resolvers.find_file("context.tex")
+    if found == "" then
+        logs.simple("unknown extra: %s", extra)
+    else
+        pattern = file.join(dir.expand_name(file.dirname(found)),string.format("mtx-context-%s.tex",pattern or "*"))
+        local list = dir.glob(pattern)
+        if not extra or extra == "" then
+            logs.extendbanner("extras")
+        else
+            logs.extendbanner(extra)
+        end
+        for i=1,#list do
+            local v = list[i]
+            local data = io.loaddata(v) or ""
+            data = string.match(data,"begin help(.-)end help")
+            if data then
+                local h = { string.format("extra: %s (%s)",string.gsub(v,"^.*mtx%-context%-(.-)%.tex$","%1"),v) }
+                for s in string.gmatch(data,"%% *(.-)[\n\r]") do
+                    h[#h+1] = s
+                end
+                h[#h+1] = ""
+                logs.help(table.concat(h,"\n"),"nomoreinfo")
+            end
+        end
+    end
+end
+
+function scripts.context.extra()
+    local extra = environment.argument("extra")
+    if type(extra) == "string" then
+        if environment.argument("help") then
+            scripts.context.extras(extra)
+        else
+            local fullextra = extra
+            if not string.find(fullextra,"mtx%-context%-") then
+                fullextra = "mtx-context-" .. extra
+            end
+            local foundextra = resolvers.find_file(fullextra)
+            if foundextra == "" then
+                scripts.context.extras()
+                return
+            else
+                logs.simple("processing extra: %s", foundextra)
+            end
+            environment.setargument("purgeall",true)
+            local result = environment.setargument("result") or ""
+            if result == "" then
+                environment.setargument("result","context-extra")
+            end
+            scripts.context.run(nil,foundextra)
+        end
+    else
+        scripts.context.extras()
+    end
+end
+
+-- todo: we need to do a dummy run
+
+function scripts.context.trackers()
+    environment.files = { resolvers.find_file("m-trackers.tex") }
+    scripts.context.multipass.nofruns = 1
+    scripts.context.run()
+    -- maybe filter from log
+end
+
+function scripts.context.directives()
+    environment.files = { resolvers.find_file("m-directives.tex") }
+    scripts.context.multipass.nofruns = 1
+    scripts.context.run()
+    -- maybe filter from log
+end
+
+function scripts.context.timed(action)
+    statistics.timed(action)
+end
+
+local zipname    = "cont-tmf.zip"
+local mainzip    = "http://www.pragma-ade.com/context/latest/" .. zipname
+local validtrees = { "texmf-local", "texmf-context" }
+
+function zip.loaddata(zipfile,filename) -- should be in zip lib
+    local f = zipfile:open(filename)
+    if f then
+        local data = f:read("*a")
+        f:close()
+        return data
+    end
+    return nil
+end
+
+function scripts.context.update()
+    local force = environment.argument("force")
+    local socket = require("socket")
+    local http   = require("socket.http")
+    local basepath = resolvers.find_file("context.tex") or ""
+    if basepath == "" then
+        logs.simple("quiting, no 'context.tex' found")
+        return
+    end
+    local basetree = basepath.match(basepath,"^(.-)tex/context/base/context.tex$") or ""
+    if basetree == "" then
+        logs.simple("quiting, no proper tds structure (%s)",basepath)
+        return
+    end
+    local function is_okay(basetree)
+        for _, tree in next, validtrees do
+            local pattern = string.gsub(tree,"%-","%%-")
+            if basetree:find(pattern) then
+                return tree
+            end
+        end
+        return false
+    end
+    local okay = is_okay(basetree)
+    if not okay then
+        logs.simple("quiting, tree '%s' is protected",okay)
+        return
+    else
+        logs.simple("updating tree '%s'",okay)
+    end
+    if not lfs.chdir(basetree) then
+        logs.simple("quiting, unable to change to '%s'",okay)
+        return
+    end
+    logs.simple("fetching '%s'",mainzip)
+    local latest = http.request(mainzip)
+    if not latest then
+        logs.simple("context tree '%s' can be updated, use --force",okay)
+        return
+    end
+    io.savedata("cont-tmf.zip",latest)
+    if false then
+        -- variant 1
+        os.execute("mtxrun --script unzip cont-tmf.zip")
+    else
+        -- variant 2
+        local zipfile = zip.open(zipname)
+        if not zipfile then
+            logs.simple("quiting, unable to open '%s'",zipname)
+            return
+        end
+        local newfile = zip.loaddata(zipfile,"tex/context/base/context.tex")
+        if not newfile then
+            logs.simple("quiting, unable to open '%s'","context.tex")
+            return
+        end
+        local oldfile = io.loaddata(resolvers.find_file("context.tex")) or ""
+        local function versiontonumber(what,str)
+            local version = str:match("\\edef\\contextversion{(.-)}") or ""
+            local year, month, day, hour, minute = str:match("\\edef\\contextversion{(%d+)%.(%d+)%.(%d+) *(%d+)%:(%d+)}")
+            if year and minute then
+                local time = os.time { year=year,month=month,day=day,hour=hour,minute=minute}
+                logs.simple("%s version: %s (%s)",what,version,time)
+                return time
+            else
+                logs.simple("%s version: %s (unknown)",what,version)
+                return nil
+            end
+        end
+        local oldversion = versiontonumber("old",oldfile)
+        local newversion = versiontonumber("new",newfile)
+        if not oldversion or not newversion then
+            logs.simple("quiting, version cannot be determined")
+            return
+        elseif oldversion == newversion then
+            logs.simple("quiting, your current version is up-to-date")
+            return
+        elseif oldversion > newversion then
+            logs.simple("quiting, your current version is newer")
+            return
+        end
+        for k in zipfile:files() do
+            local filename = k.filename
+            if filename:find("/$") then
+                lfs.mkdir(filename)
+            else
+                local data = zip.loaddata(zipfile,filename)
+                if data then
+                    if force then
+                        io.savedata(filename,data)
+                    end
+                    logs.simple(filename)
+                end
+            end
+        end
+        for _, scriptname in next, { "luatools.lua", "mtxrun.lua" } do
+            local oldscript = resolvers.find_file(scriptname) or ""
+            if oldscript ~= "" and is_okay(oldscript) then
+                local newscript = "./scripts/context/lua/" .. scriptname
+                local data = io.loaddata(newscript) or ""
+                if data ~= "" then
+                    logs.simple("replacing script '%s' by '%s'",oldscript,newscript)
+                    if force then
+                        io.savedata(oldscript,data)
+                    end
+                end
+            else
+                logs.simple("keeping script '%s'",oldscript)
+            end
+        end
+        if force then
+            os.execute("context --generate")
+            os.execute("context --make")
+        end
+    end
+    if force then
+        logs.simple("context tree '%s' has been updated",okay)
+    else
+        logs.simple("context tree '%s' can been updated (use --force)",okay)
+    end
+end
+
+logs.extendbanner("ConTeXt Process Management 0.51",true)
+
+messages.help = [[
+--run                 process (one or more) files (default action)
+--make                create context formats
+
+--ctx=name            use ctx file (process management specification)
+--interface           use specified user interface (default: en)
+
+--autopdf             close pdf file in viewer and start pdf viewer afterwards
+--purge(all)          purge files either or not after a run (--pattern=...)
+
+--usemodule=list      load the given module or style, normally part o fthe distribution
+--environment=list    load the given environment file first (document styles)
+--mode=list           enable given the modes (conditional processing in styles)
+--path=list           also consult the given paths when files are looked for
+--arguments=list      set variables that can be consulted during a run (key/value pairs)
+--randomseed=number   set the randomseed
+--result=name         rename the resulting output to the given name
+--trackers=list       show/set tracker variables
+--directives=list     show/set directive variables
+
+--forcexml            force xml stub (optional flag: --mkii)
+--forcecld            force cld (context lua document) stub
+
+--arrange             run extra imposition pass, given that the style sets up imposition
+--noarrange           ignore imposition specifications in the style
+
+--once                only run once (no multipass data file is produced)
+--batchmode           run without stopping and don't show messages on the console
+--nonstopmode         run without stopping
+
+--generate            generate file database etc. (as luatools does)
+--paranoid            don't descend to .. and ../..
+--version             report installed context version
+
+--expert              expert options
+]]
+
+-- filter=list      is kind of obsolete
+-- color            is obsolete for mkiv, always on
+-- separation       is obsolete for mkiv, no longer available
+-- output           is currently obsolete for mkiv
+-- setuppath=list   must check
+-- modefile=name    must check
+-- input=name   load the given inputfile (must check)
+
+messages.expert = [[
+expert options:
+
+--touch               update context version number (remake needed afterwards, also provide --expert)
+--nostats             omit runtime statistics at the end of the run
+--update              update context from website (not to be confused with contextgarden)
+--profile             profile job (use: mtxrun --script profile --analyse)
+--timing              generate timing and statistics overview
+--tracefiles          show some extra info when locating files (at the tex end)
+
+--extra=name          process extra (mtx-context-<name> in distribution)
+--extras              show extras
+]]
+
+messages.private = [[
+private options:
+
+--dumphash            dump hash table afterwards
+--dumpdelta           dump hash table afterwards (only new entries)
+]]
+
+messages.special = [[
+special options:
+
+--pdftex              process file with texexec using pdftex
+--xetex               process file with texexec using xetex
+
+--pipe                don't check for file and enter scroll mode (--dummyfile=whatever.tmp)
+]]
+
+if environment.argument("once") then
+    scripts.context.multipass.nofruns = 1
+elseif environment.argument("runs") then
+    scripts.context.multipass.nofruns = tonumber(environment.argument("runs")) or nil
+end
+
+if environment.argument("profile") then
+    os.setenv("MTX_PROFILE_RUN","YES")
+end
+
+if environment.argument("run") then
+--  scripts.context.timed(scripts.context.run)
+    scripts.context.timed(scripts.context.autoctx)
+elseif environment.argument("make") or environment.argument("generate") then
+    scripts.context.timed(function()
+        if environment.argument("generate") then
+            scripts.context.generate()
+        end
+        if environment.argument("make") then
+            scripts.context.make()
+        end
+    end)
+elseif environment.argument("ctx") then
+    scripts.context.timed(scripts.context.ctx)
+elseif environment.argument("mp") or environment.argument("metapost") then
+    scripts.context.timed(scripts.context.metapost)
+elseif environment.argument("version") then
+    scripts.context.version()
+elseif environment.argument("touch") then
+    scripts.context.touch()
+elseif environment.argument("update") then
+    scripts.context.update()
+elseif environment.argument("expert") then
+    logs.help(table.join({ messages.expert, messages.private, messages.special },"\n"))
+elseif environment.argument("extras") then
+    scripts.context.extras()
+elseif environment.argument("extra") then
+    scripts.context.extra()
+elseif environment.argument("help") then
+    if environment.files[1] == "extras" then
+        scripts.context.extras()
+    else
+        logs.help(messages.help)
+    end
+elseif environment.argument("trackers") and type(environment.argument("trackers")) == "boolean" then
+    scripts.context.trackers()
+elseif environment.argument("directives") and type(environment.argument("directives")) == "boolean" then
+    scripts.context.directives()
+elseif environment.argument("track") and type(environment.argument("track")) == "boolean" then -- for old times sake, will go
+    scripts.context.trackers()
+elseif environment.files[1] then
+--  scripts.context.timed(scripts.context.run)
+    scripts.context.timed(scripts.context.autoctx)
+elseif environment.argument("pipe") then
+    scripts.context.timed(scripts.context.pipe)
+elseif environment.argument("purge") then
+    -- only when no filename given, supports --pattern
+    scripts.context.purge()
+elseif environment.argument("purgeall") then
+    -- only when no filename given, supports --pattern
+    scripts.context.purge(true)
+else
+    logs.help(messages.help)
+end
+
+if environment.argument("profile") then
+    os.setenv("MTX_PROFILE_RUN","NO")
+end
diff --git a/scripts/context/lua/mtx-convert.lua b/scripts/context/lua/mtx-convert.lua
new file mode 100644
index 000000000..62198a621
--- /dev/null
+++ b/scripts/context/lua/mtx-convert.lua
@@ -0,0 +1,139 @@
+if not modules then modules = { } end modules ['mtx-convert'] = {
+    version   = 1.001,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- todo: eps and svg
+
+graphics            = graphics            or { }
+graphics.converters = graphics.converters or { }
+
+local gsprogram = (os.type == "windows" and "gswin32c") or "gs"
+local gstemplate = "%s -q -sDEVICE=pdfwrite -dEPSCrop -dNOPAUSE -dNOCACHE -dBATCH -dAutoRotatePages=/None -dProcessColorModel=/DeviceCMYK -sOutputFile=%s %s -c quit"
+
+function graphics.converters.eps(oldname,newname)
+    return gstemplate:format(gsprogram,newname,oldname)
+end
+
+local improgram  = "convert"
+local imtemplate = {
+    low    = "%s -quality   0 -compress zip %s pdf:%s",
+    medium = "%s -quality  75 -compress zip %s pdf:%s",
+    high   = "%s -quality 100 -compress zip %s pdf:%s",
+}
+
+function graphics.converters.jpg(oldname,newname)
+    local ea = environment.arguments
+    local quality = (ea.high and 'high') or (ea.medium and 'medium') or (ea.low and 'low') or 'high'
+    return imtemplate[quality]:format(improgram,oldname,newname)
+end
+
+graphics.converters.gif  = graphics.converters.jpg
+graphics.converters.tif  = graphics.converters.jpg
+graphics.converters.tiff = graphics.converters.jpg
+graphics.converters.png  = graphics.converters.jpg
+
+local function convert(kind,oldname,newname)
+    if graphics.converters[kind] then -- extra test
+        local tmpname = file.replacesuffix(newname,"tmp")
+        local command = graphics.converters[kind](oldname,tmpname)
+        logs.simple("command: %s",command)
+        io.flush()
+        os.spawn(command)
+        os.remove(newname)
+        os.rename(tmpname,newname)
+        if lfs.attributes(newname,"size") == 0 then
+            os.remove(newname)
+        end
+    end
+end
+
+function graphics.converters.convertpath(inputpath,outputpath)
+    inputpath  = inputpath  or "."
+    outputpath = outputpath or "."
+    for name in lfs.dir(inputpath) do
+        local suffix = file.extname(name)
+        if name:find("%.$") then
+            -- skip . and ..
+        elseif graphics.converters[suffix] then
+            local oldname = file.join(inputpath,name)
+            local newname = file.join(outputpath,file.replacesuffix(name,"pdf"))
+            local et = lfs.attributes(oldname,"modification")
+            local pt = lfs.attributes(newname,"modification")
+            if not pt or et > pt then
+                dir.mkdirs(outputpath)
+                convert(suffix,oldname,newname)
+            end
+        elseif lfs.isdir(inputpath .. "/".. name) then
+            graphics.converters.convertpath(inputpath .. "/".. name,outputpath .. "/".. name)
+        end
+    end
+end
+
+function graphics.converters.convertfile(oldname)
+    local suffix = file.extname(oldname)
+    if graphics.converters[suffix] then
+        local newname = file.replacesuffix(name,"pdf")
+        if oldname == newname then
+            -- todo: downsample, crop etc
+        elseif environment.argument("force") then
+            convert(suffix,oldname,newname)
+        else
+            local et = lfs.attributes(oldname,"modification")
+            local pt = lfs.attributes(newname,"modification")
+            if not pt or et > pt then
+                convert(suffix,oldname,newname)
+            end
+        end
+    end
+end
+
+scripts         = scripts         or { }
+scripts.convert = scripts.convert or { }
+
+scripts.convert.delay = 5 * 60 -- 5 minutes
+
+function scripts.convert.convertall()
+    local watch  = environment.arguments.watch      or false
+    local delay  = environment.arguments.delay      or scripts.convert.delay
+    local input  = environment.arguments.inputpath  or "."
+    local output = environment.arguments.outputpath or "."
+    while true do
+        graphics.converters.convertpath(input, output)
+        if watch then
+            os.sleep(delay)
+        else
+            break
+        end
+    end
+end
+
+function scripts.convert.convertgiven()
+    local files = environment.files
+    for i=1,#files do
+        graphics.converters.convertfile(files[i])
+    end
+end
+
+
+logs.extendbanner("ConTeXT Graphic Conversion Helpers 0.10",true)
+
+messages.help = [[
+--convertall          convert all graphics on path
+--inputpath=string    original graphics path
+--outputpath=string   converted graphics path
+--watch               watch folders
+--force               force conversion (even if older)
+--delay               time between sweeps
+]]
+
+if environment.argument("convertall") then
+    scripts.convert.convertall()
+elseif environment.files[1] then
+    scripts.convert.convertgiven()
+else
+    logs.help(messages.help)
+end
diff --git a/scripts/context/lua/mtx-fonts.lua b/scripts/context/lua/mtx-fonts.lua
new file mode 100644
index 000000000..74012ae38
--- /dev/null
+++ b/scripts/context/lua/mtx-fonts.lua
@@ -0,0 +1,345 @@
+if not modules then modules = { } end modules ['mtx-fonts'] = {
+    version   = 1.001,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+if not fontloader then fontloader = fontforge end
+
+dofile(resolvers.find_file("font-otp.lua","tex"))
+dofile(resolvers.find_file("font-syn.lua","tex"))
+dofile(resolvers.find_file("font-mis.lua","tex"))
+
+scripts       = scripts       or { }
+scripts.fonts = scripts.fonts or { }
+
+function fonts.names.simple()
+    local simpleversion = 1.001
+    local simplelist = { "ttf", "otf", "ttc", "dfont" }
+    local name = "luatex-fonts-names.lua"
+    fonts.names.filters.list = simplelist
+    fonts.names.version = simpleversion -- this number is the same as in font-dum.lua
+    logs.report("fontnames","generating font database for 'luatex-fonts' version %s",fonts.names.version)
+    fonts.names.identify(true)
+    local data = fonts.names.data
+    if data then
+        local simplemappings = { }
+        local simplified = {
+            mappings = simplemappings,
+            version = simpleversion,
+        }
+        local specifications = data.specifications
+        for i=1,#simplelist do
+            local format = simplelist[i]
+            for tag, index in next, data.mappings[format] do
+                local s = specifications[index]
+                simplemappings[tag] = { s.rawname, s.filename, s.subfont }
+            end
+        end
+        logs.report("fontnames","saving names in '%s'",name)
+        io.savedata(name,table.serialize(simplified,true))
+        local data = io.loaddata(resolvers.find_file("font-dum.lua","tex"))
+        local dummy = string.match(data,"fonts%.names%.version%s*=%s*([%d%.]+)")
+        if tonumber(dummy) ~= simpleversion then
+            logs.report("fontnames","warning: version number %s in 'font-dum' does not match database version number %s",dummy or "?",simpleversion)
+        end
+    elseif lfs.isfile(name) then
+        os.remove(name)
+    end
+end
+
+function scripts.fonts.reload()
+    if environment.argument("simple") then
+        fonts.names.simple()
+    else
+        fonts.names.load(true)
+    end
+end
+
+local function subfont(sf)
+    if sf then
+        return string.format("index: % 2s", sf)
+    else
+        return ""
+    end
+end
+
+local function fontweight(fw)
+    if fw then
+        return string.format("conflict: %s", fw)
+    else
+        return ""
+    end
+end
+
+local function showfeatures(tag,specification)
+    logs.simple("mapping : %s",tag)
+    logs.simple("fontname: %s",specification.fontname)
+    logs.simple("fullname: %s",specification.fullname)
+    logs.simple("filename: %s",specification.filename)
+    logs.simple("family  : %s",specification.familyname or "<nofamily>")
+    logs.simple("weight  : %s",specification.weight or "<noweight>")
+    logs.simple("style   : %s",specification.style or "<nostyle>")
+    logs.simple("width   : %s",specification.width or "<nowidth>")
+    logs.simple("variant : %s",specification.variant or "<novariant>")
+    logs.simple("subfont : %s",subfont(specification.subfont))
+    logs.simple("fweight : %s",fontweight(specification.fontweight))
+    -- maybe more
+    local features = fonts.get_features(specification.filename,specification.format)
+    if features then
+        for what, v in table.sortedhash(features) do
+            local data = features[what]
+            if data and next(data) then
+                logs.simple()
+                logs.simple("%s features:",what)
+                logs.simple()
+                logs.simple("feature  script   languages")
+                logs.simple()
+                for f,ff in table.sortedhash(data) do
+                    local done = false
+                    for s, ss in table.sortedhash(ff) do
+                        if s == "*"  then s       = "all" end
+                        if ss  ["*"] then ss["*"] = nil ss.all = true end
+                        if done then
+                            f = ""
+                        else
+                            done = true
+                        end
+                        logs.simple("% -8s % -8s % -8s",f,s,table.concat(table.sortedkeys(ss), " "))
+                    end
+                end
+            end
+        end
+    else
+        logs.simple()
+        logs.simple("no features")
+        logs.simple()
+    end
+    logs.reportline()
+end
+
+local function reloadbase(reload)
+    if reload then
+        logs.simple("fontnames, reloading font database")
+        names.load(true)
+        logs.simple("fontnames, done\n\n")
+    end
+end
+
+local function list_specifications(t,info)
+    if t then
+        local s = table.sortedkeys(t)
+        if info then
+            for k=1,#s do
+                local v = s[k]
+                showfeatures(v,t[v])
+            end
+        else
+            for k=1,#s do
+                local v = s[k]
+                local entry = t[v]
+                s[k] = {
+                    entry.familyname  or "<nofamily>",
+                    entry.weight      or "<noweight>",
+                    entry.style       or "<nostyle>",
+                    entry.width       or "<nowidth>",
+                    entry.variant     or "<novariant>",
+                    entry.fontname,
+                    entry.filename,
+                    subfont(entry.subfont),
+                    fontweight(entry.fontweight),
+                }
+                e[k] = entry
+            end
+            table.formatcolumns(s)
+            for k=1,#s do
+                local v = s[k]
+                texio.write_nl(v)
+            end
+        end
+    end
+end
+
+local function list_matches(t,info)
+    if t then
+        local s, w = table.sortedkeys(t), { 0, 0, 0 }
+        if info then
+            for k=1,#s do
+                local v = s[k]
+                showfeatures(v,t[v])
+            end
+        else
+            for k=1,#s do
+                local v = s[k]
+                local entry = t[v]
+                s[k] = {
+                    v,
+                    entry.fontname,
+                    entry.filename,
+                    subfont(entry.subfont)
+                }
+            end
+            table.formatcolumns(s)
+            for k=1,#s do
+                texio.write_nl(s[k])
+            end
+        end
+    end
+end
+
+function scripts.fonts.list()
+
+    local all     = environment.argument("all")
+    local info    = environment.argument("info")
+    local reload  = environment.argument("reload")
+    local pattern = environment.argument("pattern")
+    local filter  = environment.argument("filter")
+    local given   = environment.files[1]
+
+    reloadbase(reload)
+
+    if environment.argument("name") then
+        if pattern then
+            --~ mtxrun --script font --list --name --pattern=*somename*
+            list_matches(fonts.names.list(string.topattern(pattern,true),reload,all),info)
+        elseif filter then
+            logs.report("fontnames","not supported: --list --name --filter",name)
+        elseif given then
+            --~ mtxrun --script font --list --name somename
+            list_matches(fonts.names.list(given,reload,all),info)
+        else
+            logs.report("fontnames","not supported: --list --name <no specification>",name)
+        end
+    elseif environment.argument("spec") then
+        if pattern then
+            --~ mtxrun --script font --list --spec --pattern=*somename*
+            logs.report("fontnames","not supported: --list --spec --pattern",name)
+        elseif filter then
+            --~ mtxrun --script font --list --spec --filter="fontname=somename"
+            list_specifications(fonts.names.getlookups(filter),info)
+        elseif given then
+            --~ mtxrun --script font --list --spec somename
+            list_specifications(fonts.names.collectspec(given,reload,all),info)
+        else
+            logs.report("fontnames","not supported: --list --spec <no specification>",name)
+        end
+    elseif environment.argument("file") then
+        if pattern then
+            --~ mtxrun --script font --list --file --pattern=*somename*
+            list_specifications(fonts.names.collectfiles(string.topattern(pattern,true),reload,all),info)
+        elseif filter then
+            logs.report("fontnames","not supported: --list --spec",name)
+        elseif given then
+            --~ mtxrun --script font --list --file somename
+            list_specifications(fonts.names.collectfiles(given,reload,all),info)
+        else
+            logs.report("fontnames","not supported: --list --file <no specification>",name)
+        end
+    elseif pattern then
+        --~ mtxrun --script font --list --pattern=*somename*
+       list_matches(fonts.names.list(string.topattern(pattern,true),reload,all),info)
+    elseif given then
+        --~ mtxrun --script font --list somename
+        list_matches(fonts.names.list(given,reload,all),info)
+    else
+        logs.report("fontnames","not supported: --list <no specification>",name)
+    end
+
+end
+
+function scripts.fonts.save()
+    local name = environment.files[1] or ""
+    local sub  = environment.files[2] or ""
+    local function save(savename,fontblob)
+        if fontblob then
+            savename = savename:lower() .. ".lua"
+            logs.simple("fontsave, saving data in %s",savename)
+            table.tofile(savename,fontloader.to_table(fontblob),"return")
+            fontloader.close(fontblob)
+        end
+    end
+    if name and name ~= "" then
+        local filename = resolvers.find_file(name) -- maybe also search for opentype
+        if filename and filename ~= "" then
+            local suffix = file.extname(filename)
+            if suffix == 'ttf' or suffix == 'otf' or suffix == 'ttc' or suffix == "dfont" then
+                local fontinfo = fontloader.info(filename)
+                if fontinfo then
+                    logs.simple("font: %s located as %s",name,filename)
+                    if fontinfo[1] then
+                        for k=1,#fontinfo do
+                            local v = fontinfo[k]
+                            save(v.fontname,fontloader.open(filename,v.fullname))
+                        end
+                    else
+                        save(fontinfo.fullname,fontloader.open(filename))
+                    end
+                else
+                    logs.simple("font: %s cannot be read",filename)
+                end
+            else
+                logs.simple("font: %s not saved",filename)
+            end
+        else
+            logs.simple("font: %s not found",name)
+        end
+    else
+        logs.simple("font: no name given")
+    end
+end
+
+logs.extendbanner("ConTeXt Font Database Management 0.21",true)
+
+messages.help = [[
+--save                save open type font in raw table
+
+--reload              generate new font database
+--reload --simple     generate 'luatex-fonts-names.lua' (not for context!)
+
+--list --name         list installed fonts, filter by name [--pattern]
+--list --spec         list installed fonts, filter by spec [--filter]
+--list --file         list installed fonts, filter by file [--pattern]
+
+--pattern=str         filter files using pattern
+--filter=list         key-value pairs
+--all                 show all found instances
+--info                give more details
+--track=list          enable trackers
+
+examples of searches:
+
+mtxrun --script font --list somename (== --pattern=*somename*)
+
+mtxrun --script font --list --name somename
+mtxrun --script font --list --name --pattern=*somename*
+
+mtxrun --script font --list --spec somename
+mtxrun --script font --list --spec somename-bold-italic
+mtxrun --script font --list --spec --pattern=*somename*
+mtxrun --script font --list --spec --filter="fontname=somename"
+mtxrun --script font --list --spec --filter="familyname=somename,weight=bold,style=italic,width=condensed"
+
+mtxrun --script font --list --file somename
+mtxrun --script font --list --file --pattern=*somename*
+]]
+
+local track = environment.argument("track")
+
+if track then trackers.enable(track) end
+
+if environment.argument("names") then
+    environment.setargument("reload",true)
+    environment.setargument("simple",true)
+end
+
+if environment.argument("list") then
+    scripts.fonts.list()
+elseif environment.argument("reload") then
+    scripts.fonts.reload()
+elseif environment.argument("save") then
+    scripts.fonts.save()
+else
+    logs.help(messages.help)
+end
diff --git a/scripts/context/lua/mtx-grep.lua b/scripts/context/lua/mtx-grep.lua
new file mode 100644
index 000000000..9604bc9f8
--- /dev/null
+++ b/scripts/context/lua/mtx-grep.lua
@@ -0,0 +1,114 @@
+if not modules then modules = { } end modules ['mtx-babel'] = {
+    version   = 1.001,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+scripts      = scripts      or { }
+scripts.grep = scripts.grep or { }
+
+logs.extendbanner("Simple Grepper 0.10",true)
+
+local find, format = string.find, string.format
+
+local cr       = lpeg.P("\r")
+local lf       = lpeg.P("\n")
+local crlf     = cr * lf
+local newline  = crlf + cr + lf
+local content  = lpeg.C((1-newline)^0) * newline
+
+local write_nl = texio.write_nl
+
+function scripts.grep.find(pattern, files, offset)
+    if pattern and pattern ~= "" then
+        statistics.starttiming(scripts.grep)
+        local nofmatches, noffiles, nofmatchedfiles = 0, 0, 0
+        local n, m, name, check = 0, 0, "", nil
+        local count, nocomment = environment.argument("count"), environment.argument("nocomment")
+        if nocomment then
+            if count then
+                check = function(line)
+                    n = n + 1
+                    if find(line,"^[%%#]") then
+                        -- skip
+                    elseif find(line,pattern) then
+                        m = m + 1
+                    end
+                end
+            else
+                check = function(line)
+                    n = n + 1
+                    if find(line,"^[%%#]") then
+                        -- skip
+                    elseif find(line,pattern) then
+                        m = m + 1
+                        write_nl(format("%s %6i: %s",name,n,line))
+                        io.flush()
+                    end
+                end
+            end
+        else
+            if count then
+                check = function(line)
+                    n = n + 1
+                    if find(line,pattern) then
+                        m = m + 1
+                    end
+                end
+            else
+                check = function(line)
+                    n = n + 1
+                    if find(line,pattern) then
+                        m = m + 1
+                        write_nl(format("%s %6i: %s",name,n,line))
+                        io.flush()
+                    end
+                end
+            end
+        end
+        local capture = (content/check)^0
+        for i=offset or 1, #files do
+            local globbed = dir.glob(files[i])
+            for i=1,#globbed do
+                local nam = globbed[i]
+                name = nam
+                local data = io.loaddata(name)
+                if data then
+                    n, m, noffiles = 0, 0, noffiles + 1
+                    capture:match(data)
+                    if count and m > 0 then
+                        nofmatches = nofmatches + m
+                        nofmatchedfiles = nofmatchedfiles + 1
+                        write_nl(format("%s: %s",name,m))
+                        io.flush()
+                    end
+                end
+            end
+        end
+        statistics.stoptiming(scripts.grep)
+        if count and nofmatches > 0 then
+            write_nl(format("\nfiles: %s, matches: %s, matched files: %s, runtime: %0.3f seconds",noffiles,nofmatches,nofmatchedfiles,statistics.elapsedtime(scripts.grep)))
+        end
+    end
+end
+
+messages.help = [[
+--pattern             search for pattern (optional)
+--count               count matches only
+--nocomment           skip lines that start with %% or #
+
+patterns are lua patterns and need to be escaped accordingly
+]]
+
+local pattern = environment.argument("pattern")
+local files   = environment.files and #environment.files > 0 and environment.files
+
+if pattern and files then
+    scripts.grep.find(pattern, files)
+elseif files then
+    scripts.grep.find(files[1], files, 2)
+else
+    logs.help(messages.help)
+end
diff --git a/scripts/context/lua/mtx-interface.lua b/scripts/context/lua/mtx-interface.lua
new file mode 100644
index 000000000..730a030d9
--- /dev/null
+++ b/scripts/context/lua/mtx-interface.lua
@@ -0,0 +1,274 @@
+if not modules then modules = { } end modules ['mtx-cache'] = {
+    version   = 1.001,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format = string.format
+
+scripts           = scripts           or { }
+scripts.interface = scripts.interface or { }
+
+local flushers          = { }
+local userinterfaces    = { 'en','cs','de','it','nl','ro','fr','pe' }
+local messageinterfaces = { 'en','cs','de','it','nl','ro','fr','pe','no' }
+
+function flushers.scite(interface,collection)
+    local result, i = {}, 0
+    result[#result+1] = format("keywordclass.macros.context.%s=",interface)
+    for i=1,#collection do
+        local command = collection[i]
+        if i==0 then
+            result[#result+1] = "\\\n"
+            i = 5
+        else
+            i = i - 1
+        end
+        result[#result+1] = format("%s ",command)
+    end
+    io.savedata(format("cont-%s-scite.properties",interface), table.concat(result),"\n")
+    io.savedata(format("cont-%s-scite.lua",interface), table.serialize(collection,true))
+end
+
+function flushers.jedit(interface,collection)
+    local result = {}
+    result[#result+1] = "<?xml version='1.0'?>"
+    result[#result+1] = "<!DOCTYPE MODE SYSTEM 'xmode.dtd'>\n"
+    result[#result+1] = "<MODE>"
+    result[#result+1] = "\t<RULES>"
+    result[#result+1] = "\t\t<KEYWORDS>"
+    for i=1,#collection do
+        local command = collection[i]
+        result[#result+1] = format("\t\t\t<KEYWORD2>%s</KEYWORD2>",command)
+    end
+    result[#result+1] = "\t\t</KEYWORDS>"
+    result[#result+1] = "\t</RULES>"
+    result[#result+1] = "</MODE>"
+    io.savedata(format("context-jedit-%s.xml",interface), table.concat(result),"\n")
+end
+
+function flushers.bbedit(interface,collection)
+    local result = {}
+    result[#result+1] = "<?xml version='1.0'?>"
+    result[#result+1] = "<key>BBLMKeywordList</key>"
+    result[#result+1] = "<array>"
+    for i=1,#collection do
+        local command = collection[i]
+        result[#result+1]  = format("\t<string>\\%s</string>",command)
+    end
+    result[#result+1] = "</array>"
+    io.savedata(format("context-bbedit-%s.xml",interface), table.concat(result),"\n")
+end
+
+function flushers.raw(interface,collection)
+    for i=1,#collection do
+        local command = collection[i]
+        logs.simple(command)
+    end
+end
+
+function scripts.interface.editor(editor)
+    local interfaces= environment.files
+    if #interfaces == 0 then
+        interfaces= userinterfaces
+    end
+    local xmlfile = resolvers.find_file("cont-en.xml") or ""
+    if xmlfile == "" then
+        logs.simple("unable to locate cont-en.xml")
+    end
+    for i=1,#interfaces do
+        local interface = interfaces[i]
+        local keyfile = resolvers.find_file(format("keys-%s.xml",interface)) or ""
+        if keyfile == "" then
+            logs.simple("unable to locate keys-*.xml")
+        else
+            local collection = { }
+            local mappings   = { }
+            local x = xml.load(keyfile)
+            for e, d, k in xml.elements(x,"cd:command") do
+                local at = d[k].at
+                local name, value = at.name, at.value
+                if name and value then
+                    mappings[name] = value
+                end
+            end
+            local x = xml.load(xmlfile)
+            for e, d, k in xml.elements(x,"cd:command") do
+                local at = d[k].at
+                local name, type = at.name, at["type"]
+                if name and name ~= "" then
+                    local remapped = mappings[name] or name
+                    if type == "environment" then
+                        collection[#collection+1] = "start" .. remapped
+                        collection[#collection+1] = "stop" .. remapped
+                    else
+                        collection[#collection+1] = remapped
+                    end
+                end
+            end
+            if #collection > 0 then
+                table.sort(collection)
+                flushers[editor](interface,collection)
+            end
+        end
+    end
+end
+
+function scripts.interface.check()
+    local xmlfile = resolvers.find_file("cont-en.xml") or ""
+    if xmlfile ~= "" then
+        local f = io.open("cont-en-check.tex","w")
+        if f then
+            f:write("\\starttext\n")
+            local x = xml.load(xmlfile)
+            for e, d, k in xml.elements(x,"cd:command") do
+                local dk = d[k]
+                local at = dk.at
+                if at then
+                    local name = xml.filter(dk,"cd:sequence/cd:string/attribute(value)")
+                    if name and name ~= "" then
+                        if at.type == "environment" then
+                            name = "start" .. name
+                        end
+                        f:write(format("\\doifundefined{%s}{\\writestatus{check}{command '%s' is undefined}}\n",name,name))
+                    end
+                end
+            end
+            f:write("\\stoptext\n")
+            f:close()
+        end
+    end
+end
+
+function scripts.interface.context()
+    local filename = resolvers.find_file(environment.files[1] or "mult-def.lua") or ""
+    if filename ~= "" then
+        local interface = dofile(filename)
+        if interface and next(interface) then
+            local variables, constants, commands, elements = interface.variables, interface.constants, interface.commands, interface.elements
+            local filename = resolvers.find_file("cont-en.xml") or ""
+            local xmldata = filename ~= "" and (io.loaddata(filename) or "")
+            local function flush(texresult,xmlresult,language,what,tag)
+                local t = interface[what]
+                texresult[#texresult+1] = format("%% definitions for interface %s for language %s\n%%",what,language)
+                xmlresult[#xmlresult+1] = format("\t<!-- definitions for interface %s for language %s -->\n",what,language)
+                xmlresult[#xmlresult+1] = format("\t<cd:%s>",what)
+                local sorted = table.sortedkeys(t)
+                for i=1,#sorted do
+                    local key = sorted[i]
+                    local v = t[key]
+                    local value = v[language] or v["en"]
+                    if not value then
+                        logs.simple(format("warning, no value for key '%s' for language '%s'",key,language))
+                    else
+                        local value = t[key][language] or t[key].en
+                        texresult[#texresult+1] = format("\\setinterface%s{%s}{%s}",tag,key,value)
+                        xmlresult[#xmlresult+1] = format("\t\t<cd:%s name='%s' value='%s'/>",tag,key,value)
+                    end
+                end
+                xmlresult[#xmlresult+1] = format("\t</cd:%s>\n",tag)
+            end
+            local function replace(str, element, attribute, category, othercategory, language)
+                return str:gsub(format("(<%s[^>]-%s=)([\"\'])([^\"\']-)([\"\'])",element,attribute), function(a,b,c)
+                    local cc = category[c]
+                    if not cc and othercategory then
+                        cc = othercategory[c]
+                    end
+                    if cc then
+                        ccl = cc[language]
+                        if ccl then
+                            return a .. b .. ccl .. b
+                        end
+                    end
+                    return a .. b .. c .. b
+                end)
+            end
+            for language, _ in next, commands.setuplayout do
+                local texresult, xmlresult = { }, { }
+                texresult[#texresult+1] = format("%% this file is auto-generated, don't edit this file\n%%")
+                xmlresult[#xmlresult+1] = format("<?xml version='1.0'?>\n",tag)
+                xmlresult[#xmlresult+1] = format("<cd:interface xmlns:cd='http://www.pragma-ade.com/commands' name='context' language='%s' version='2008.10.21 19:42'>\n",language)
+                flush(texresult,xmlresult,language,"variables","variable")
+                flush(texresult,xmlresult,language,"constants","constant")
+                flush(texresult,xmlresult,language,"elements", "element")
+                flush(texresult,xmlresult,language,"commands", "command")
+                texresult[#texresult+1] = format("%%\n\\endinput")
+                xmlresult[#xmlresult+1] = format("</cd:interface>")
+                local texfilename = format("mult-%s.tex",language)
+                local xmlfilename = format("keys-%s.xml",language)
+                io.savedata(texfilename,table.concat(texresult,"\n"))
+                logs.simple(format("saving interface definitions '%s'",texfilename))
+                io.savedata(xmlfilename,table.concat(xmlresult,"\n"))
+                logs.simple(format("saving interface translations '%s'",xmlfilename))
+                if language ~= "en" and xmldata ~= "" then
+                    local newdata = xmldata:gsub("(<cd:interface.*language=.)en(.)","%1"..language.."%2",1)
+                    newdata = replace(newdata, 'cd:string', 'value', interface.commands, interface.elements, language)
+                    newdata = replace(newdata, 'cd:variable' , 'value', interface.variables, nil, language)
+                    newdata = replace(newdata, 'cd:parameter', 'name', interface.constants, nil, language)
+                    newdata = replace(newdata, 'cd:constant', 'type', interface.variables, nil, language)
+                    newdata = replace(newdata, 'cd:variable', 'type', interface.variables, nil, language)
+                    newdata = replace(newdata, 'cd:inherit', 'name', interface.commands, interface.elements, language)
+                    local xmlfilename = format("cont-%s.xml",language)
+                    io.savedata(xmlfilename,newdata)
+                    logs.simple(format("saving interface specification '%s'",xmlfilename))
+                end
+            end
+        end
+    end
+end
+
+function scripts.interface.messages()
+    local filename = resolvers.find_file(environment.files[1] or "mult-mes.lua") or ""
+    if filename ~= "" then
+        local messages = dofile(filename)
+        for i=1,#messageinterfaces do
+            local interface = messageinterfaces[i]
+            local texresult = { }
+            for category, data in next, messages do
+                for tag, message in next, data do
+                    if tag ~= "files" then
+                        local msg = message[interface] or message["all"] or message["en"]
+                        if msg then
+                            texresult[#texresult+1] = format("\\setinterfacemessage{%s}{%s}{%s}",category,tag,msg)
+                        end
+                    end
+                end
+            end
+            texresult[#texresult+1] = format("%%\n\\endinput")
+            io.savedata(format("mult-m%s.tex",interface),table.concat(texresult,"\n"))
+        end
+    end
+end
+
+logs.extendbanner("ConTeXt Interface Related Goodies 0.11",true)
+
+messages.help = [[
+--scite               generate scite interface
+--bbedit              generate scite interface
+--jedit               generate scite interface
+--check               generate check file
+--context             generate context definition files
+--messages            generate context message files
+]]
+
+if environment.argument("context") then
+    scripts.interface.context()
+elseif environment.argument("messages") then
+    scripts.interface.messages()
+elseif environment.argument("scite") or environment.argument("bbedit") or environment.argument("jedit") then
+    if environment.argument("scite") then
+        scripts.interface.editor("scite")
+    end
+    if environment.argument("bbedit") then
+        scripts.interface.editor("bbedit")
+    end
+    if environment.argument("jedit") then
+        scripts.interface.editor("jedit")
+    end
+elseif environment.argument("check") then
+    scripts.interface.check()
+else
+    logs.help(messages.help)
+end
diff --git a/scripts/context/lua/mtx-metatex.lua b/scripts/context/lua/mtx-metatex.lua
new file mode 100644
index 000000000..4453e2ccb
--- /dev/null
+++ b/scripts/context/lua/mtx-metatex.lua
@@ -0,0 +1,69 @@
+if not modules then modules = { } end modules ['mtx-metatex'] = {
+    version   = 1.001,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- future versions will deal with specific variants of metatex
+
+scripts         = scripts         or { }
+scripts.metatex = scripts.metatex or { }
+
+-- metatex
+
+function scripts.metatex.make()
+    local command = "luatools --make --compile metatex"
+    logs.simple("running command: %s",command)
+    os.spawn(command)
+end
+
+--~ function scripts.metatex.run()
+--~     local name = environment.files[1] or ""
+--~     if name ~= "" then
+--~         local command = "luatools --fmt=metatex " .. name
+--~         logs.simple("running command: %s",command)
+--~         os.spawn(command)
+--~     end
+--~ end
+
+function scripts.metatex.run(ctxdata,filename)
+    local filename = environment.files[1] or ""
+    if filename ~= "" then
+        local formatfile, scriptfile = resolvers.locate_format("metatex")
+        if formatfile and scriptfile then
+            local command = string.format("luatex --fmt=%s --lua=%s  %s",
+                string.quote(formatfile), string.quote(scriptfile), string.quote(filename))
+            logs.simple("running command: %s",command)
+            os.spawn(command)
+        elseif formatname then
+            logs.simple("error, no format found with name: %s",formatname)
+        else
+            logs.simple("error, no format found (provide formatname or interface)")
+        end
+    end
+end
+
+function scripts.metatex.timed(action)
+    statistics.timed(action)
+end
+
+logs.extendbanner("MetaTeX Process Management 0.10",true)
+
+messages.help = [[
+--run                 process (one or more) files (default action)
+--make                create metatex format(s)
+]]
+
+if environment.argument("run") then
+    scripts.metatex.timed(scripts.metatex.run)
+elseif environment.argument("make") then
+    scripts.metatex.timed(scripts.metatex.make)
+elseif environment.argument("help") then
+    logs.help(messages.help,false)
+elseif environment.files[1] then
+    scripts.metatex.timed(scripts.metatex.run)
+else
+    logs.help(messages.help,false)
+end
diff --git a/scripts/context/lua/mtx-modules.lua b/scripts/context/lua/mtx-modules.lua
new file mode 100644
index 000000000..3a348593f
--- /dev/null
+++ b/scripts/context/lua/mtx-modules.lua
@@ -0,0 +1,167 @@
+if not modules then modules = { } end modules ['mtx-modules'] = {
+    version   = 1.002,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+scripts         = scripts         or { }
+scripts.modules = scripts.modules or { }
+
+-- Documentation can be woven into a source file. This script can generates
+-- a file with the documentation and source fragments properly tagged. The
+-- documentation is included as comment:
+--
+-- %D ......  some kind of documentation
+-- %M ......  macros needed for documenation
+-- %S B       begin skipping
+-- %S E       end skipping
+--
+-- The generated file is structured as:
+--
+-- \starttypen
+-- \startmodule[type=suffix]
+-- \startdocumentation
+-- \stopdocumentation
+-- \startdefinition
+-- \stopdefinition
+-- \stopmodule
+-- \stoptypen
+--
+-- Macro definitions specific to the documentation are not surrounded by
+-- start-stop commands. The suffix specificaction can be overruled at runtime,
+-- but defaults to the file extension. This specification can be used for language
+-- depended verbatim typesetting.
+
+local find, format, sub, is_empty, strip = string.find, string.format, string.sub, string.is_empty, string.strip
+
+local function source_to_ted(inpname,outname,filetype)
+    local inp = io.open(inpname)
+    if not inp then
+        logs.simple("unable to open '%s'",inpname)
+        return
+    end
+    local out = io.open(outname,"w")
+    if not out then
+        logs.simple("unable to open '%s'",outname)
+        return
+    end
+    logs.simple("converting '%s' to '%s'",inpname,outname)
+    local skiplevel, indocument, indefinition = 0, false, false
+    out:write(format("\\startmodule[type=%s]\n",filetype or file.suffix(inpname)))
+    for line in inp:lines() do
+--~         line = strip(line)
+        if find(line,"^%%D ") or find(line,"^%%D$") then
+            if skiplevel == 0 then
+                local someline = (#line < 3 and "") or sub(line,4,#line)
+                if indocument then
+                    out:write(format("%s\n",someline))
+                else
+                    if indefinition then
+                        out:write("\\stopdefinition\n")
+                        indefinition = false
+                    end
+                    if not indocument then
+                        out:write("\n\\startdocumentation\n")
+                    end
+                    out:write(format("%s\n",someline))
+                    indocument = true
+                end
+            end
+        elseif find(line,"^%%M ") or find(line,"^%%M$") then
+            if skiplevel == 0 then
+                local someline = (#line < 3 and "") or sub(line,4,#line)
+                out:write(format("%s\n",someline))
+            end
+        elseif find(line,"^%%S B") then
+            skiplevel = skiplevel + 1
+        elseif find(line,"^%%S E") then
+            skiplevel = skiplevel - 1
+        elseif find(line,"^%%") then
+            -- nothing
+        elseif skiplevel == 0 then
+            inlocaldocument = indocument
+            inlocaldocument = false
+            local someline = line
+            if indocument then
+                out:write("\\stopdocumentation\n")
+                indocument = false
+            end
+            if indefinition then
+                if is_empty(someline) then
+                    out:write("\\stopdefinition\n")
+                    indefinition = false
+                else
+                    out:write(format("%s\n",someline))
+                end
+            elseif not is_empty(someline) then
+                out:write("\n\\startdefinition\n")
+                indefinition = true
+                if inlocaldocument then
+                    -- nothing
+                else
+                    out:write(format("%s\n",someline))
+                end
+            end
+        end
+    end
+    if indocument then
+        out:write("\\stopdocumentation\n")
+    end
+    if indefinition then
+        out:write("\\stopdefinition\n")
+    end
+    out:write("\\stopmodule\n")
+    out:close()
+    inp:close()
+    return true
+end
+
+local suffixes = table.tohash { 'tex','mkii','mkiv','mp' }
+
+function scripts.modules.process(runtex)
+    local processed = { }
+    local prep = environment.argument("prep")
+    local files = environment.files
+    for i=1,#files do
+        local shortname = files[i]
+        local suffix = file.suffix(shortname)
+        if suffixes[suffix] then
+            local longname
+            if prep then
+                longname = shortname .. ".prep"
+            else
+                longname = file.removesuffix(shortname) .. "-" .. suffix .. ".ted"
+            end
+            local done = source_to_ted(shortname,longname)
+            if done and runtex then
+                os.execute(format("mtxrun --script context --usemodule=mod-01 %s",longname))
+                processed[#processed+1] = longname
+            end
+        end
+    end
+    for i=1,#processed do
+        local name = processed[i]
+        logs.simple("modules","processed: %s",name)
+    end
+end
+
+--  context --ctx=m-modules.ctx xxx.mkiv
+
+
+logs.extendbanner("ConTeXt Module Documentation Generators 1.00",true)
+
+messages.help = [[
+--convert             convert source files (tex, mkii, mkiv, mp) to 'ted' files
+--process             process source files (tex, mkii, mkiv, mp) to 'pdf' files
+--prep                use original name with suffix 'prep' appended
+]]
+
+if environment.argument("process") then
+    scripts.modules.process(true)
+elseif environment.argument("convert") then
+    scripts.modules.process(false)
+else
+    logs.help(messages.help)
+end
diff --git a/scripts/context/lua/mtx-mptopdf.lua b/scripts/context/lua/mtx-mptopdf.lua
new file mode 100644
index 000000000..342ff1c28
--- /dev/null
+++ b/scripts/context/lua/mtx-mptopdf.lua
@@ -0,0 +1,127 @@
+if not modules then modules = { } end modules ['mtx-mptopdf'] = {
+    version   = 1.303,
+    comment   = "companion to mtxrun.lua, patched by HH so errors are his",
+    author    = "Taco Hoekwater, Elvenkind BV, Dordrecht NL",
+    copyright = "Elvenkind BV / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+scripts             = scripts             or { }
+scripts.mptopdf     = scripts.mptopdf     or { }
+scripts.mptopdf.aux = scripts.mptopdf.aux or { }
+
+local dosish      = os.type == 'windows'
+local miktex      = dosish and environment.TEXSYSTEM and environment.TEXSYSTEM:find("miktex")
+local escapeshell = environment.SHELL and environment.SHELL:find("sh") and true
+
+function scripts.mptopdf.aux.find_latex(fname)
+    local d = io.loaddata(fname) or ""
+    return d:find("\\documentstyle") or d:find("\\documentclass") or d:find("\\begin{document}")
+end
+
+function scripts.mptopdf.aux.do_convert (fname)
+    local command, done, pdfdest = "", 0, ""
+    if fname:find(".%d+$") or fname:find("%.mps$") then
+        if miktex then
+            command = "pdftex -undump=mptopdf"
+        else
+            command = "pdftex -fmt=mptopdf -progname=context"
+        end
+        if dosish then
+            command = string.format('%s \\relax "%s"',command,fname)
+        else
+            command = string.format('%s \\\\relax "%s"',command,fname)
+        end
+        local result = os.execute(command)
+        if result == 0 then
+            local name, suffix = file.nameonly(fname), file.extname(fname)
+            local pdfsrc =  name .. ".pdf"
+            if lfs.isfile(pdfsrc) then
+                pdfdest = name .. "-" .. suffix .. ".pdf"
+                os.rename(pdfsrc, pdfdest)
+                if lfs.isfile(pdfsrc) then -- rename failed
+                    file.copy(pdfsrc, pdfdest)
+                end
+                done = 1
+            end
+        end
+    end
+    return done, pdfdest
+end
+
+function scripts.mptopdf.aux.make_mps(fn,latex,rawmp,metafun)
+    local rest, mpbin = latex and " --tex=latex " or " ", ""
+    if rawmp then
+        if metafun then
+            mpbin = "mpost --progname=mpost --mem=metafun"
+        else
+            mpbin = "mpost --mem=mpost"
+        end
+    else
+        if latex then
+            mpbin = "mpost --mem=mpost"
+        else
+            mpbin = "texexec --mptex"
+        end
+    end
+    local runner = mpbin .. rest .. fn
+    logs.simple("running: %s\n", runner)
+    return (os.execute(runner))
+end
+
+function scripts.mptopdf.convertall()
+    local rawmp   = environment.arguments.rawmp   or false
+    local metafun = environment.arguments.metafun or false
+    local latex   = environment.arguments.latex   or false
+    local files   = dir.glob(environment.files)
+    if #files > 0 then
+        local fn = files[1]
+        if #files == 1 and fn:find("%.mp$") then
+            latex = scripts.mptopdf.aux.find_latex(fn) or latex
+        end
+        if scripts.mptopdf.aux.make_mps(fn,latex,rawmp,metafun) then
+            files = dir.glob(file.nameonly(fn) .. ".*") -- reset
+        else
+            logs.simple("error while processing mp file '%s'", fn)
+            exit(1)
+        end
+        local report = { }
+        for i=1,#files do
+            local fn = files[i]
+            local success, name = scripts.mptopdf.aux.do_convert(fn)
+            if success > 0 then
+                report[#report+1] = { fn, name }
+            end
+        end
+        if #report > 0 then
+            logs.simple("number of converted files: %i", #report)
+            logs.simple("")
+            for i=1,#report do
+                local r = report[i]
+                logs.simple("%s => %s", r[1], r[2])
+            end
+        else
+            logs.simple("no files are converted")
+        end
+    else
+        logs.simple("no files match %s", table.concat(environment.files,' '))
+    end
+end
+
+logs.extendbanner("MetaPost to PDF Converter 0.51",true)
+
+messages.help = [[
+--rawmp               raw metapost run
+--metafun             use metafun instead of plain
+--latex               force --tex=latex
+]]
+
+if environment.files[1] then
+    scripts.mptopdf.convertall()
+else
+    if not environment.arguments.help then
+        logs.simple("provide MP output file (or pattern)")
+        logs.simple("")
+    end
+    logs.help(messages.help)
+end
diff --git a/scripts/context/lua/mtx-mtxworks.lua b/scripts/context/lua/mtx-mtxworks.lua
new file mode 100644
index 000000000..1239ae4c5
--- /dev/null
+++ b/scripts/context/lua/mtx-mtxworks.lua
@@ -0,0 +1,14 @@
+if not modules then modules = { } end modules ['mtx-mtxworks'] = {
+    version   = 1.002,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- this is a shortcut to "mtxrun --script texworks --start"
+
+environment.setargument("start",true)
+
+require "mtx-texworks"
+
diff --git a/scripts/context/lua/mtx-package.lua b/scripts/context/lua/mtx-package.lua
new file mode 100644
index 000000000..b36fc0ed8
--- /dev/null
+++ b/scripts/context/lua/mtx-package.lua
@@ -0,0 +1,68 @@
+if not modules then modules = { } end modules ['mtx-package'] = {
+    version   = 1.002,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, gsub, gmatch = string.format, string.gsub, string.gmatch
+
+scripts         = scripts         or { }
+messages        = messages        or { }
+scripts.package = scripts.package or { }
+
+function scripts.package.merge_luatex_files(name,strip)
+    local oldname = resolvers.find_file(name) or ""
+    oldname = file.replacesuffix(oldname,"lua")
+    if oldname == "" then
+        logs.simple("missing '%s'",name)
+    else
+        local newname = file.removesuffix(oldname) .. "-merged.lua"
+        local data = io.loaddata(oldname) or ""
+        if data == "" then
+            logs.simple("missing '%s'",newname)
+        else
+            logs.simple("loading '%s'",oldname)
+            local collected = { }
+            collected[#collected+1] = format("-- merged file : %s\n",newname)
+            collected[#collected+1] = format("-- parent file : %s\n",oldname)
+            collected[#collected+1] = format("-- merge date  : %s\n",os.date())
+            -- loadmodule can have extra arguments
+            for lib in gmatch(data,"loadmodule *%([\'\"](.-)[\'\"]") do
+                if file.basename(lib) ~= file.basename(newname) then
+                    local fullname = resolvers.find_file(lib) or ""
+                    if fullname == "" then
+                        logs.simple("missing '%s'",lib)
+                    else
+                        logs.simple("fetching '%s'",fullname)
+                        local data = io.loaddata(fullname)
+                        if strip then
+                            data = gsub(data,"%-%-%[%[ldx%-%-.-%-%-%ldx%]%]%-%-[\n\r]*","")
+                            data = gsub(data,"%-%-%~[^\n\r]*[\n\r]*","\n")
+                            data = gsub(data,"%s+%-%-[^\n\r]*[\n\r]*","\n")
+                            data = gsub(data,"[\n\r]+","\n")
+                        end
+                        collected[#collected+1] = "\ndo -- begin closure to overcome local limits and interference\n\n"
+                        collected[#collected+1] = data
+                        collected[#collected+1] = "\nend -- closure\n"
+                    end
+                end
+            end
+            logs.simple("saving '%s'",newname)
+            io.savedata(newname,table.concat(collected))
+        end
+    end
+end
+
+logs.extendbanner("Distribution Related Goodies 0.10",true)
+
+messages.help = [[
+--merge               merge 'loadmodule' into merge file
+]]
+
+if environment.argument("merge") then
+    scripts.package.merge_luatex_files(environment.files[1] or "")
+else
+    logs.help(messages.help)
+end
diff --git a/scripts/context/lua/mtx-patterns.lua b/scripts/context/lua/mtx-patterns.lua
new file mode 100644
index 000000000..293016991
--- /dev/null
+++ b/scripts/context/lua/mtx-patterns.lua
@@ -0,0 +1,366 @@
+if not modules then modules = { } end modules ['mtx-patterns'] = {
+    version   = 1.001,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format = string.format
+
+scripts          = scripts          or { }
+scripts.patterns = scripts.patterns or { }
+
+scripts.patterns.list = {
+    { "??",  "hyph-ar.tex",            "arabic" },
+    { "bg",  "hyph-bg.tex",            "bulgarian" },
+    { "ca",  "hyph-ca.tex",            "catalan" },
+    { "??",  "hyph-cop.tex",           "coptic" },
+    { "cs",  "hyph-cs.tex",            "czech" },
+    { "cy",  "hyph-cy.tex",            "welsh" },
+    { "da",  "hyph-da.tex",            "danish" },
+    { "deo", "hyph-de-1901.tex",       "german, old spelling" },
+    { "de",  "hyph-de-1996.tex",       "german, new spelling" },
+--~ { "??",  "hyph-el-monoton.tex",    "" },
+--~ { "??",  "hyph-el-polyton.tex",    "" },
+    { "agr", "hyph-grc",               "ancient greek" },
+--~ { "???", "hyph-x-ibycus",          "ancient greek in ibycus encoding" },
+--~ { "gr",  "",                       "" },
+    { "eo",  "hyph-eo.tex",            "esperanto" },
+    { "gb",  "hyph-en-gb.tex",         "british english" },
+    { "us",  "hyph-en-us.tex",	       "american english" },
+    { "es",  "hyph-es.tex",            "spanish" },
+    { "et",  "hyph-et.tex",            "estonian" },
+    { "eu",  "hyph-eu.tex",            "basque" }, -- ba is Bashkir!
+    { "fa",  "hyph-fa.tex",            "farsi" },
+    { "fi",  "hyph-fi.tex",            "finnish" },
+    { "fr",  "hyph-fr.tex",            "french" },
+--  { "??",  "hyph-ga.tex",            "" },
+--  { "??",  "hyph-gl.tex",            "" },
+--  { "??",  "hyph-grc.tex",           "" },
+    { "hr",  "hyph-hr.tex",            "croatian" },
+    { "??",  "hyph-hsb.tex",           "upper sorbian" },
+    { "hu",  "hyph-hu.tex",            "hungarian" },
+    { "??",  "hyph-ia.tex",            "interlingua" },
+    { "??",  "hyph-id.tex",            "indonesian" },
+    { "is",  "hyph-is.tex",            "icelandic" },
+    { "it",  "hyph-it.tex",            "italian" },
+    { "la",  "hyph-la.tex",            "latin" },
+    { "lt",  "hyph-lt.tex",            "lithuanian" },
+    { "mn",  "hyph-mn-cyrl.tex",       "mongolian, cyrillic script" },
+    { "nb",  "hyph-nb.tex",            "norwegian bokmål" },
+    { "nl",  "hyph-nl.tex",            "dutch" },
+    { "nn",  "hyph-nn.tex",            "norwegian nynorsk" },
+    { "pl",  "hyph-pl.tex",            "polish" },
+    { "pt",  "hyph-pt.tex",            "portuguese" },
+    { "ro",  "hyph-ro.tex",            "romanian" },
+    { "ru",  "hyph-ru.tex",            "russian" },
+    { "sk",  "hyph-sk.tex",            "slovak" },
+    { "sl",  "hyph-sl.tex",            "slovenian" },
+    { "sr",  "hyph-sr-cyrl.tex",       "serbian" },
+    { "sv",  "hyph-sv.tex",            "swedish" },
+    { "tr",  "hyph-tr.tex",            "turkish" },
+    { "tk",  "hyph-tk.tex",            "turkman" },
+    { "uk",  "hyph-uk.tex",            "ukrainian" },
+    { "zh",  "hyph-zh-latn.tex",       "zh-latn, chinese Pinyin" },
+}
+
+
+-- stripped down from lpeg example:
+
+local utf = unicode.utf8
+
+local cont = lpeg.R("\128\191")   -- continuation byte
+
+local utf8 = lpeg.R("\0\127")
+           + lpeg.R("\194\223") * cont
+           + lpeg.R("\224\239") * cont * cont
+           + lpeg.R("\240\244") * cont * cont * cont
+
+local validutf = (utf8^0/function() return true end) * (lpeg.P(-1)/function() return false end)
+
+function utf.check(str)
+    return lpeg.match(validutf,str)
+end
+
+local permitted_commands = table.tohash {
+    "message",
+    "endinput"
+}
+
+local permitted_characters = table.tohash {
+    0x0009, -- tab
+    0x0027, -- apostrofe
+    0x002D, -- hyphen
+    0x200C, --
+}
+
+function scripts.patterns.load(path,name,mnemonic,fullcheck)
+    local fullname = file.join(path,name)
+    local data = io.loaddata(fullname) or ""
+    local byte, char = utf.byte, utf.char
+    if data ~= "" then
+        data = data:gsub("([\n\r])\\input ([^ \n\r]+)", function(previous,subname)
+            local subname = file.addsuffix(subname,"tex")
+            local subfull = file.join(file.dirname(fullname),subname)
+            local subdata = io.loaddata(subfull) or ""
+            if subdata == "" then
+                if mnemonic then
+                    logs.simple("no subfile %s for language %s",subname,mnemonic)
+                else
+                    logs.simple("no subfile %s",name)
+                end
+            end
+            return previous .. subdata
+        end)
+        local comment = data:match("^(.-)[\n\r]\\patterns") or ""
+        local n, okay = 0, true
+        local cd = characters.data
+        for line in data:gmatch("[^ \n\r]+") do
+            local ok = utf.check(line)
+            n = n + 1
+            if not ok then
+                okay = false
+                line = line:gsub("%%","%%%%")
+                if fullcheck then
+                    if mnemonic then
+                        logs.simple("invalid utf in language %s, file %s, line %s: %s",mnemonic,name,n,line)
+                    else
+                        logs.simple("invalid utf in file %s, line %s: %s",name,n,line)
+                    end
+                else
+                    if mnemonic then
+                        logs.simple("file %s for %s contains invalid utf",name,mnemonic)
+                    else
+                        logs.simple("file %s contains invalid utf",name)
+                    end
+                    break
+                end
+            end
+        end
+        local c, h = { }, { }
+        for line in data:gmatch("[^\n\r]+") do
+            local txt, cmt = line:match("^(.-)%%(.*)$")
+            if not txt then
+                txt, cmt = line, ""
+            end
+            for s in txt:gmatch("\\([a-zA-Z]+)") do
+                h[s] = (h[s] or 0) + 1
+            end
+            for s in cmt:gmatch("\\([a-zA-Z]+)") do
+                c[s] = (c[s] or 0) + 1
+            end
+        end
+        h.patterns = nil
+        h.hyphenation = nil
+        for k, v in next, h do
+            if not permitted_commands[k] then okay = false end
+            if mnemonic then
+                logs.simple("command \\%s found in language %s, file %s, n=%s",k,mnemonic,name,v)
+            else
+                logs.simple("command \\%s found in file %s, n=%s",k,name,v)
+            end
+        end
+        if not environment.argument("fast") then
+            for k, v in next, c do
+                if mnemonic then
+                    logs.simple("command \\%s found in comment of language %s, file %s, n=%s",k,mnemonic,name,v)
+                else
+                    logs.simple("command \\%s found in comment of file %s, n=%s",k,name,v)
+                end
+            end
+        end
+        data = data:gsub("%%.-[\n\r]","")
+        data = data:gsub(" *[\n\r]+","\n")
+        local patterns = data:match("\\patterns[%s]*{[%s]*(.-)[%s]*}") or ""
+        local hyphenations = data:match("\\hyphenation[%s]*{[%s]*(.-)[%s]*}") or ""
+        patterns = patterns:gsub(" +","\n")
+        hyphenations = hyphenations:gsub(" +","\n")
+        local p, h = { }, { }
+        local pats, hyps = { } , { }
+        local pused, hused = { } , { }
+        local period = byte(".")
+        for line in patterns:gmatch("[^ \n\r]+") do
+            local ok = true
+            for b in line:utfvalues() do
+                if b == period then
+                    -- ok
+                else
+                    local ct = cd[b].category
+                    if ct == "lu" or ct == "ll" then
+                        pused[char(b)] = true
+                    elseif ct == "nd" then
+                        -- ok
+                    else
+                        p[b] = (p[b] or 0) + 1
+                        ok = false
+                    end
+                end
+            end
+            if ok then
+                pats[#pats+1] = line
+            end
+        end
+        local hyphen = byte("-")
+        for line in hyphenations:gmatch("[^ \n\r]+") do
+            local ok = true
+            for b in line:utfvalues() do
+                if b == hyphen then
+                    -- ok
+                else
+                    local ct = cd[b].category
+                    if ct == "lu" or ct == "ll" then
+                        hused[char(b)] = true
+                    else
+                        h[b] = (h[b] or 0) + 1
+                        ok = false
+                    end
+                end
+            end
+            if ok then
+                hyps[#hyps+1] = line
+            end
+        end
+        local stripped = { }
+        for k, v in next, p do
+            if mnemonic then
+                logs.simple("invalid character %s (0x%04X) in patterns of language %s, file %s, n=%s",char(k),k,mnemonic,name,v)
+            else
+                logs.simple("invalid character %s (0x%04X) in patterns of file %s, n=%s",char(k),k,name,v)
+            end
+            if not permitted_characters[k] then
+                okay = false
+            else
+                stripped[k] = true
+            end
+        end
+        for k, v in next, h do
+            if mnemonic then
+                logs.simple("invalid character %s (0x%04X) in exceptions of language %s, file %s, n=%s",char(k),k,mnemonic,name,v)
+            else
+                logs.simple("invalid character %s (0x%04X) in exceptions of file %s, n=%s",char(k),k,name,v)
+            end
+            if not permitted_characters[k] then
+                okay = false
+            else
+                stripped[k] = true
+            end
+        end
+        local stripset = ""
+        for k, v in next, stripped do
+            logs.simple("entries that contain character %s will be omitted",char(k))
+            stripset = stripset .. "%" .. char(k)
+        end
+        return okay, pats, hyps, comment, stripset, pused, hused
+    else
+        if mnemonic then
+            logs.simple("no file %s for language %s",fullname,mnemonic)
+        else
+            logs.simple("no file %s",fullname)
+        end
+        return false, { }, { }, "", "", { }, { }
+    end
+end
+
+function scripts.patterns.save(destination,mnemonic,patterns,hyphenations,comment,stripped,pused,hused)
+    local nofpatterns = #patterns
+    local nofhyphenations = #hyphenations
+    local pu = table.concat(table.sortedkeys(pused), " ")
+    local hu = table.concat(table.sortedkeys(hused), " ")
+    logs.simple("language %s has %s patterns and %s exceptions",mnemonic,nofpatterns,nofhyphenations)
+    if mnemonic ~= "??" then
+        local rmefile = file.join(destination,"lang-"..mnemonic..".rme")
+        local patfile = file.join(destination,"lang-"..mnemonic..".pat")
+        local hypfile = file.join(destination,"lang-"..mnemonic..".hyp")
+        local topline = "% generated by mtxrun --script pattern --convert"
+        local banner = "% for comment and copyright, see " .. rmefile
+        logs.simple("saving language data for %s",mnemonic)
+        if not comment or comment == "" then comment = "% no comment" end
+        if not type(destination) == "string" then destination = "." end
+        os.remove(rmefile)
+        os.remove(patfile)
+        os.remove(hypfile)
+        io.savedata(rmefile,format("%s\n\n%s",topline,comment))
+        io.savedata(patfile,format("%s\n\n%s\n\n%% used: %s\n\n\\patterns{\n%s}",topline,banner,pu,table.concat(patterns,"\n")))
+        io.savedata(hypfile,format("%s\n\n%s\n\n%% used: %s\n\n\\hyphenation{\n%s}",topline,banner,hu,table.concat(hyphenations,"\n")))
+    end
+end
+
+function scripts.patterns.prepare()
+    dofile(resolvers.find_file("char-def.lua"))
+end
+
+function scripts.patterns.check()
+    local path = environment.argument("path") or "."
+    local found = false
+    local files = environment.files
+    if #files > 0 then
+        for i=1,#files do
+            local name = files[i]
+            logs.simple("checking language file %s", name)
+            local okay = scripts.patterns.load(path,name,nil,not environment.argument("fast"))
+            if #environment.files > 1 then
+                logs.simple("")
+            end
+        end
+    else
+        for k, v in next, scripts.patterns.list do
+            local mnemonic, name = v[1], v[2]
+            logs.simple("checking language %s, file %s", mnemonic, name)
+            local okay = scripts.patterns.load(path,name,mnemonic,not environment.argument("fast"))
+            if not okay then
+                logs.simple("there are errors that need to be fixed")
+            end
+            logs.simple("")
+        end
+    end
+end
+
+function scripts.patterns.convert()
+    local path = environment.argument("path") or "."
+    if path == "" then
+        logs.simple("provide sourcepath using --path ")
+    else
+        local destination = environment.argument("destination") or "."
+        if path == destination then
+            logs.simple("source path and destination path should differ (use --path and/or --destination)")
+        else
+            for k, v in next, scripts.patterns.list do
+                local mnemonic, name = v[1], v[2]
+                logs.simple("converting language %s, file %s", mnemonic, name)
+                local okay, patterns, hyphenations, comment, stripped, pused, hused = scripts.patterns.load(path,name,false)
+                if okay then
+                    scripts.patterns.save(destination,mnemonic,patterns,hyphenations,comment,stripped,pused,hused)
+                else
+                    logs.simple("convertion aborted due to error(s)")
+                end
+                logs.simple("")
+            end
+        end
+    end
+end
+
+logs.extendbanner("ConTeXt Pattern File Management 0.20",true)
+
+messages.help = [[
+--convert             generate context language files (mnemonic driven, if not given then all)
+--check               check pattern file (or those used by context when no file given)
+
+--fast                only report filenames, no lines
+]]
+
+if environment.argument("check") then
+    scripts.patterns.prepare()
+    scripts.patterns.check()
+elseif environment.argument("convert") then
+    scripts.patterns.prepare()
+    scripts.patterns.convert()
+else
+    logs.help(messages.help)
+end
+
+-- mtxrun --script pattern --check hyph-*.tex
+-- mtxrun --script pattern --check          --path=c:/data/develop/svn-hyphen/trunk/hyph-utf8/tex/generic/hyph-utf8/patterns
+-- mtxrun --script pattern --check   --fast --path=c:/data/develop/svn-hyphen/trunk/hyph-utf8/tex/generic/hyph-utf8/patterns
+-- mtxrun --script pattern --convert        --path=c:/data/develop/svn-hyphen/trunk/hyph-utf8/tex/generic/hyph-utf8/patterns --destination=e:/tmp/patterns
+-- mtxrun --script pattern --convert        --path=c:/data/develop/svn-hyphen/branches/luatex/hyph-utf8/tex/generic/hyph-utf8/patterns/tex --destination=e:/tmp/patterns
diff --git a/scripts/context/lua/mtx-profile.lua b/scripts/context/lua/mtx-profile.lua
new file mode 100644
index 000000000..11d48d039
--- /dev/null
+++ b/scripts/context/lua/mtx-profile.lua
@@ -0,0 +1,170 @@
+if not modules then modules = { } end modules ['mtx-profile'] = {
+    version   = 1.000,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- todo: also line number
+-- todo: sort runtime as option
+
+local match, format, find = string.match, string.format, string.find
+
+scripts          = scripts or { }
+scripts.profiler = scripts.profiler or { }
+
+local timethreshold    = 0
+local callthreshold    = 2500
+local countthreshold   = 2500
+
+local functiontemplate  = "%12s %03.4f %9i %s"
+local calltemplate      = "%9i %s"
+local totaltemplate     = "%i internal calls, %i function calls taking %3.4f seconds"
+local thresholdtemplate = "thresholds: %i internal calls, %i function calls, %i seconds"
+
+function scripts.profiler.analyse(filename)
+    local f = io.open(filename)
+    if f then
+        local times, counts, calls = { }, { }, { }
+        local totalruntime, totalcount, totalcalls = 0, 0, 0
+        while true do
+            local line = f:read()
+            if line then
+                local stacklevel, filename, functionname, linenumber, currentline, localtime, totaltime = line:match("^(%d+)\t(.-)\t(.-)\t(.-)\t(.-)\t(.-)\t(.-)")
+                if not filename then
+                    -- next
+                elseif filename == "=[C]" then
+                    if not functionname:find("^%(") then
+                        calls[functionname] = (calls[functionname] or 0) + 1
+                    end
+                else
+                    local filename = filename:match("^@(.*)$")
+                    if filename then
+                        local fi = times[filename]
+                        if not fi then fi = { } times[filename] = fi end
+                        fi[functionname] = (fi[functionname] or 0) + tonumber(localtime)
+                        counts[functionname] = (counts[functionname] or 0) + 1
+                    end
+                end
+            else
+                break
+            end
+        end
+        f:close()
+        print("")
+        local loaded = { }
+        local sortedtable.sortedkeys(times)
+        for i=1,#sorted do
+            local filename = sorted[i]
+            local functions = times[filename]
+            local sorted = table.sortedkeys(functions)
+            for i=1,#sorted do
+                local functionname = sorted[i]
+                local totaltime = functions[functionname]
+                local count = counts[functionname]
+                totalcount = totalcount + count
+                if totaltime > timethreshold or count > countthreshold then
+                    totalruntime = totalruntime + totaltime
+                    local functionfile, somenumber = functionname:match("^@(.+):(.-)$")
+                    if functionfile then
+                        local number = tonumber(somenumber)
+                        if number then
+                            if not loaded[functionfile] then
+                                loaded[functionfile] = string.splitlines(io.loaddata(functionfile) or "")
+                            end
+                            functionname = loaded[functionfile][number] or functionname
+                            functionname = functionname:gsub("^%s*","")
+                            functionname = functionname:gsub("%s*%-%-.*$","")
+                            functionname = number .. ": " .. functionname
+                        end
+                    end
+                    filename = file.basename(filename)
+                    print(functiontemplate:format(filename,totaltime,count,functionname))
+                end
+            end
+        end
+        print("")
+        local sorted = table.sortedkeys(calls)
+        for i=1,#sorted do
+            local call = sorted[i]
+            local n = calls[call]
+            totalcalls = totalcalls + n
+            if n > callthreshold then
+                print(calltemplate:format(n,call))
+            end
+        end
+        print("")
+        print(totaltemplate:format(totalcalls,totalcount,totalruntime))
+        print("")
+        print(thresholdtemplate:format(callthreshold,countthreshold,timethreshold))
+    end
+end
+
+function scripts.profiler.x_analyse(filename)
+    local f = io.open(filename)
+    local calls = { }
+    local lines = 0
+    if f then
+        while true do
+            local line = f:read()
+            if line then
+                lines = lines + 1
+                local c = match(line,"\\([a-zA-Z%!%?@]+) *%->")
+                if c then
+                    local cc = calls[c]
+                    if not cc then
+                        calls[c] = 1
+                    else
+                        calls[c] = cc + 1
+                    end
+                end
+            else
+                break
+            end
+        end
+        f:close()
+        local noc = 0
+local criterium = 100
+        for name, n in next, calls do
+            if n > criterium then
+                if find(name,"^@@[a-z][a-z]") then
+                    -- parameter
+                elseif find(name,"^[cvserft]%!") then
+                    -- variables and constants
+                elseif find(name,"^%?%?[a-z][a-z]$") then
+                    -- prefix
+                elseif find(name,"^%!%!") then
+                    -- reserved
+                elseif find(name,"^@.+@$") then
+                    -- weird
+                else
+                    noc = noc + n
+                    print(format("%6i: %s",n,name))
+                end
+            end
+        end
+        print("")
+        print(format("number of lines: %s",lines))
+        print(format("number of calls: %s",noc))
+        print(format("criterium calls: %s",criterium))
+    end
+end
+
+--~ scripts.profiler.analyse("t:/manuals/mk/mk-fonts-profile.lua")
+--~ scripts.profiler.analyse("t:/manuals/mk/mk-introduction-profile.lua")
+
+logs.extendbanner("ConTeXt MkIV LuaTeX Profiler 1.00",true)
+
+messages.help = [[
+--analyse             analyse lua calls
+--trace               analyse tex calls
+]]
+
+if environment.argument("analyse") then
+    scripts.profiler.analyse(environment.files[1] or "luatex-profile.log")
+elseif environment.argument("trace") then
+    scripts.profiler.analyse(environment.files[1] or "temp.log")
+else
+    logs.help(messages.help)
+end
diff --git a/scripts/context/lua/mtx-scite.lua b/scripts/context/lua/mtx-scite.lua
new file mode 100644
index 000000000..d5f0a5344
--- /dev/null
+++ b/scripts/context/lua/mtx-scite.lua
@@ -0,0 +1,166 @@
+if not modules then modules = { } end modules ['mtx-scite'] = {
+    version   = 1.001,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- todo: append to global properties else order of loading problem
+-- linux problem ... files are under root protection so we need --install
+
+scripts       = scripts       or { }
+scripts.scite = scripts.scite or { }
+
+local scitesignals = { "scite-context.rme", "context.properties" }
+local screenfont   = "lmtypewriter10-regular.ttf"
+
+function scripts.scite.start(indeed)
+    local usedsignal, datapath, fullname, workname, userpath, fontpath
+    if os.type == "windows" then
+        workname = "scite.exe"
+        userpath = os.getenv("USERPROFILE") or ""
+        fontpath = os.getenv("SYSTEMROOT")
+        fontpath = (fontpath and file.join(fontpath,"fonts")) or ""
+    else
+        workname = "scite"
+        userpath = os.getenv("HOME") or ""
+        fontpath = ""
+    end
+    local binpaths = file.split_path(os.getenv("PATH")) or file.split_path(os.getenv("path"))
+    for i=1,#scitesignals do
+        local scitesignal = scitesignals[i]
+        local scitepath = resolvers.find_file(scitesignal,"other text files") or ""
+        if scitepath ~= "" then
+            scitepath  = file.dirname(scitepath) -- data
+            if scitepath == "" then
+                scitepath = resolvers.clean_path(lfs.currentdir())
+            else
+                usedsignal, datapath = scitesignal, scitepath
+                break
+            end
+        end
+    end
+    if not datapath or datapath == "" then
+        logs.simple("invalid datapath, maybe you need to regenerate the file database")
+        return false
+    end
+    if not binpaths or #binpaths == 0 then
+        logs.simple("invalid binpath")
+        return false
+    end
+    for i=1,#binpaths do
+        local p = file.join(binpaths[i],workname)
+        if lfs.isfile(p) and lfs.attributes(p,"size") > 10000 then -- avoind stub
+            fullname = p
+            break
+        end
+    end
+    if not fullname then
+        logs.simple("unable to locate %s",workname)
+        return false
+    end
+    local properties  = dir.glob(file.join(datapath,"*.properties"))
+    local luafiles    = dir.glob(file.join(datapath,"*.lua"))
+    local extrafont   = resolvers.find_file(screenfont,"truetype font") or ""
+    local pragmafound = dir.glob(file.join(datapath,"pragma.properties"))
+    if userpath == "" then
+        logs.simple("unable to figure out userpath")
+        return false
+    end
+    local verbose = environment.argument("verbose")
+    local tobecopied, logdata = { }, { }
+    local function check_state(fullname,newpath)
+        local basename = file.basename(fullname)
+        local destination = file.join(newpath,basename)
+        local pa, da = lfs.attributes(fullname), lfs.attributes(destination)
+        if not da then
+            logdata[#logdata+1] = { "new        : %s", basename }
+            tobecopied[#tobecopied+1] = { fullname, destination }
+        elseif pa.modification > da.modification then
+            logdata[#logdata+1] = { "outdated   : %s", basename }
+            tobecopied[#tobecopied+1] = { fullname, destination }
+        else
+            logdata[#logdata+1] = { "up to date : %s", basename }
+        end
+    end
+    for i=1,#properties do
+        check_state(properties[i],userpath)
+    end
+    for i=1,#luafiles do
+        check_state(luafiles[i],userpath)
+    end
+    if fontpath ~= "" then
+        check_state(extrafont,fontpath)
+    end
+    local userpropfile = "SciTEUser.properties"
+    if os.name ~= "windows" then
+        userpropfile = "."  .. userpropfile
+    end
+    local fullpropfile = file.join(userpath,userpropfile)
+    local userpropdata = io.loaddata(fullpropfile) or ""
+    local propfiledone = false
+    if pragmafound then
+        if userpropdata == "" then
+            logdata[#logdata+1] = { "error      : no user properties found on '%s'", fullpropfile }
+        elseif string.find(userpropdata,"import *pragma") then
+            logdata[#logdata+1] = { "up to date : 'import pragma' in '%s'", userpropfile }
+        else
+            logdata[#logdata+1] = { "yet unset  : 'import pragma' in '%s'", userpropfile }
+            userproperties = userpropdata .. "\n\nimport pragma\n\n"
+            propfiledone = true
+        end
+    else
+        if string.find(userpropdata,"import *context") then
+            logdata[#logdata+1] = { "up to date : 'import context' in '%s'", userpropfile }
+        else
+            logdata[#logdata+1] = { "yet unset  : 'import context' in '%s'", userpropfile }
+            userproperties = userpropdata .. "\n\nimport context\n\n"
+            propfiledone = true
+        end
+    end
+    if not indeed or verbose then
+        logs.simple("used signal: %s", usedsignal)
+        logs.simple("data path  : %s", datapath)
+        logs.simple("full name  : %s", fullname)
+        logs.simple("user path  : %s", userpath)
+        logs.simple("extra font : %s", extrafont)
+    end
+    if #logdata > 0 then
+        logs.simple("")
+        for k=1,#logdata do
+            local v = logdata[k]
+            logs.simple(v[1],v[2])
+        end
+    end
+    if indeed then
+        if #tobecopied > 0 then
+            logs.simple("warning    : copying updated files")
+            for i=1,#tobecopied do
+                local what = tobecopied[i]
+                logs.simple("copying    : '%s' => '%s'",what[1],what[2])
+                file.copy(what[1],what[2])
+            end
+        end
+        if propfiledone then
+            logs.simple("saving     : '%s'",userpropfile)
+            io.savedata(fullpropfile,userpropdata)
+        end
+        os.launch(fullname)
+    end
+end
+
+logs.extendbanner("Scite Startup Script 1.00",true)
+
+messages.help = [[
+--start [--verbose]   start scite
+--test                report what will happen
+]]
+
+if environment.argument("start") then
+    scripts.scite.start(true)
+elseif environment.argument("test") then
+    scripts.scite.start()
+else
+    logs.help(messages.help)
+end
diff --git a/scripts/context/lua/mtx-server-ctx-fonttest.lua b/scripts/context/lua/mtx-server-ctx-fonttest.lua
new file mode 100644
index 000000000..b2a993bf8
--- /dev/null
+++ b/scripts/context/lua/mtx-server-ctx-fonttest.lua
@@ -0,0 +1,733 @@
+if not modules then modules = { } end modules ['mtx-server-ctx-fonttest'] = {
+    version   = 1.001,
+    comment   = "Font Feature Tester",
+    author    = "Hans Hagen",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+--~ dofile(resolvers.find_file("l-aux.lua","tex"))
+dofile(resolvers.find_file("trac-lmx.lua","tex"))
+dofile(resolvers.find_file("font-ott.lua","tex"))
+dofile(resolvers.find_file("font-syn.lua","tex"))
+dofile(resolvers.find_file("font-mis.lua","tex"))
+dofile(resolvers.find_file("font-otp.lua","tex"))
+
+local format, gsub, concat, match, find = string.format, string.gsub, table.concat, string.match, string.find
+
+local sample_line = "This is a sample line!"
+local tempname    = "mtx-server-ctx-fonttest-temp"
+local temppath    = caches.setpath("temp","mtx-server-ctx-fonttest")
+local basename    = "mtx-server-ctx-fonttest-data.lua"
+local basepath    = temppath
+
+local remove_suffixes = { "tex", "pdf", "log" }
+local what_options = { "trace", "basemode" }
+
+for i=1,#remove_suffixes do
+    os.remove(file.join(temppath,file.addsuffix(tempname,remove_suffixes[i])))
+end
+
+local process_templates = { }
+
+process_templates.default = [[
+\starttext
+    \setcharactermirroring[1]
+    \definefontfeature[sample][analyse=yes,%s]
+    \definedfont[name:%s*sample]
+    \startTEXpage[offset=3pt]
+        \detokenize{%s}
+    \stopTEXpage
+\stoptext
+]]
+
+process_templates.cache = [[
+\starttext
+    \definedfont[name:%s]
+    \startTEXpage[offset=3pt]
+        cached: \detokenize{%s}
+    \stopTEXpage
+\stoptext
+]]
+
+process_templates.trace = [[
+\usemodule[fnt-20]
+
+\definefontfeature[sample][%s]
+
+\setupcolors[state=start]
+
+\setcharactermirroring[1]
+
+\setvariables
+  [otftracker]
+  [title=Test Run,
+   font=name:%s,
+   direction=0,
+   features=sample,
+   sample={‍\detokenize{%s}}]
+]]
+
+local javascripts = [[
+function selected_radio(name) {
+    var form = document.forms["main-form"] ;
+    var script = form.elements[name] ;
+	if (script) {
+        var n = script.length ;
+        if (n) {
+            for (var i=0; i<n; i++) {
+                if (script[i].checked) {
+                    return script[i].value ;
+                }
+            }
+		}
+	}
+    return "" ;
+}
+
+function reset_valid() {
+    var fields = document.getElementsByTagName("span") ;
+    for (var i=0; i<fields.length; i++) {
+        var e = fields[i]
+        if (e) {
+            if (e.className == "valid") {
+                e.className = "" ;
+            }
+        }
+    }
+}
+
+function set_valid() {
+    var script = selected_radio("script") ;
+    var language = selected_radio("language") ;
+    if (script && language) {
+        var s = feature_hash[script] ;
+        if (s) {
+            for (l in s) {
+                var e = document.getElementById("t-l-" + l) ;
+                if (e) {
+                    e.className = "valid" ;
+                }
+            }
+            var l = s[language] ;
+            if (l) {
+                for (i in l) {
+                    var e = document.getElementById("t-f-" + i) ;
+                    if (e) {
+                        e.className = "valid" ;
+                    }
+                }
+            }
+            var e = document.getElementById("t-s-" + script) ;
+            if (e) {
+                e.className = "valid" ;
+            }
+        }
+    }
+}
+
+function check_form() {
+    reset_valid() ;
+    set_valid() ;
+}
+
+function check_script() {
+    reset_valid() ;
+    set_valid() ;
+}
+
+function check_language() {
+    reset_valid() ;
+    set_valid() ;
+}
+
+function check_feature() {
+    // not needed
+}
+]]
+
+local cache = { }
+
+local function showfeatures(f)
+    if f then
+        logs.simple("processing font '%s'",f)
+        local features = cache[f]
+        if features == nil then
+            features = fonts.get_features(resolvers.find_file(f))
+            if not features then
+                logs.simple("building cache for '%s'",f)
+                io.savedata(file.join(temppath,file.addsuffix(tempname,"tex")),format(process_templates.cache,f,f))
+                os.execute(format("mtxrun --path=%s --script context --once --batchmode %s",temppath,tempname))
+                features = fonts.get_features(f)
+            end
+            cache[f] = features or false
+            logs.simple("caching info of '%s'",f)
+        else
+            logs.simple("using cached info of '%s'",f)
+        end
+        if features then
+            local scr, lan, fea, rev = { }, { }, { }, { }
+            local function show(what)
+                local data = features[what]
+                if data and next(data) then
+                    for f,ff in next, data do
+                        if find(f,"<") then
+                            -- ignore aat for the moment
+                        else
+                            fea[f] = true
+                            for s, ss in next, ff do
+                                if find(s,"%*") then
+                                    -- ignore *
+                                else
+                                    scr[s] = true
+                                    local rs = rev[s] if not rs then rs = {} rev[s] = rs end
+                                    for k, l in next, ss do
+                                        if find(k,"%*") then
+                                            -- ignore *
+                                        else
+                                            lan[k] = true
+                                            local rsk = rs[k] if not rsk then rsk = { } rs[k] = rsk end
+                                            rsk[f] = true
+                                        end
+                                    end
+                                end
+                            end
+                        end
+                    end
+                end
+            end
+            for what, v in table.sortedhash(features) do
+                show(what)
+            end
+            local stupid = { }
+            stupid[#stupid+1] = "var feature_hash = new Array ;"
+            for s, sr in next, rev do
+                stupid[#stupid+1] = format("feature_hash['%s'] = new Array ;",s)
+                for l, lr in next, sr do
+                    stupid[#stupid+1] = format("feature_hash['%s']['%s'] = new Array ;",s,l)
+                    for f, fr in next, lr do
+                        stupid[#stupid+1] = format("feature_hash['%s']['%s']['%s'] = true ;",s,l,f)
+                    end
+                end
+            end
+            -- gpos feature script languages
+            return {
+                scripts = scr,
+                languages = lan,
+                features = fea,
+                javascript = concat(stupid,"\n")
+            }
+        end
+    end
+end
+
+local template_h = [[
+<tr>
+    <th>safe name&nbsp;&nbsp;&nbsp;&nbsp;</th>
+    <th>family name&nbsp;&nbsp;&nbsp;&nbsp;</th>
+    <th>style-variant-weight-width&nbsp;&nbsp;&nbsp;&nbsp;</th>
+    <th>font name&nbsp;&nbsp;&nbsp;&nbsp;</th>
+    <th>weight&nbsp;&nbsp;&nbsp;&nbsp;</th>
+    <th>filename</th>
+</tr>]]
+
+local template_d = [[
+<tr>
+    <td><a href='mtx-server-ctx-fonttest.lua?selection=%s'>%s</a>&nbsp;&nbsp;&nbsp;&nbsp;</td>
+    <td>%s&nbsp;&nbsp;&nbsp;&nbsp;</td>
+    <td>%s-%s-%s-%s&nbsp;&nbsp;&nbsp;&nbsp;</td>
+    <td>%s&nbsp;&nbsp;&nbsp;&nbsp;</td>
+    <td>%s&nbsp;&nbsp;&nbsp;&nbsp;</td>
+    <td>%s</td>
+</tr>]]
+
+local function select_font()
+    local t = fonts.names.list(".*",false,true)
+    if t then
+        local listoffonts = { }
+        listoffonts[#listoffonts+1] = "<table>"
+        listoffonts[#listoffonts+1] = template_h
+        for k, v in table.sortedhash(t) do
+            local kind = v.format
+            if kind == "otf" or kind == "ttf" or kind == "ttc" then
+                local fontname = v.fontname
+                listoffonts[#listoffonts+1] = format(template_d, fontname, fontname,
+                    v.familyname or "",
+                    t.variant    or "normal",
+                    t.weight     or "normal",
+                    t.width      or "normal",
+                    t.style      or "normal",
+                    v.rawname    or fontname,
+                    v.fontweight or "",
+                    v.filename   or ""
+                )
+            end
+        end
+        listoffonts[#listoffonts+1] = "</table>"
+        return concat(listoffonts,"\n")
+    end
+    return "<b>no fonts</b>"
+end
+
+local edit_template = [[
+    <textarea name='sampletext' rows='5' cols='100'>%s</textarea>
+    <br/> <br/>name:&nbsp;<input type='text' name='name' size='20' value=%q/>&nbsp;&nbsp; title:&nbsp;<input type='text' name='title' size='40' value=%q/>
+    <br/> <br/>scripts:&nbsp;%s
+    <br/> <br/>languages:&nbsp;%s
+    <br/> <br/>features:&nbsp;%s
+    <br/> <br/>options:&nbsp;%s
+]]
+
+local result_template = [[
+    <br/> <br/>
+    <embed src="%s#toolbar=0&amp;navpanes=0&amp;scrollbar=0" width="100%%"/>
+    <br/> <br/> results:
+    <a href='%s' target="source">tex file</a>
+    <a href='%s' target="result">pdf file</a>
+    <br/> <br/>
+]]
+
+scripts.webserver.registerpath(temppath)
+
+local function get_specification(name)
+    return fonts.names.resolvedspecification(name or "")
+end
+
+local function edit_font(currentfont,detail,tempname)
+    logs.simple("entering edit mode for '%s'",currentfont)
+    local specification = get_specification(currentfont)
+    if specification then
+        local htmldata = showfeatures(specification.filename)
+        if htmldata then
+            local features, languages, scripts, options = { }, { }, { }, { }
+            local sorted = table.sortedkeys(htmldata.scripts)
+            for k=1,#sorted do
+                local v = sorted[k]
+                local s = fonts.otf.tables.scripts[v] or v
+                if detail and v == detail.script then
+                    scripts[#scripts+1] = format("<input title='%s' id='s-%s' type='radio' name='script' value='%s' onclick='check_script()' checked='checked'/>&nbsp;<span id='t-s-%s'>%s</span>",s,v,v,v,v)
+                else
+                    scripts[#scripts+1] = format("<input title='%s' id='s-%s' type='radio' name='script' value='%s' onclick='check_script()' />&nbsp;<span id='t-s-%s'>%s</span>",s,v,v,v,v)
+                end
+            end
+            local sorted = table.sortedkeys(htmldata.languages)
+            for k=1,#sorted do
+                local v = sorted[k]
+                local l = fonts.otf.tables.languages[v] or v
+                if detail and v == detail.language then
+                    languages[#languages+1] = format("<input title='%s' id='l-%s' type='radio' name='language' value='%s' onclick='check_language()' checked='checked'/>&nbsp;<span id='t-l-%s'>%s</span>",l,v,v,v,v)
+                else
+                    languages[#languages+1] = format("<input title='%s' id='l-%s' type='radio' name='language' value='%s' onclick='check_language()' />&nbsp;<span id='t-l-%s'>%s</span>",l,v,v,v,v)
+                end
+            end
+            local sorted = table.sortedkeys(htmldata.features)
+            for k=1,#sorted do
+                local v = sorted[k]
+                local f = fonts.otf.tables.features[v] or v
+                if detail and detail["f-"..v] then
+                    features[#features+1] = format("<input title='%s' id='f-%s' type='checkbox' name='f-%s' onclick='check_feature()' checked='checked'/>&nbsp;<span id='t-f-%s'>%s</span>",f,v,v,v,v)
+                else
+                    features[#features+1] = format("<input title='%s' id='f-%s' type='checkbox' name='f-%s' onclick='check_feature()' />&nbsp;<span id='t-f-%s'>%s</span>",f,v,v,v,v)
+                end
+            end
+            for k=1,#what_options do
+                local v = what_options[k]
+                if detail and detail["o-"..v] then
+                    options[#options+1] = format("<input id='o-%s' type='checkbox' name='o-%s' checked='checked'/>&nbsp;%s",v,v,v)
+                else
+                    options[#options+1] = format("<input id='o-%s' type='checkbox' name='o-%s'/>&nbsp;%s",v,v,v)
+                end
+            end
+            local e = format(edit_template,
+                (detail and detail.sampletext) or sample_line,(detail and detail.name) or "no name",(detail and detail.title) or "",
+                concat(scripts,"  "),concat(languages,"  "),concat(features,"  "),concat(options,"  "))
+            if tempname then
+                local pdffile, texfile = file.addsuffix(tempname,"pdf"), file.addsuffix(tempname,"tex")
+                local r = format(result_template,pdffile,texfile,pdffile)
+                return e .. r, htmldata.javascript or ""
+            else
+                return e, htmldata.javascript or ""
+            end
+        else
+            return "error, nothing set up yet"
+        end
+    else
+        return "error, no info about font"
+    end
+end
+
+local function process_font(currentfont,detail) -- maybe just fontname
+    local features = {
+        "mode=node",
+        format("language=%s",detail.language or "dflt"),
+        format("script=%s",detail.script or "dflt"),
+    }
+    for k,v in next, detail do
+        local f = match(k,"^f%-(.*)$")
+        if f then
+            features[#features+1] = format("%s=yes",f)
+        end
+    end
+    local variant = process_templates.default
+    if detail["o-trace"] then
+        variant = process_templates.trace
+    end
+    local sample = string.strip(detail.sampletext or "")
+    if sample == "" then sample = sample_line end
+    logs.simple("sample text: %s",sample)
+    io.savedata(file.join(temppath,file.addsuffix(tempname,"tex")),format(variant,concat(features,","),currentfont,sample))
+    os.execute(format("mtxrun --path=%s --script context --once --batchmode %s",temppath,tempname))
+    return edit_font(currentfont,detail,tempname)
+end
+
+local tex_template = [[
+<pre><tt>
+%s
+</tt></pre>
+]]
+
+local function show_source(currentfont,detail)
+    if tempname and tempname ~= "" then
+        return format(tex_template,io.loaddata(file.join(temppath,file.addsuffix(tempname,"tex"))) or "no source yet")
+    else
+        return "no source file"
+    end
+end
+
+local function show_log(currentfont,detail)
+    if tempname and tempname ~= "" then
+        local data = io.loaddata(file.join(temppath,file.addsuffix(tempname,'log'))) or "no log file yet"
+        data = gsub(data,"[%s%%]*begin of optionfile.-end of optionfile[%s%%]*","\n")
+        return format(tex_template,data)
+    else
+        return "no log file"
+    end
+end
+
+local function show_font(currentfont,detail)
+    local specification = get_specification(currentfont)
+    local features = fonts.get_features(specification.filename)
+    local result = { }
+    result[#result+1] = format("<h1>names</h1>",what)
+    result[#result+1] = "<table>"
+    result[#result+1] = format("<tr><td class='tc'>fontname:   </td><td>%s</td></tr>",currentfont)
+    result[#result+1] = format("<tr><td class='tc'>fullname:   </td><td>%s</td></tr>",specification.fontname   or "-")
+    result[#result+1] = format("<tr><td class='tc'>filename:   </td><td>%s</td></tr>",specification.fontfile   or "-")
+    result[#result+1] = format("<tr><td class='tc'>familyname: </td><td>%s</td></tr>",specification.familyname or "-")
+    result[#result+1] = format("<tr><td class='tc'>fontweight: </td><td>%s</td></tr>",specification.fontweight or "-")
+    result[#result+1] = format("<tr><td class='tc'>format:     </td><td>%s</td></tr>",specification.format     or "-")
+    result[#result+1] = format("<tr><td class='tc'>fullname:   </td><td>%s</td></tr>",specification.fullname   or "-")
+    result[#result+1] = format("<tr><td class='tc'>subfamily:  </td><td>%s</td></tr>",specification.subfamily  or "-")
+    result[#result+1] = format("<tr><td class='tc'>rawname:    </td><td>%s</td></tr>",specification.rawname    or "-")
+    result[#result+1] = format("<tr><td class='tc'>designsize: </td><td>%s</td></tr>",specification.designsize or "-")
+    result[#result+1] = format("<tr><td class='tc'>minimumsize:</td><td>%s</td></tr>",specification.minsize    or "-")
+    result[#result+1] = format("<tr><td class='tc'>maximumsize:</td><td>%s</td></tr>",specification.maxsize    or "-")
+    result[#result+1] = format("<tr><td class='tc'>style:      </td><td>%s</td></tr>",specification.style   ~= "" and specification.style or "normal")
+    result[#result+1] = format("<tr><td class='tc'>variant:    </td><td>%s</td></tr>",specification.variant ~= "" and specification.variant    or "normal")
+    result[#result+1] = format("<tr><td class='tc'>weight:     </td><td>%s</td></tr>",specification.weight  ~= "" and specification.weight     or "normal")
+    result[#result+1] = format("<tr><td class='tc'>width:      </td><td>%s</td></tr>",specification.width   ~= "" and specification.width      or "normal")
+    result[#result+1] = "</table>"
+    if features then
+        for what, v in table.sortedhash(features) do
+            local data = features[what]
+            if data and next(data) then
+                result[#result+1] = format("<h1>%s features</h1>",what)
+                result[#result+1] = "<table>"
+                result[#result+1] = "<tr><th>feature</th><th>tag&nbsp;</th><th>script&nbsp;</th><th>languages&nbsp;</th></tr>"
+                for f,ff in table.sortedhash(data) do
+                    local done = false
+                    for s, ss in table.sortedhash(ff) do
+                        if s == "*"  then s       = "all" end
+                        if ss  ["*"] then ss["*"] = nil ss.all = true end
+                        if done then
+                            f = ""
+                        else
+                            done = true
+                        end
+                        local title = fonts.otf.tables.features[f] or ""
+                        result[#result+1] = format("<tr><td width='50%%'>%s&nbsp;&nbsp;</td><td><tt>%s&nbsp;&nbsp;</tt></td><td><tt>%s&nbsp;&nbsp;</tt></td><td><tt>%s&nbsp;&nbsp;</tt></td></tr>",title,f,s,concat(table.sortedkeys(ss)," "))
+                    end
+                end
+                result[#result+1] = "</table>"
+            end
+        end
+    else
+        result[#result+1] = "<br/><br/>This font has no features."
+    end
+    return concat(result,"\n")
+end
+
+
+local info_template = [[
+<pre><tt>
+version   : %s
+comment   : %s
+author    : %s
+copyright : %s
+
+maillist  : ntg-context at ntg.nl
+webpage   : www.pragma-ade.nl
+wiki      : contextgarden.net
+</tt></pre>
+]]
+
+local function info_about()
+    local m = modules ['mtx-server-ctx-fonttest']
+    return format(info_template,m.version,m.comment,m.author,m.copyright)
+end
+
+local save_template = [[
+    the current setup has been saved:
+    <br/> <br/>
+    <table>
+    <tr><td class='tc'>name&nbsp;      </td><td>%s</td></tr>
+    <tr><td class='tc'>title&nbsp;     </td><td>%s</td></tr>
+    <tr><td class='tc'>font&nbsp;      </td><td>%s</td></tr>
+    <tr><td class='tc'>script&nbsp;    </td><td>%s</td></tr>
+    <tr><td class='tc'>language&nbsp;  </td><td>%s</td></tr>
+    <tr><td class='tc'>features&nbsp;  </td><td>%s</td></tr>
+    <tr><td class='tc'>options&nbsp;   </td><td>%s</td></tr>
+    <tr><td class='tc'>sampletext&nbsp;</td><td>%s</td></tr>
+    </table>
+]]
+
+local function loadbase()
+    local datafile = file.join(basepath,basename)
+    local storage = io.loaddata(datafile) or ""
+    if storage == "" then
+        storage = { }
+    else
+        logs.simple("loading '%s'",datafile)
+        storage = loadstring(storage)
+        storage = (storage and storage()) or { }
+    end
+    return storage
+end
+
+local function loadstored(detail,currentfont,name)
+    local storage = loadbase()
+    storage = storage and storage[name]
+    if storage then
+        currentfont = storage.font
+        detail.script = storage.script or detail.script
+        detail.language = storage.language or detail.language
+        detail.title = storage.title or detail.title
+        detail.sampletext = storage.text or detail.sampletext
+        detail.name = name or "no name"
+        for k,v in next, storage.features do
+            detail["f-"..k] = v
+        end
+        for k,v in next, storage.options do
+            detail["o-"..k] = v
+        end
+    end
+    detail.loadname = nil
+    return detail, currentfont
+end
+
+local function savebase(storage,name)
+    local datafile = file.join(basepath,basename)
+    logs.simple("saving '%s' in '%s'",name or "data",datafile)
+    io.savedata(datafile,table.serialize(storage,true))
+end
+
+local function deletestored(detail,currentfont,name)
+    local storage = loadbase()
+    if storage and name and storage[name] then
+        logs.simple("deleting '%s' from base",name)
+        storage[name] = nil
+        savebase(storage)
+    end
+    detail.deletename = nil
+    return detail, ""
+end
+
+local function save_font(currentfont,detail)
+    local specification = get_specification(currentfont)
+    local name, title, script, language, features, options, text = currentfont, "", "dflt", "dflt", { }, { }, ""
+    if detail then
+        local htmldata = showfeatures(specification.filename)
+        script = detail.script or script
+        language = detail.language or language
+        text = string.strip(detail.sampletext or text)
+        name = string.strip(detail.name or name)
+        title = string.strip(detail.title or title)
+        for k,v in next, htmldata.features do
+            if detail["f-"..k] then features[k] = true end
+        end
+        for k=1,#what_options do
+            local v = what_options[k]
+            if detail["o-"..v] then options[k] = true end
+        end
+    end
+    if name == "" then
+        name = "no name"
+    end
+    local storage = loadbase()
+    storage[name] = {
+        font = currentfont, title = title, script = script, language = language, features = features, options = options, text = text,
+    }
+    savebase(storage,name)
+    return format(save_template,name,title,currentfont,script,language,concat(table.sortedkeys(features)," "),concat(table.sortedkeys(options)," "),text)
+end
+
+local function load_font(currentfont)
+    local datafile = file.join(basepath,basename)
+    local storage = loadbase(datafile)
+    local result = {}
+    result[#result+1] = format("<tr><th>del&nbsp;</th><th>name&nbsp;</th><th>font&nbsp;</th><th>fontname&nbsp;</th><th>script&nbsp;</th><th>language&nbsp;</th><th>features&nbsp;</th><th>title&nbsp;</th><th>sampletext&nbsp;</th></tr>")
+    for k,v in table.sortedhash(storage) do
+        local fontname, fontfile = get_specification(v.font)
+        result[#result+1] = format("<tr><td><a href='mtx-server-ctx-fonttest.lua?deletename=%s'>x</a>&nbsp;</td><td><a href='mtx-server-ctx-fonttest.lua?loadname=%s'>%s</a>&nbsp;</td><td>%s&nbsp;</td<td>%s&nbsp;</td><td>%s&nbsp;</td><td>%s&nbsp;</td><td>%s&nbsp;</td><td>%s&nbsp;</td><td>%s&nbsp;</td></tr>",
+            k,k,k,v.font,fontname,v.script,v.language,concat(table.sortedkeys(v.features)," "),v.title or "no title",v.text or "")
+    end
+    if #result == 1 then
+        return "nothing saved yet"
+    else
+        return format("<table>%s</table>",concat(result,"\n"))
+    end
+end
+
+local function reset_font(currentfont)
+    return edit_font(currentfont)
+end
+
+local extras_template = [[
+    <a href='mtx-server-ctx-fonttest.lua?extra=reload'>remake font database</a> (take some time)<br/><br/>
+]]
+
+local function do_extras(detail,currentfont,extra)
+    return extras_template
+end
+
+local extras = { }
+
+local function do_extra(detail,currentfont,extra)
+    local e = extras[extra]
+    if e then e(detail,currentfont,extra) end
+    return do_extras(detail,currentfont,extra)
+end
+
+function extras.reload()
+    local command = "mtxrun --script font --reload"
+    logs.simple("run command: %s",command)
+    os.execute(command)
+    return do_extras()
+end
+
+
+local status_template = [[
+    <input type="hidden" name="currentfont" value="%s" />
+]]
+
+local variables = {
+    ['color-background-one'] = lmx.get('color-background-green'),
+    ['color-background-two'] = lmx.get('color-background-blue'),
+    ['title']                = 'ConTeXt Font Tester',
+    ['formaction']           = "mtx-server-ctx-fonttest.lua",
+}
+
+function doit(configuration,filename,hashed)
+
+    local start = os.clock()
+
+    local detail = url.query(hashed.query or "")
+
+    local currentfont = detail.currentfont
+    local action      = detail.action
+    local selection   = detail.selection
+
+    local loadname    = detail.loadname
+    local deletename  = detail.deletename
+    local extra       = detail.extra
+
+    if loadname and loadname ~= "" then
+        detail, currentfont = loadstored(detail,currentfont,loadname)
+        action = "process"
+    elseif deletename and deletename ~= "" then
+        detail, currentfont = deletestored(detail,currentfont,deletename)
+        action = "load"
+    elseif selection and selection ~= "" then
+        currentfont = selection
+    elseif extra and extra ~= "" then
+        do_extra(detail,currentfont,extra)
+        action = "extras"
+    end
+
+    local fontname, fontfile = get_specification(currentfont)
+
+    if fontfile then
+        variables.title = format('ConTeXt Font Tester: %s (%s)',fontname,fontfile)
+    else
+        variables.title = 'ConTeXt Font Tester'
+    end
+
+    -- lua table and adapt
+
+    local buttons = { 'process', 'select', 'save', 'load', 'edit', 'reset', 'features', 'source', 'log', 'info', 'extras'}
+    local menu    = { }
+
+    for i=1,#buttons do
+        local button = buttons[i]
+        menu[#menu+1] = format("<button name='action' value='%s' type='submit'>%s</button>",button,button)
+    end
+
+    variables.menu           = concat(menu,"&nbsp;")
+    variables.status         = format(status_template,currentfont or "")
+    variables.maintext       = ""
+    variables.javascriptdata = ""
+    variables.javascripts    = ""
+    variables.javascriptinit = ""
+
+    logs.simple("action: %s",action or "no action")
+
+    local result
+
+    if action == "select" then
+        variables.maintext = select_font()
+    elseif action == "info" then
+        variables.maintext = info_about()
+    elseif action == "extras" then
+        variables.maintext = do_extras()
+    elseif currentfont and currentfont ~= "" then
+        if action == "save" then
+            variables.maintext = save_font(currentfont,detail)
+        elseif action == "load" then
+            variables.maintext = load_font(currentfont,detail)
+        elseif action == "source" then
+            variables.maintext = show_source(currentfont,detail)
+        elseif action == "log" then
+            variables.maintext = show_log(currentfont,detail)
+        elseif action == "features" then
+            variables.maintext = show_font(currentfont,detail)
+        else
+            local e, s
+            if action == "process" then
+                e, s = process_font(currentfont,detail)
+            elseif action == "reset" then
+                e, s = reset_font(currentfont)
+            elseif action == "edit" then
+                e, s = edit_font(currentfont,detail)
+            else
+                e, s = process_font(currentfont,detail)
+            end
+            variables.maintext       = e
+            variables.javascriptdata = s
+            variables.javascripts    = javascripts
+            variables.javascriptinit = "check_form()"
+        end
+    else
+        variables.maintext = select_font()
+    end
+
+    result = { content = lmx.convert('context-fonttest.lmx',false,variables) }
+
+    logs.simple("time spent on page: %0.03f seconds",os.clock()-start)
+
+    return result
+
+end
+
+return doit, true
+
+--~ make_lmx_page("test")
diff --git a/scripts/context/lua/mtx-server-ctx-help.lua b/scripts/context/lua/mtx-server-ctx-help.lua
new file mode 100644
index 000000000..2f072f977
--- /dev/null
+++ b/scripts/context/lua/mtx-server-ctx-help.lua
@@ -0,0 +1,665 @@
+if not modules then modules = { } end modules ['mtx-server-ctx-help'] = {
+    version   = 1.001,
+    comment   = "Basic Definition Browser",
+    author    = "Hans Hagen",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- todo in lua interface: noargument, oneargument, twoarguments, threearguments
+
+--~ dofile(resolvers.find_file("l-aux.lua","tex"))
+--~ dofile(resolvers.find_file("l-url.lua","tex"))
+dofile(resolvers.find_file("trac-lmx.lua","tex"))
+
+-- problem ... serialize parent stack
+
+local format = string.format
+local concat = table.concat
+
+-- -- -- make this a module: cont-xx.lua
+
+document = document or { }
+document.setups = document.setups or { }
+
+document.setups.div = {
+    pe = "<div dir='rtl' lang='arabic'>%s</div>"
+}
+
+document.setups.span = {
+    pe = "<span dir='rtl' lang='arabic'>%s</span>"
+}
+
+document.setups.translations =  document.setups.translations or {
+
+    nl = {
+        ["title"]       = "setup",
+        ["formula"]     = "formule",
+        ["number"]      = "getal",
+        ["list"]        = "lijst",
+        ["dimension"]   = "maat",
+        ["mark"]        = "markering",
+        ["reference"]   = "verwijzing",
+        ["command"]     = "commando",
+        ["file"]        = "file",
+        ["name"]        = "naam",
+        ["identifier"]  = "naam",
+        ["text"]        = "tekst",
+        ["section"]     = "sectie",
+        ["singular"]    = "naam enkelvoud",
+        ["plural"]      = "naam meervoud",
+        ["matrix"]      = "n*m",
+        ["see"]         = "zie",
+        ["inherits"]    = "erft van",
+        ["optional"]    = "optioneel",
+        ["displaymath"] = "formule",
+        ["index"]       = "ingang",
+        ["math"]        = "formule",
+        ["nothing"]     = "leeg",
+        ["file"]        = "file",
+        ["position"]    = "positie",
+        ["reference"]   = "verwijzing",
+        ["csname"]      = "naam",
+        ["destination"] = "bestemming",
+        ["triplet"]     = "triplet",
+        ["word"]        = "woord",
+        ["content"]     = "tekst",
+    },
+
+    en = {
+        ["title"]       = "setup",
+        ["formula"]     = "formula",
+        ["number"]      = "number",
+        ["list"]        = "list",
+        ["dimension"]   = "dimension",
+        ["mark"]        = "mark",
+        ["reference"]   = "reference",
+        ["command"]     = "command",
+        ["file"]        = "file",
+        ["name"]        = "name",
+        ["identifier"]  = "identifier",
+        ["text"]        = "text",
+        ["section"]     = "section",
+        ["singular"]    = "singular name",
+        ["plural"]      = "plural name",
+        ["matrix"]      = "n*m",
+        ["see"]         = "see",
+        ["inherits"]    = "inherits from",
+        ["optional"]    = "optional",
+        ["displaymath"] = "formula",
+        ["index"]       = "entry",
+        ["math"]        = "formula",
+        ["nothing"]     = "empty",
+        ["file"]        = "file",
+        ["position"]    = "position",
+        ["reference"]   = "reference",
+        ["csname"]      = "name",
+        ["destination"] = "destination",
+        ["triplet"]     = "triplet",
+        ["word"]        = "word",
+        ["content"]     = "text",
+
+        ["noargument"]     = "\\cs",
+        ["oneargument"]    = "\\cs#1{..}",
+        ["twoarguments"]   = "\\cs#1#2{..}{..}",
+        ["threearguments"] = "\\cs#1#2#3{..}{..}{..}",
+
+    },
+
+    de = {
+        ["title"]       = "Setup",
+        ["formula"]     = "Formel",
+        ["number"]      = "Nummer",
+        ["list"]        = "Liste",
+        ["dimension"]   = "Dimension",
+        ["mark"]        = "Beschriftung",
+        ["reference"]   = "Referenz",
+        ["command"]     = "Befehl",
+        ["file"]        = "Datei",
+        ["name"]        = "Name",
+        ["identifier"]  = "Name",
+        ["text"]        = "Text",
+        ["section"]     = "Abschnitt",
+        ["singular"]    = "singular",
+        ["plural"]      = "plural",
+        ["matrix"]      = "n*m",
+        ["see"]         = "siehe",
+        ["inherits"]    = "inherits from",
+        ["optional"]    = "optioneel",
+        ["displaymath"] = "formula",
+        ["index"]       = "entry",
+        ["math"]        = "formula",
+        ["nothing"]     = "empty",
+        ["file"]        = "file",
+        ["position"]    = "position",
+        ["reference"]   = "reference",
+        ["csname"]      = "name",
+        ["destination"] = "destination",
+        ["triplet"]     = "triplet",
+        ["word"]        = "word",
+        ["content"]     = "text",
+    },
+
+    cz = {
+        ["title"]       = "setup",
+        ["formula"]     = "rovnice",
+        ["number"]      = "cislo",
+        ["list"]        = "seznam",
+        ["dimension"]   = "dimenze",
+        ["mark"]        = "znacka",
+        ["reference"]   = "reference",
+        ["command"]     = "prikaz",
+        ["file"]        = "soubor",
+        ["name"]        = "jmeno",
+        ["identifier"]  = "jmeno",
+        ["text"]        = "text",
+        ["section"]     = "sekce",
+        ["singular"]    = "jmeno v singularu",
+        ["plural"]      = "jmeno v pluralu",
+        ["matrix"]      = "n*m",
+        ["see"]         = "viz",
+        ["inherits"]    = "inherits from",
+        ["optional"]    = "optioneel",
+        ["displaymath"] = "formula",
+        ["index"]       = "entry",
+        ["math"]        = "formula",
+        ["nothing"]     = "empty",
+        ["file"]        = "file",
+        ["position"]    = "position",
+        ["reference"]   = "reference",
+        ["csname"]      = "name",
+        ["destination"] = "destination",
+        ["triplet"]     = "triplet",
+        ["word"]        = "word",
+        ["content"]     = "text",
+    },
+
+    it = {
+        ["title"]       = "setup",
+        ["formula"]     = "formula",
+        ["number"]      = "number",
+        ["list"]        = "list",
+        ["dimension"]   = "dimension",
+        ["mark"]        = "mark",
+        ["reference"]   = "reference",
+        ["command"]     = "command",
+        ["file"]        = "file",
+        ["name"]        = "name",
+        ["identifier"]  = "name",
+        ["text"]        = "text",
+        ["section"]     = "section",
+        ["singular"]    = "singular name",
+        ["plural"]      = "plural name",
+        ["matrix"]      = "n*m",
+        ["see"]         = "see",
+        ["inherits"]    = "inherits from",
+        ["optional"]    = "optioneel",
+        ["displaymath"] = "formula",
+        ["index"]       = "entry",
+        ["math"]        = "formula",
+        ["nothing"]     = "empty",
+        ["file"]        = "file",
+        ["position"]    = "position",
+        ["reference"]   = "reference",
+        ["csname"]      = "name",
+        ["destination"] = "destination",
+        ["triplet"]     = "triplet",
+        ["word"]        = "word",
+        ["content"]     = "text",
+    },
+
+    ro = {
+        ["title"]       = "setari",
+        ["formula"]     = "formula",
+        ["number"]      = "numar",
+        ["list"]        = "lista",
+        ["dimension"]   = "dimensiune",
+        ["mark"]        = "marcaj",
+        ["reference"]   = "referinta",
+        ["command"]     = "comanda",
+        ["file"]        = "fisier",
+        ["name"]        = "nume",
+        ["identifier"]  = "nume",
+        ["text"]        = "text",
+        ["section"]     = "sectiune",
+        ["singular"]    = "nume singular",
+        ["plural"]      = "nume pluram",
+        ["matrix"]      = "n*m",
+        ["see"]         = "vezi",
+        ["inherits"]    = "inherits from",
+        ["optional"]    = "optioneel",
+        ["displaymath"] = "formula",
+        ["index"]       = "entry",
+        ["math"]        = "formula",
+        ["nothing"]     = "empty",
+        ["file"]        = "file",
+        ["position"]    = "position",
+        ["reference"]   = "reference",
+        ["csname"]      = "name",
+        ["destination"] = "destination",
+        ["triplet"]     = "triplet",
+        ["word"]        = "word",
+        ["content"]     = "text",
+    },
+
+    fr = {
+        ["title"]       = "réglage",
+        ["formula"]     = "formule",
+        ["number"]      = "numéro",
+        ["list"]        = "liste",
+        ["dimension"]   = "dimension",
+        ["mark"]        = "marquage",
+        ["reference"]   = "reference",
+        ["command"]     = "commande",
+        ["file"]        = "fichier",
+        ["name"]        = "nom",
+        ["identifier"]  = "identificateur",
+        ["text"]        = "texte",
+        ["section"]     = "section",
+        ["singular"]    = "nom singulier",
+        ["plural"]      = "nom pluriel",
+        ["matrix"]      = "n*m",
+        ["see"]         = "vois",
+        ["inherits"]    = "herite de",
+        ["optional"]    = "optionel",
+        ["displaymath"] = "formule",
+        ["index"]       = "entrée",
+        ["math"]        = "formule",
+        ["nothing"]     = "vide",
+        ["file"]        = "fichier",
+        ["position"]    = "position",
+        ["reference"]   = "réference",
+        ["csname"]      = "nom",
+        ["destination"] = "destination",
+        ["triplet"]     = "triplet",
+        ["word"]        = "mot",
+        ["content"]     = "texte",
+    }
+
+}
+
+document.setups.formats = {
+    open_command    = { [[\%s]], [[context.%s (]] },
+    close_command   = { [[]], [[ )]] },
+    connector       = { [[]], [[, ]] },
+    href_in_list    = { [[<a href='mtx-server-ctx-help.lua?command=%s&mode=%s'>%s</a>]], [[<a href='mtx-server-ctx-help.lua?command=%s&mode=%s'>%s</a>]] },
+    href_as_command = { [[<a href='mtx-server-ctx-help.lua?command=%s&mode=%s'>\%s</a>]], [[<a href='mtx-server-ctx-help.lua?command=%s&mode=%s'>context.%s</a>]] },
+    interface       = [[<a href='mtx-server-ctx-help.lua?interface=%s&mode=%s'>%s</a>]],
+    source          = [[<a href='mtx-server-ctx-help.lua?source=%s&mode=%s'>%s</a>]],
+    modes           = { [[<a href='mtx-server-ctx-help.lua?mode=2'>lua mode</a>]], [[<a href='mtx-server-ctx-help.lua?mode=1'>tex mode</a>]] },
+    optional_single = { "[optional string %s]", "{optional string %s}" },
+    optional_list   = { "[optional list %s]", "{optional table %s}" } ,
+    mandate_single  = { "[mandate string %s]", "{mandate string %s}" },
+    mandate_list    = { "[mandate list %s]", "{mandate list %s}" },
+    parameter       = [[<tr><td width='15%%'>%s</td><td width='15%%'>%s</td><td width='70%%'>%s</td></tr>]],
+    parameters      = [[<table width='100%%'>%s</table>]],
+    listing         = [[<pre><t>%s</t></listing>]],
+    special         = [[<i>%s</i>]],
+    default         = [[<u>%s</u>]],
+}
+
+local function translate(tag,int,noformat)
+    local t = document.setups.translations
+    local te = t["en"]
+    local ti = t[int] or te
+    if noformat then
+        return ti[tag] or te[tag] or tag
+    else
+        return format(document.setups.formats.special,ti[tag] or te[tag] or tag)
+    end
+end
+
+local function translated(e,int)
+    local attributes = e.at
+    local s = attributes.type or "?"
+    local tag = s:match("^cd:(.*)$")
+    if attributes.default == "yes" then
+        return format(document.setups.formats.default,tag or "?")
+    elseif tag then
+        return translate(tag,int)
+    else
+        return s
+    end
+end
+
+document.setups.loaded = document.setups.loaded or { }
+
+document.setups.current = { }
+document.setups.showsources = true
+document.setups.mode = 1
+
+function document.setups.load(filename)
+    filename = resolvers.find_file(filename) or ""
+    if filename ~= "" then
+        local current = document.setups.loaded[filename]
+        if not current then
+            local loaded = xml.load(filename)
+            if loaded then
+                -- xml.inject(document.setups.root,"/",loaded)
+                current = {
+                    file = filename,
+                    root = loaded,
+                    names = { },
+                    used = { },
+                }
+                document.setups.loaded[filename] = current
+            end
+        end
+        document.setups.current = current or { }
+    end
+end
+
+function document.setups.name(ek)
+    local at = ek.at
+    local name = at.name
+    if at.type == 'environment' then
+        name = "start" .. name
+    end
+    if at.variant then
+        name = name .. ":" .. at.variant
+    end
+    if at.generated == "yes" then
+        name = name .. "*"
+    end
+    return name:lower()
+end
+
+function document.setups.csname(ek,int)
+    local cs = ""
+    local at = ek.at or { }
+    if at.type == 'environment' then
+        cs = translate("start",int,true) .. cs
+    end
+    for e in xml.collected(ek,'cd:sequence/(cd:string|variable)') do
+        if e.tg == "string" then
+            cs = cs .. e.at.value
+        else
+            cs = cs .. e.at.value -- to be translated
+        end
+    end
+    return cs
+end
+
+function document.setups.names()
+    local current = document.setups.current
+    local names = current.names
+    if not names or #names == 0 then
+        names = { }
+        local name = document.setups.name
+        local csname = document.setups.csname
+        for e in xml.collected(current.root,'cd:command') do
+            names[#names+1] = { e.at.name, csname(e,int) }
+        end
+        table.sort(names, function(a,b) return a[2]:lower() < b[2]:lower() end)
+        current.names = names
+    end
+    return names
+end
+
+function document.setups.show(name)
+    local current = document.setups.current
+    if current.root then
+        local name = name:gsub("[<>]","")
+        local setup = xml.first(current.root,"cd:command[@name='" .. name .. "']")
+        current.used[#current.used+1] = setup
+        xml.sprint(setup)
+    end
+end
+
+function document.setups.showused()
+    local current = document.setups.current
+    if current.root and next(current.used) then
+        local sorted = table.sortedkeys(current.used)
+        for i=1,#sorted do
+            xml.sprint(current.used[sorted[i]])
+        end
+    end
+end
+function document.setups.showall()
+    local current = document.setups.current
+    if current.root then
+        local list = { }
+        for e in xml.collected(current.root,"cd:command") do
+            list[document.setups.name(e)] = e
+        end
+        local sorted = table.sortedkeys(list)
+        for i=1,#sorted do
+            xml.sprint(list[sorted[i]])
+        end
+    end
+end
+function document.setups.resolve(name)
+    local current = document.setups.current
+    if current.root then
+        local e = xml.filter(current.root,format("cd:define[@name='%s']/text()",name))
+        if e then
+            xml.sprint(e)
+        end
+    end
+end
+
+function document.setups.collect(name,int,lastmode)
+    local current = document.setups.current
+    local formats = document.setups.formats
+    local command = xml.filter(current.root,format("cd:command[@name='%s']/first()",name))
+    if command then
+        local attributes = command.at or { }
+        local data = {
+            command = command,
+            category = attributes.category or "",
+        }
+        if document.setups.showsources then
+            data.source = (attributes.file and formats.source:format(attributes.file,lastmode,attributes.file)) or ""
+        else
+            data.source = attributes.file or ""
+        end
+        local n, sequence, tags = 0, { }, { }
+        sequence[#sequence+1] = formats.open_command[lastmode]:format(document.setups.csname(command,int))
+        local arguments, tag = { }, ""
+        for r, d, k in xml.elements(command,"(cd:keywords|cd:assignments)") do
+            n = n + 1
+            local attributes = d[k].at
+            if #sequence > 1 then
+                local c = formats.connector[lastmode]
+                if c ~= "" then
+                    sequence[#sequence+1] = c
+                end
+            end
+            if attributes.optional == 'yes' then
+                if attributes.list == 'yes' then
+                    tag = formats.optional_list[lastmode]:format(n)
+                else
+                    tag = formats.optional_single[lastmode]:format(n)
+                end
+            else
+                if attributes.list == 'yes' then
+                    tag = formats.mandate_list[lastmode]:format(n)
+                else
+                    tag = formats.mandate_single[lastmode]:format(n)
+                end
+            end
+            sequence[#sequence+1] = tag
+            tags[#tags+1] = tag
+        end
+        sequence[#sequence+1] = formats.close_command[lastmode]
+        data.sequence = concat(sequence, " ")
+        local parameters, n = { }, 0
+        for r, d, k in xml.elements(command,"(cd:keywords|cd:assignments)") do
+            n = n + 1
+            if d[k].tg == "keywords" then
+                local left = tags[n]
+                local right = { }
+                for r, d, k in xml.elements(d[k],"(cd:constant|cd:resolve)") do
+                    local tag = d[k].tg
+                    if tag == "resolve" then
+                        local name = d[k].at.name or ""
+                        if name ~= "" then
+                            local resolved = xml.filter(current.root,format("cd:define[@name='%s']",name))
+                            for r, d, k in xml.elements(resolved,"cd:constant") do
+                                right[#right+1] = translated(d[k],int)
+                            end
+                        end
+                    else
+                        right[#right+1] = translated(d[k],int)
+                    end
+                end
+                parameters[#parameters+1] = formats.parameter:format(left,"",concat(right, ", "))
+            else
+                local what = tags[n]
+                for r, d, k in xml.elements(d[k],"(cd:parameter|cd:inherit)") do
+                    local tag = d[k].tg
+                    local left, right = d[k].at.name or "?", { }
+                    if tag == "inherit" then
+                        local name = d[k].at.name or "?"
+                        local goto = document.setups.formats.href_as_command[lastmode]:format(name,lastmode,name)
+                        if #parameters > 0 and not parameters[#parameters]:find("<br/>") then
+                            parameters[#parameters+1] = formats.parameter:format("<br/>","","")
+                        end
+                        parameters[#parameters+1] = formats.parameter:format(what,formats.special:format(translate("inherits",int)),goto)
+                    else
+                        for r, d, k in xml.elements(d[k],"(cd:constant|cd:resolve)") do
+                            local tag = d[k].tg
+                            if tag == "resolve" then
+                                local name = d[k].at.name or ""
+                                if name ~= "" then
+                                    local resolved = xml.filter(current.root,format("cd:define[@name='%s']",name))
+                                    for r, d, k in xml.elements(resolved,"cd:constant") do
+                                        right[#right+1] = translated(d[k],int)
+                                    end
+                                end
+                            else
+                                right[#right+1] = translated(d[k],int)
+                            end
+                        end
+                        parameters[#parameters+1] = formats.parameter:format(what,left,concat(right, ", "))
+                    end
+                    what = ""
+                end
+            end
+            parameters[#parameters+1] = formats.parameter:format("<br/>","","")
+        end
+        data.parameters = parameters or { }
+        data.mode = formats.modes[lastmode or 1]
+        return data
+    else
+        return nil
+    end
+end
+
+-- -- --
+
+tex = tex or { }
+
+-- -- --
+
+local interfaces = {
+    czech    = 'cz',
+    dutch    = 'nl',
+    english  = 'en',
+    french   = 'fr',
+    german   = 'de',
+    italian  = 'it',
+    persian  = 'pe',
+    romanian = 'ro',
+}
+
+local lastinterface, lastcommand, lastsource, lastmode = "en", "", "", 1
+
+local variables = {
+    ['color-background-main-left']  = '#3F3F3F',
+    ['color-background-main-right'] = '#5F5F5F',
+    ['color-background-one']        = lmx.get('color-background-green'),
+    ['color-background-two']        = lmx.get('color-background-blue'),
+    ['title']                       = 'ConTeXt Help Information',
+}
+
+--~ function lmx.loadedfile(filename)
+--~     return io.loaddata(resolvers.find_file(filename)) -- return resolvers.texdatablob(filename)
+--~ end
+
+local function doit(configuration,filename,hashed)
+
+    local formats = document.setups.formats
+
+    local start = os.clock()
+
+    local detail = url.query(hashed.query or "")
+
+    lastinterface = detail.interface or lastinterface
+    lastcommand   = detail.command or lastcommand
+    lastsource    = detail.source or lastsource
+    lastmode      = tonumber(detail.mode or lastmode) or 1
+
+    if lastinterface then
+        logs.simple("checking interface: %s",lastinterface)
+        document.setups.load(format("cont-%s.xml",lastinterface))
+    end
+
+    local div = document.setups.div[lastinterface]
+    local span = document.setups.span[lastinterface]
+
+    local result = { content = "error" }
+
+    local names, refs, ints = document.setups.names(lastinterface), { }, { }
+    for k=1,#names do
+        local v = names[k]
+        refs[k] = formats.href_in_list[lastmode]:format(v[1],lastmode,v[2])
+    end
+    if lastmode ~= 2 then
+        local sorted = table.sortedkeys(interfaces)
+        for k=1,#sorted do
+            local v = sorted[k]
+            ints[k] = formats.interface:format(interfaces[v],lastmode,v)
+        end
+    end
+
+    local n = concat(refs,"<br/>")
+    local i = concat(ints,"<br/><br/>")
+
+    if div then
+        variables.names      = div:format(n)
+        variables.interfaces = div:format(i)
+    else
+        variables.names      = n
+        variables.interfaces = i
+    end
+
+    -- first we need to add information about mkii/mkiv
+
+    variables.maintitle = "no definition"
+    variables.maintext  = ""
+    variables.extra     = ""
+
+    if document.setups.showsources and lastsource and lastsource ~= "" then
+        -- todo: mkii, mkiv, tex (can be different)
+        local data = io.loaddata(resolvers.find_file(lastsource))
+        variables.maintitle = lastsource
+        variables.maintext  = formats.listing:format(data)
+        lastsource = ""
+    elseif lastcommand and lastcommand ~= "" then
+        local data = document.setups.collect(lastcommand,lastinterface,lastmode)
+        if data then
+            local what, extra = { "environment", "category", "source", "mode" }, { }
+            for k=1,#what do
+                local v = what[k]
+                if data[v] and data[v] ~= "" then
+                    lmx.set(v, data[v])
+                    extra[#extra+1] = v .. ": " .. data[v]
+                end
+            end
+            variables.maintitle = data.sequence
+            variables.maintext  = formats.parameters:format(concat(data.parameters))
+            variables.extra     = concat(extra,"&nbsp;&nbsp;&nbsp;")
+        else
+            variables.maintext = "select command"
+        end
+    end
+
+    local content = lmx.convert('context-help.lmx',false,variables)
+
+    logs.simple("time spent on page: %0.03f seconds",os.clock()-start)
+
+    return { content = content }
+end
+
+return doit, true
diff --git a/scripts/context/lua/mtx-server-ctx-startup.lua b/scripts/context/lua/mtx-server-ctx-startup.lua
new file mode 100644
index 000000000..59536c36c
--- /dev/null
+++ b/scripts/context/lua/mtx-server-ctx-startup.lua
@@ -0,0 +1,39 @@
+if not modules then modules = { } end modules ['mtx-server-ctx-startup'] = {
+    version   = 1.001,
+    comment   = "Overview Of Goodies",
+    author    = "Hans Hagen",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+dofile(resolvers.find_file("trac-lmx.lua","tex"))
+
+function doit(configuration,filename,hashed)
+
+    local list = { }
+    local root = file.dirname(resolvers.find_file("mtx-server.lua") or ".")
+    if root == "" then root = "." end
+    local pattern = root .. "/mtx-server-ctx-*.lua"
+    local files = dir.glob(pattern)
+    for i=1,#files do
+        local filename = file.basename(files[i])
+        local name = string.match(filename,"mtx%-server%-ctx%-(.-)%.lua$")
+        if name and name ~= "startup" then
+            list[#list+1] = string.format("<a href='%s' target='ctx-%s'>%s</a><br/><br/>",filename,name,name)
+        end
+    end
+
+    local variables = {
+        ['color-background-one']    = lmx.get('color-background-green'),
+        ['color-background-two']    = lmx.get('color-background-blue'),
+        ['title']                   = "Overview Of Goodies",
+        ['color-background-one']    = lmx.get('color-background-green'),
+        ['color-background-two']    = lmx.get('color-background-blue'),
+        ['maintext']                = table.concat(list,"\n"),
+    }
+
+    return  { content = lmx.convert('context-base.lmx',false,variables) }
+
+end
+
+return doit, true
diff --git a/scripts/context/lua/mtx-server.lua b/scripts/context/lua/mtx-server.lua
new file mode 100644
index 000000000..dc0befcaa
--- /dev/null
+++ b/scripts/context/lua/mtx-server.lua
@@ -0,0 +1,361 @@
+if not modules then modules = { } end modules ['mtx-server'] = {
+    version   = 1.001,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen & Taco Hoekwater",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+scripts           = scripts           or { }
+scripts.webserver = scripts.webserver or { }
+
+dofile(resolvers.find_file("l-url.lua","tex"))
+dofile(resolvers.find_file("luat-soc.lua","tex"))
+
+local socket = socket or require("socket") -- redundant in future version
+local format = string.format
+
+-- The following two lists are taken from webrick (ruby) and
+-- extended with a few extra suffixes.
+
+local mimetypes = {
+    ai    = 'application/postscript',
+    asc   = 'text/plain',
+    avi   = 'video/x-msvideo',
+    bin   = 'application/octet-stream',
+    bmp   = 'image/bmp',
+    bz2   = 'application/x-bzip2',
+    cer   = 'application/pkix-cert',
+    class = 'application/octet-stream',
+    crl   = 'application/pkix-crl',
+    crt   = 'application/x-x509-ca-cert',
+    css   = 'text/css',
+    dms   = 'application/octet-stream',
+    doc   = 'application/msword',
+    dvi   = 'application/x-dvi',
+    eps   = 'application/postscript',
+    etx   = 'text/x-setext',
+    exe   = 'application/octet-stream',
+    gif   = 'image/gif',
+    gz    = 'application/x-tar',
+    hqx   = 'application/mac-binhex40',
+    htm   = 'text/html',
+    html  = 'text/html',
+    jpe   = 'image/jpeg',
+    jpeg  = 'image/jpeg',
+    jpg   = 'image/jpeg',
+    lha   = 'application/octet-stream',
+    lzh   = 'application/octet-stream',
+    mov   = 'video/quicktime',
+    mpe   = 'video/mpeg',
+    mpeg  = 'video/mpeg',
+    mpg   = 'video/mpeg',
+    pbm   = 'image/x-portable-bitmap',
+    pdf   = 'application/pdf',
+    pgm   = 'image/x-portable-graymap',
+    png   = 'image/png',
+    pnm   = 'image/x-portable-anymap',
+    ppm   = 'image/x-portable-pixmap',
+    ppt   = 'application/vnd.ms-powerpoint',
+    ps    = 'application/postscript',
+    qt    = 'video/quicktime',
+    ras   = 'image/x-cmu-raster',
+    rb    = 'text/plain',
+    rd    = 'text/plain',
+    rgb   = 'image/x-rgb',
+    rtf   = 'application/rtf',
+    sgm   = 'text/sgml',
+    sgml  = 'text/sgml',
+    snd   = 'audio/basic',
+    tar   = 'application/x-tar',
+    tgz   = 'application/x-tar',
+    tif   = 'image/tiff',
+    tiff  = 'image/tiff',
+    txt   = 'text/plain',
+    xbm   = 'image/x-xbitmap',
+    xls   = 'application/vnd.ms-excel',
+    xml   = 'text/xml',
+    xpm   = 'image/x-xpixmap',
+    xwd   = 'image/x-xwindowdump',
+    zip   = 'application/zip',
+}
+
+local messages = {
+    [100] = 'Continue',
+    [101] = 'Switching Protocols',
+    [200] = 'OK',
+    [201] = 'Created',
+    [202] = 'Accepted',
+    [203] = 'Non-Authoritative Information',
+    [204] = 'No Content',
+    [205] = 'Reset Content',
+    [206] = 'Partial Content',
+    [300] = 'Multiple Choices',
+    [301] = 'Moved Permanently',
+    [302] = 'Found',
+    [303] = 'See Other',
+    [304] = 'Not Modified',
+    [305] = 'Use Proxy',
+    [307] = 'Temporary Redirect',
+    [400] = 'Bad Request',
+    [401] = 'Unauthorized',
+    [402] = 'Payment Required',
+    [403] = 'Forbidden',
+    [404] = 'Not Found',
+    [405] = 'Method Not Allowed',
+    [406] = 'Not Acceptable',
+    [407] = 'Proxy Authentication Required',
+    [408] = 'Request Timeout',
+    [409] = 'Conflict',
+    [410] = 'Gone',
+    [411] = 'Length Required',
+    [412] = 'Precondition Failed',
+    [413] = 'Request Entity Too Large',
+    [414] = 'Request-URI Too Large',
+    [415] = 'Unsupported Media Type',
+    [416] = 'Request Range Not Satisfiable',
+    [417] = 'Expectation Failed',
+    [500] = 'Internal Server Error',
+    [501] = 'Not Implemented',
+    [502] = 'Bad Gateway',
+    [503] = 'Service Unavailable',
+    [504] = 'Gateway Timeout',
+    [505] = 'HTTP Version Not Supported',
+}
+
+local handlers = { }
+
+local function errormessage(client,configuration,n)
+    local data = format("<head><title>%s %s</title></head><html><h2>%s %s</h2></html>",n,messages[n],n,messages[n])
+    logs.simple("handling error %s: %s",n,messages[n])
+    handlers.generic(client,configuration,data,nil,true)
+end
+
+local validpaths, registered = { }, { }
+
+function scripts.webserver.registerpath(name)
+    if not registered[name] then
+        local cleanname = string.gsub(name,"%.%.","deleted-parent")
+        logs.simple("registering path '%s'",cleanname)
+        validpaths[#validpaths+1] = cleanname
+        registered[name] = true
+    end
+end
+
+function handlers.generic(client,configuration,data,suffix,iscontent)
+    if not iscontent then
+        local name = data
+        logs.simple("requested file '%s'",name)
+        local fullname = file.join(configuration.root,name)
+        data = io.loaddata(fullname) or ""
+        if data == "" then
+            for n=1,#validpaths do
+                local fullname = file.join(validpaths[n],name)
+                data = io.loaddata(fullname) or ""
+                if data ~= "" then
+                    logs.simple("sending generic file '%s'",fullname)
+                    break
+                end
+            end
+        else
+            logs.simple("sending generic file '%s'",fullname)
+        end
+    end
+    if data and data ~= "" then
+        client:send("HTTP/1.1 200 OK\r\n")
+        client:send("Connection: close\r\n")
+        client:send(format("Content-Length: %s\r\n",#data))
+        client:send(format("Content-Type: %s\r\n",(suffix and mimetypes[suffix]) or "text/html"))
+        client:send("\r\n")
+        client:send(data)
+        client:send("\r\n")
+    else
+        errormessage(client,configuration,404)
+    end
+end
+
+--~ return os.date()
+
+--~ return { content = "crap" }
+
+--~ return function(configuration,filename)
+--~     return { content = filename }
+--~ end
+
+local loaded = { }
+
+function handlers.lua(client,configuration,filename,suffix,iscontent,hashed) -- filename will disappear, and become hashed.filename
+    local filename = file.join(configuration.scripts,filename)
+    if not file.is_qualified_path(filename) then
+        filename = file.join(configuration.root,filename)
+    end
+    -- todo: split url in components, see l-url; rather trivial
+    local result, keep = loaded[filename], false
+    if result then
+        logs.simple("reusing script: %s",filename)
+    else
+        logs.simple("locating script: %s",filename)
+        if lfs.isfile(filename) then
+            logs.simple("loading script: %s",filename)
+            result = loadfile(filename)
+            logs.simple("return type: %s",type(result))
+            if result and type(result) == "function" then
+             -- result() should return a table { [type=,] [length=,] content= }, function or string
+                result, keep = result()
+                if keep then
+                    logs.simple("saving script: %s",type(result))
+                    loaded[filename] = result
+                end
+            end
+        else
+            errormessage(client,configuration,404)
+        end
+    end
+    if result then
+        if type(result) == "function" then
+            result = result(configuration,filename,hashed) -- second argument will become query
+        end
+        if result and type(result) == "string" then
+            result = { content = result }
+        end
+        if result and type(result) == "table" then
+            if result.content then
+                local suffix = result.type or "text/html"
+                local action = handlers[suffix] or handlers.generic
+                action(client,configuration,result.content,suffix,true) -- content
+            elseif result.filename then
+                local suffix = file.extname(result.filename) or "text/html"
+                local action = handlers[suffix] or handlers.generic
+                action(client,configuration,result.filename,suffix,false) -- filename
+            else
+                errormessage(client,configuration,404)
+            end
+        else
+            errormessage(client,configuration,500)
+        end
+    else
+        errormessage(client,configuration,404)
+    end
+end
+
+handlers.luc  = handlers.lua
+handlers.html = handlers.htm
+
+local indices    = { "index.htm", "index.html" }
+local portnumber = 31415 -- pi suits tex
+
+function scripts.webserver.run(configuration)
+    -- check configuration
+    configuration.port = tonumber(configuration.port or os.getenv("MTX_SERVER_PORT") or portnumber) or portnumber
+    if not configuration.root or not lfs.isdir(configuration.root) then
+        configuration.root = os.getenv("MTX_SERVER_ROOT") or "."
+    end
+    -- locate root and index file in tex tree
+    if not lfs.isdir(configuration.root) then
+        for i=1,#indices do
+            local name = indices[i]
+            local root = resolvers.resolve("path:" .. name) or ""
+            if root ~= "" then
+                configuration.root = root
+                configuration.index = configuration.index or name
+                break
+            end
+        end
+    end
+    configuration.root = dir.expand_name(configuration.root)
+    if not configuration.index then
+        for i=1,#indices do
+            local name = indices[i]
+            if lfs.isfile(file.join(configuration.root,name)) then
+                configuration.index = name -- we will prepend the rootpath later
+                break
+            end
+        end
+        configuration.index = configuration.index or "unknown"
+    end
+    if not configuration.scripts or configuration.scripts == "" then
+        configuration.scripts = dir.expand_name(file.join(configuration.root or ".",configuration.scripts or "."))
+    end
+    -- so far for checks
+    logs.simple("running at port: %s",configuration.port)
+    logs.simple("document root: %s",configuration.root or resolvers.ownpath)
+    logs.simple("main index file: %s",configuration.index)
+    logs.simple("scripts subpath: %s",configuration.scripts)
+    logs.simple("context services: http://localhost:%s/mtx-server-ctx-startup.lua",configuration.port)
+    local server = assert(socket.bind("*", configuration.port))
+--~ local reading = { server }
+    while true do -- no multiple clients
+        local start = os.clock()
+--~ local input = socket.select(reading)
+--~ local client = input:accept()
+        local client = server:accept()
+        client:settimeout(configuration.timeout or 60)
+        local request, e = client:receive()
+        if e then
+            errormessage(client,configuration,404)
+        else
+            local from = client:getpeername()
+            logs.simple("request from: %s",tostring(from))
+            local fullurl = request:match("GET (.+) HTTP/.*$") -- todo: more clever
+            fullurl = socket.url.unescape(fullurl)
+            local hashed = url.hashed(fullurl)
+            local query = url.query(hashed.query)
+            local filename = hashed.path
+            if filename then
+                filename = socket.url.unescape(filename)
+                logs.simple("requested action: %s",filename)
+                if filename:find("%.%.") then
+                    filename = nil -- invalid path
+                end
+                if filename == nil or filename == "" or filename == "/" then
+                    filename = configuration.index
+                    logs.simple("invalid filename, forcing: %s",filename)
+                end
+                local suffix = file.extname(filename)
+                local action = handlers[suffix] or handlers.generic
+                if action then
+                    logs.simple("performing action: %s",filename)
+                    action(client,configuration,filename,suffix,false,hashed) -- filename and no content
+                else
+                    errormessage(client,configuration,404)
+                end
+            else
+                errormessage(client,configuration,404)
+            end
+        end
+        client:close()
+        logs.simple("time spent with client: %0.03f seconds",os.clock()-start)
+    end
+end
+
+logs.extendbanner("Simple Webserver For Helpers 0.10")
+
+messages.help = [[
+--start               start server
+--port                port to listen to
+--root                server root
+--scripts             scripts sub path
+--index               index file
+--auto                start on own path
+]]
+
+if environment.argument("auto") then
+    local path = resolvers.find_file("mtx-server.lua") or "."
+    scripts.webserver.run {
+        port    = environment.argument("port"),
+        root    = environment.argument("root") or file.dirname(path) or ".",
+        scripts = environment.argument("scripts") or file.dirname(path) or ".",
+    }
+elseif environment.argument("start") then
+    scripts.webserver.run {
+        port    = environment.argument("port"),
+        root    = environment.argument("root") or ".",           -- "e:/websites/www.pragma-ade.com",
+        index   = environment.argument("index"),
+        scripts = environment.argument("scripts"),
+    }
+else
+    logs.help(messages.help)
+end
+
+
+-- mtxrun --script server --start => http://localhost:8080/mtx-server-ctx-help.lua
diff --git a/scripts/context/lua/mtx-texworks.lua b/scripts/context/lua/mtx-texworks.lua
new file mode 100644
index 000000000..73ab846cd
--- /dev/null
+++ b/scripts/context/lua/mtx-texworks.lua
@@ -0,0 +1,99 @@
+if not modules then modules = { } end modules ['mtx-texworks'] = {
+    version   = 1.002,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+scripts          = scripts          or { }
+scripts.texworks = scripts.texworks or { }
+
+local texworkspaths = {
+    "completion",
+    "configuration",
+    "dictionaries",
+    "translations",
+    "scripts",
+    "templates",
+    "TUG"
+}
+
+local texworkssignal = "texworks-context.rme"
+local texworkininame = "texworks.ini"
+
+function scripts.texworks.start(indeed)
+    local workname = (os.type == "windows" and "texworks.exe") or "texworks"
+    local fullname = nil
+    local binpaths = file.split_path(os.getenv("PATH")) or file.split_path(os.getenv("path"))
+    local usedsignal = texworkssignal
+    local datapath = resolvers.find_file(usedsignal,"other text files") or ""
+    if datapath ~= "" then
+        datapath  = file.dirname(datapath) -- data
+        if datapath == "" then
+            datapath = resolvers.clean_path(lfs.currentdir())
+        end
+    else
+        usedsignal = texworkininame
+        datapath = resolvers.find_file(usedsignal,"other text files") or ""
+        if datapath == "" then
+            usedsignal = string.lower(usedsignal)
+            datapath = resolvers.find_file(usedsignal,"other text files") or ""
+        end
+        if datapath ~= "" and lfs.isfile(datapath) then
+            datapath  = file.dirname(datapath) -- TUG
+            datapath  = file.dirname(datapath) -- data
+            if datapath == "" then
+                datapath = resolvers.clean_path(lfs.currentdir())
+            end
+        end
+    end
+    if datapath == "" then
+        logs.simple("invalid datapath, maybe you need to regenerate the file database")
+        return false
+    end
+    if not binpaths or #binpaths == 0 then
+        logs.simple("invalid binpath")
+        return false
+    end
+    for i=1,#binpaths do
+        local p = file.join(binpaths[i],workname)
+        if lfs.isfile(p) and lfs.attributes(p,"size") > 10000 then -- avoind stub
+            fullname = p
+            break
+        end
+    end
+    if not fullname then
+        logs.simple("unable to locate %s",workname)
+        return false
+    end
+    for i=1,#texworkspaths do
+        dir.makedirs(file.join(datapath,texworkspaths[i]))
+    end
+    os.setenv("TW_INIPATH",datapath)
+    os.setenv("TW_LIBPATH",datapath)
+    if not indeed or environment.argument("verbose") then
+        logs.simple("used signal: %s", usedsignal)
+        logs.simple("data path  : %s", datapath)
+        logs.simple("full name  : %s", fullname)
+        logs.simple("set paths  : TW_INIPATH TW_LIBPATH")
+    end
+    if indeed then
+        os.launch(fullname)
+    end
+end
+
+logs.extendbanner("TeXworks Startup Script 1.00",true)
+
+messages.help = [[
+--start [--verbose]   start texworks
+--test                report what will happen
+]]
+
+if environment.argument("start") then
+    scripts.texworks.start(true)
+elseif environment.argument("test") then
+    scripts.texworks.start()
+else
+    logs.help(messages.help)
+end
diff --git a/scripts/context/lua/mtx-timing.lua b/scripts/context/lua/mtx-timing.lua
new file mode 100644
index 000000000..40e33cdae
--- /dev/null
+++ b/scripts/context/lua/mtx-timing.lua
@@ -0,0 +1,193 @@
+if not modules then modules = { } end modules ['mtx-timing'] = {
+    version   = 1.002,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, gsub, concat = string.format, string.gsub, table.concat
+
+dofile(resolvers.find_file("trac-tim.lua","tex"))
+dofile(resolvers.find_file("trac-lmx.lua","tex"))
+
+local meta = [[
+    beginfig(%s) ;
+        begingroup ;
+            save p, q, b, h, w ;
+            path p, q, b ; numeric h, w ;
+            linecap := butt ;
+            h := 100 ;
+            w := 800pt ;
+            p := %s ;
+            q := %s ;
+            p := p shifted -llcorner p ;
+            q := q shifted -llcorner q ;
+            q := q xstretched w ;
+            p := p xstretched w ;
+            b := boundingbox (llcorner p -- llcorner p shifted (w,h)) ;
+            draw b withcolor white withpen pencircle scaled 4pt ;
+            draw p withcolor red   withpen pencircle scaled 4pt ;
+            draw q withcolor blue  withpen pencircle scaled 2pt ;
+        endgroup ;
+    endfig ;
+]]
+
+local html_graphic = [[
+    <h1><a name='graphic-%s'>%s (red) %s (blue)</a></h1>
+    <table>
+        <tr>
+            <td>%s</td>
+            <td valign='top'>
+                &nbsp;&nbsp;min: %s<br/>
+                &nbsp;&nbsp;max: %s<br/>
+                &nbsp;&nbsp;pages: %s<br/>
+                &nbsp;&nbsp;average: %s<br/>
+            </td>
+        </tr>
+    </table>
+    <br/>
+]]
+
+local html_menu = [[
+    <a href='#graphic-%s'>%s</a>
+]]
+
+local directrun = true
+
+local what = { "parameters", "nodes" }
+
+function plugins.progress.make_svg(filename,other)
+    local metadata, menudata, c = { }, { }, 0
+    metadata[#metadata+1] = 'outputformat := "svg" ;'
+    for i=1,#what do
+        local kind, mdk = what[i], { }
+        menudata[kind] = mdk
+        for n, name in next, plugins.progress[kind](filename) do
+            local first = plugins.progress.path(filename,name)
+            local second = plugins.progress.path(filename,other)
+            c = c + 1
+            metadata[#metadata+1] = format(meta,c,first,second)
+            mdk[#mdk+1] = { name, c }
+        end
+    end
+    metadata[#metadata+1] = "end ."
+    metadata = concat(metadata,"\n\n")
+    if directrun then
+        dofile(resolvers.find_file("mlib-run.lua","tex"))
+        commands = commands or { }
+        commands.writestatus = logs.report
+        local result = metapost.directrun("metafun","timing data","svg",true,metadata)
+        return menudata, result
+    else
+        local mpname = file.replacesuffix(filename,"mp")
+        io.savedata(mpname,metadata)
+        os.execute(format("mpost --progname=context --mem=metafun.mem %s",mpname))
+        os.remove(mpname)
+        os.remove(file.removesuffix(filename).."-mpgraph.mpo") -- brr
+        os.remove(file.removesuffix(filename)..".log") -- brr
+        return menudata
+    end
+end
+
+function plugins.progress.makehtml(filename,other,menudata,metadata)
+    local graphics = { }
+    local result = { graphics = graphics }
+    for i=1,#what do
+        local kind, menu = what[i], { }
+        local md = menudata[kind]
+        result[kind] = menu
+        for k=1,#md do
+            local v = md[k]
+            local name, number = v[1], v[2]
+            local min     = plugins.progress.bot(filename,name)
+            local max     = plugins.progress.top(filename,name)
+            local pages   = plugins.progress.pages(filename)
+            local average = math.round(max/pages)
+            if directrun then
+                local data = metadata[number]
+                menu[#menu+1] = format(html_menu,name,name)
+                graphics[#graphics+1] = format(html_graphic,name,name,other,data,min,max,pages,average)
+            else
+                local mpname = file.replacesuffix(filename,number)
+                local data = io.loaddata(mpname) or ""
+            --  data = gsub(data,"<!%-%-(.-)%-%->[\n\r]*","")
+                data = gsub(data,"<%?xml.->","")
+                menu[#menu+1] = format(html_menu,name,name)
+                graphics[#graphics+1] = format(html_graphic,name,name,other,data,min,max,pages,average)
+                os.remove(mpname)
+            end
+        end
+    end
+    return result
+end
+
+function plugins.progress.valid_file(name)
+    return name and name ~= "" and lfs.isfile(name .. "-luatex-progress.lut")
+end
+
+function plugins.progress.make_lmx_page(name,launch,remove)
+
+    local filename = name .. "-luatex-progress"
+    local other    = "elapsed_time"
+    local template = 'context-timing.lmx'
+
+    plugins.progress.convert(filename)
+
+    local menudata, metadata = plugins.progress.make_svg(filename,other)
+    local htmldata = plugins.progress.makehtml(filename,other,menudata,metadata)
+
+    lmx.htmfile = function(name) return name .. "-timing.xhtml" end
+    lmx.lmxfile = function(name) return resolvers.find_file(name,'tex') end
+
+    local variables = {
+        ['title-default']        = 'ConTeXt Timing Information',
+        ['title']                = format('ConTeXt Timing Information: %s',file.basename(name)),
+        ['parametersmenu']       = concat(htmldata.parameters, "&nbsp;&nbsp;"),
+        ['nodesmenu']            = concat(htmldata.nodes, "&nbsp;&nbsp;"),
+        ['graphics']             = concat(htmldata.graphics, "\n\n"),
+        ['color-background-one'] = lmx.get('color-background-green'),
+        ['color-background-two'] = lmx.get('color-background-blue'),
+    }
+
+    if launch then
+        local htmfile = lmx.show(template,variables)
+        if remove then
+            os.sleep(1) -- give time to launch
+            os.remove(htmfile)
+        end
+    else
+        lmx.make(template,variables)
+    end
+
+end
+
+scripts         = scripts         or { }
+scripts.timings = scripts.timings or { }
+
+function scripts.timings.xhtml(filename)
+    if filename == "" then
+        logs.simple("provide filename")
+    elseif not plugins.progress.valid_file(filename) then
+        logs.simple("first run context again with the --timing option")
+    else
+        local basename = file.removesuffix(filename)
+        local launch   = environment.argument("launch")
+        local remove   = environment.argument("remove")
+        plugins.progress.make_lmx_page(basename,launch,remove)
+    end
+end
+
+logs.extendbanner("ConTeXt Timing Tools 0.10",true)
+
+messages.help = [[
+--xhtml               make xhtml file
+--launch              launch after conversion
+--remove              remove after launching
+]]
+
+if environment.argument("xhtml") then
+    scripts.timings.xhtml(environment.files[1] or "")
+else
+    logs.help(messages.help)
+end
diff --git a/scripts/context/lua/mtx-tools.lua b/scripts/context/lua/mtx-tools.lua
new file mode 100644
index 000000000..bf4add168
--- /dev/null
+++ b/scripts/context/lua/mtx-tools.lua
@@ -0,0 +1,176 @@
+if not modules then modules = { } end modules ['mtx-tools'] = {
+    version   = 1.002,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local find, format, sub, rep, gsub, lower = string.find, string.format, string.sub, string.rep, string.gsub, string.lower
+
+scripts       = scripts       or { }
+scripts.tools = scripts.tools or { }
+
+local bomb_1, bomb_2 = "^\254\255", "^\239\187\191"
+
+function scripts.tools.disarmutfbomb()
+    local force, done = environment.argument("force"), false
+    local files = environment.files
+    for i=1,#files do
+        local name = files[i]
+        if lfs.isfile(name) then
+            local data = io.loaddata(name)
+            if not data then
+                -- just skip
+            elseif find(data,bomb_1) then
+                logs.simple("file '%s' has a 2 character utf bomb",name)
+                if force then
+                    io.savedata(name,(gsub(data,bomb_1,"")))
+                end
+                done = true
+            elseif find(data,bomb_2) then
+                logs.simple("file '%s' has a 3 character utf bomb",name)
+                if force then
+                    io.savedata(name,(gsub(data,bomb_2,"")))
+                end
+                done = true
+            else
+            --  logs.simple("file '%s' has no utf bomb",name)
+            end
+        end
+    end
+    if done and not force then
+        logs.simple("use --force to do a real disarming")
+    end
+end
+
+function scripts.tools.downcase()
+    local pattern = environment.argument('pattern') or "*"
+    local recurse = environment.argument('recurse')
+    local force   = environment.argument('force')
+    local n = 0
+    if recurse and not find(pattern,"^%*%*%/") then
+        pattern = "**/*" .. pattern
+    end
+    dir.glob(pattern,function(name)
+        local basename = file.basename(name)
+        if lower(basename) ~= basename then
+            n = n + 1
+            if force then
+                os.rename(name,lower(name))
+            end
+        end
+    end)
+    if n > 0 then
+        if force then
+            logs.simple("%s files renamed",n)
+        else
+            logs.simple("use --force to do a real rename (%s files involved)",n)
+        end
+    else
+        logs.simple("nothing to do")
+    end
+end
+
+
+function scripts.tools.dirtoxml()
+
+    local join, removesuffix, extname, date = file.join, file.removesuffix, file.extname, os.date
+
+    local xmlns      = "http://www.pragma-ade.com/rlg/xmldir.rng"
+    local timestamp  = "%Y-%m-%d %H:%M"
+
+    local pattern    = environment.argument('pattern') or ".*"
+    local url        = environment.argument('url')     or "no-url"
+    local root       = environment.argument('root')    or "."
+    local outputfile = environment.argument('output')
+
+    local recurse    = environment.argument('recurse')
+    local stripname  = environment.argument('stripname')
+    local longname   = environment.argument('longname')
+
+    local function flush(list,result,n,path)
+        n, result = n or 1, result or { }
+        local d = rep("  ",n)
+        for name, attr in table.sortedhash(list) do
+            local mode = attr.mode
+            if mode == "file" then
+                result[#result+1] = format("%s<file name='%s'>",d,(longname and path and join(path,name)) or name)
+                result[#result+1] = format("%s  <base>%s</base>",d,removesuffix(name))
+                result[#result+1] = format("%s  <type>%s</type>",d,extname(name))
+                result[#result+1] = format("%s  <size>%s</size>",d,attr.size)
+                result[#result+1] = format("%s  <permissions>%s</permissions>",d,sub(attr.permissions,7,9))
+                result[#result+1] = format("%s  <date>%s</date>",d,date(timestamp,attr.modification))
+                result[#result+1] = format("%s</file>",d)
+            elseif mode == "directory" then
+                result[#result+1] = format("%s<directory name='%s'>",d,name)
+                flush(attr.list,result,n+1,(path and join(path,name)) or name)
+                result[#result+1] = format("%s</directory>",d)
+            end
+        end
+    end
+
+    if not pattern or pattern == ""  then
+        logs.report('provide --pattern=')
+        return
+    end
+
+    if stripname then
+        pattern = file.dirname(pattern)
+    end
+
+    local luapattern = string.topattern(pattern,true)
+
+    lfs.chdir(root)
+
+    local list = dir.collect_pattern(root,luapattern,recurse)
+
+    if list[outputfile] then
+        list[outputfile] = nil
+    end
+
+    local result = { "<?xml version='1.0'?>" }
+    result[#result+1] = format("<files url=%q root=%q pattern=%q luapattern=%q xmlns='%s' timestamp='%s'>",url,root,pattern,luapattern,xmlns,date(timestamp))
+    flush(list,result)
+    result[#result+1] = "</files>"
+
+    result = table.concat(result,"\n")
+
+    if not outputfile or outputfile == "" then
+        texio.write_nl(result)
+    else
+        io.savedata(outputfile,result)
+    end
+
+end
+
+logs.extendbanner("Some File Related Goodies 1.01",true)
+
+messages.help = [[
+--disarmutfbomb       remove utf bomb if present
+    --force             remove indeed
+
+--dirtoxml              glob directory into xml
+    --pattern           glob pattern (default: *)
+    --url               url attribute (no processing)
+    --root              the root of the globbed path (default: .)
+    --output            output filename (console by default)
+    --recurse           recurse into subdirecories
+    --stripname         take pathpart of given pattern
+    --longname          set name attributes to full path name
+
+--downcase
+    --pattern           glob pattern (default: *)
+    --recurse           recurse into subdirecories
+    --force             downcase indeed
+]]
+
+if environment.argument("disarmutfbomb") then
+    scripts.tools.disarmutfbomb()
+elseif environment.argument("dirtoxml") then
+    scripts.tools.dirtoxml()
+elseif environment.argument("downcase") then
+    scripts.tools.downcase()
+else
+    logs.help(messages.help)
+end
diff --git a/scripts/context/lua/mtx-unzip.lua b/scripts/context/lua/mtx-unzip.lua
new file mode 100644
index 000000000..f990f4210
--- /dev/null
+++ b/scripts/context/lua/mtx-unzip.lua
@@ -0,0 +1,101 @@
+-- maybe --pattern
+
+logs.extendbanner("Simple Unzipper 0.10")
+
+messages.help = [[
+--list                list files in archive
+--junk                flatten unzipped directory structure
+--extract             extract files
+]]
+
+scripts          = scripts          or { }
+scripts.unzipper = scripts.unzipper or { }
+
+function scripts.unzipper.help()
+    logs.help(messages.help)
+end
+
+function scripts.unzipper.opened()
+    local filename = environment.files[1]
+    if filename and filename ~= "" then
+        filename = file.addsuffix(filename,'zip')
+        local zipfile = zip.open(filename)
+        if zipfile then
+            return zipfile
+        end
+    end
+    logs.report("unzip", "no zip file: " .. filename)
+    return false
+end
+
+function scripts.unzipper.list()
+    local zipfile = scripts.unzipper.opened()
+    if zipfile then
+        local n = 0
+        for k in zipfile:files() do
+            if #k.filename > n then n = #k.filename end
+        end
+        local files, paths, compressed, uncompressed = 0, 0, 0, 0
+        for k in zipfile:files() do
+            if k.filename:find("/$") then
+                paths = paths + 1
+                print(string.format("%s", k.filename:rpadd(n," ")))
+            else
+                files = files + 1
+                local cs, us = k.compressed_size, k.uncompressed_size
+                if cs > compressed then
+                    compressed = cs
+                end
+                if us > uncompressed then
+                    uncompressed = us
+                end
+                print(string.format("%s  % 9i  % 9i", k.filename:rpadd(n," "),cs,us))
+            end
+        end
+        print(string.format("\n%s  % 9i  % 9i", (files .. " files, " .. paths .. " directories"):rpadd(n," "),compressed,uncompressed))
+    end
+end
+
+function zip.loaddata(zipfile,filename)
+    local f = zipfile:open(filename)
+    if f then
+        local data = f:read("*a")
+        f:close()
+        return data
+    end
+    return nil
+end
+
+function scripts.unzipper.extract()
+    local zipfile = scripts.unzipper.opened()
+    if zipfile then
+        local junk = environment.arguments["j"] or environment.arguments["junk"]
+        for k in zipfile:files() do
+            local filename = k.filename
+            if filename:find("/$") then
+                if not junk then
+                    lfs.mkdir(filename)
+                end
+            else
+                local data = zip.loaddata(zipfile,filename)
+                if data then
+                    if junk then
+                        filename = file.basename(filename)
+                    end
+                    io.savedata(filename,data)
+                    print(filename)
+                end
+            end
+        end
+    end
+end
+
+if environment.arguments["h"] or environment.arguments["help"] then
+    scripts.unzipper.help()
+elseif environment.arguments["l"] or environment.arguments["list"] then
+    scripts.unzipper.list(zipfile)
+elseif environment.files[1] then -- implicit --extract
+    scripts.unzipper.extract(zipfile)
+else
+    scripts.unzipper.help()
+end
diff --git a/scripts/context/lua/mtx-update.lua b/scripts/context/lua/mtx-update.lua
new file mode 100644
index 000000000..b56083d38
--- /dev/null
+++ b/scripts/context/lua/mtx-update.lua
@@ -0,0 +1,580 @@
+if not modules then modules = { } end modules ['mtx-update'] = {
+    version   = 1.002,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- This script is dedicated to Mojca Miklavec, who is the driving force behind
+-- moving minimal generation from our internal machines to the context garden.
+-- Together with Arthur Reutenauer she made sure that it worked well on all
+-- platforms that matter.
+
+local format, concat, gmatch = string.format, table.concat, string.gmatch
+
+scripts         = scripts         or { }
+scripts.update  = scripts.update  or { }
+
+minimals        = minimals        or { }
+minimals.config = minimals.config or { }
+
+-- this is needed under windows
+-- else rsync fails to set the right chmod flags to files
+
+os.setenv("CYGWIN","nontsec")
+
+scripts.update.texformats = {
+    "cont-en",
+    "cont-nl",
+    "cont-cz",
+    "cont-de",
+    "cont-fa",
+    "cont-it",
+    "cont-ro",
+    "cont-uk",
+    "cont-pe",
+    "cont-xp",
+    "mptopdf",
+    "plain"
+}
+
+scripts.update.mpformats = {
+    "metafun",
+    "mpost",
+}
+
+-- experimental is not functional at the moment
+
+scripts.update.repositories = {
+    "current",
+    "experimental"
+}
+
+-- more options than just these two are available (no idea why this is here)
+
+scripts.update.versions = {
+    "current",
+    "latest"
+}
+
+-- list of basic folders that are needed to make a functional distribution
+
+scripts.update.base = {
+    { "base/tex/",                "texmf" },
+    { "base/metapost/",           "texmf" },
+    { "fonts/common/",            "texmf" },
+    { "fonts/other/",             "texmf" }, -- not *really* needed, but helpful
+    { "context/<version>/",       "texmf-context" },
+    { "context/img/",             "texmf-context" },
+    { "misc/setuptex/",           "." },
+    { "misc/web2c",               "texmf" },
+    { "bin/common/<platform>/",   "texmf-<platform>" },
+    { "bin/context/<platform>/",  "texmf-<platform>" },
+    { "bin/metapost/<platform>/", "texmf-<platform>" },
+    { "bin/man/",                 "texmf-<platform>" },
+}
+
+-- binaries and font-related files
+-- for pdftex we don't need OpenType fonts, for LuaTeX/XeTeX we don't need TFM files
+
+scripts.update.engines = {
+    ["luatex"] = {
+        { "fonts/new/",               "texmf" },
+        { "bin/luatex/<platform>/",   "texmf-<platform>" },
+    },
+    ["xetex"] = {
+        { "base/xetex/",              "texmf" },
+        { "fonts/new/",               "texmf" },
+        { "bin/luatex/<platform>/",   "texmf-<platform>" }, -- tools
+        { "bin/xetex/<platform>/",    "texmf-<platform>" },
+    },
+    ["pdftex"] = {
+        { "fonts/old/",               "texmf" },
+        { "bin/luatex/<platform>/",   "texmf-<platform>" }, -- tools
+        { "bin/pdftex/<platform>/",   "texmf-<platform>" },
+    },
+    ["all"] = {
+        { "fonts/new/",               "texmf" },
+        { "fonts/old/",               "texmf" },
+        { "base/xetex/",              "texmf" },
+        { "bin/luatex/<platform>/",   "texmf-<platform>" },
+        { "bin/xetex/<platform>/",    "texmf-<platform>" },
+        { "bin/pdftex/<platform>/",   "texmf-<platform>" },
+    },
+}
+
+scripts.update.goodies = {
+    ["scite"] = {
+        { "bin/<platform>/scite/",     "texmf-<platform>" },
+    },
+    ["texworks"] = {
+        { "bin/<platform>/texworks/",  "texmf-<platform>" },
+    },
+}
+
+scripts.update.platforms = {
+    ["mswin"]          = "mswin",
+    ["windows"]        = "mswin",
+    ["win32"]          = "mswin",
+    ["win"]            = "mswin",
+    ["linux"]          = "linux",
+    ["freebsd"]        = "freebsd",
+    ["freebsd-amd64"]  = "freebsd-amd64",
+    ["kfreebsd"]       = "kfreebsd-i386",
+    ["kfreebsd-i386"]  = "kfreebsd-i386",
+    ["kfreebsd-amd64"] = "kfreebsd-amd64",
+    ["linux-32"]       = "linux",
+    ["linux-64"]       = "linux-64",
+    ["linux32"]        = "linux",
+    ["linux64"]        = "linux-64",
+    ["linux-ppc"]      = "linux-ppc",
+    ["ppc"]            = "linux-ppc",
+    ["osx"]            = "osx-intel",
+    ["macosx"]         = "osx-intel",
+    ["osx-intel"]      = "osx-intel",
+    ["osx-ppc"]        = "osx-ppc",
+    ["osx-powerpc"]    = "osx-ppc",
+    ["osx-64"]         = "osx-64",
+    ["osxintel"]       = "osx-intel",
+    ["osxppc"]         = "osx-ppc",
+    ["osxpowerpc"]     = "osx-ppc",
+    ["solaris-intel"]  = "solaris-intel",
+    ["solaris-sparc"]  = "solaris-sparc",
+    ["solaris"]        = "solaris-sparc",
+}
+
+-- the list is filled up later (when we know what modules to download)
+
+scripts.update.modules = {
+}
+
+function scripts.update.run(str)
+    -- important, otherwise formats fly to a weird place
+    -- (texlua sets luatex as the engine, we need to reset that or to fix texexec :)
+    os.setenv("engine",nil)
+    if environment.argument("force") then
+        logs.report("run", str)
+        os.execute(str)
+    else
+        logs.report("dry run", str)
+    end
+end
+
+function scripts.update.fullpath(path)
+    if file.is_rootbased_path(path) then
+        return path
+    else
+        return lfs.currentdir() .. "/" .. path
+    end
+end
+
+function scripts.update.synchronize()
+
+    logs.report("update","start")
+
+    local texroot      = scripts.update.fullpath(states.get("paths.root"))
+    local engines      = states.get('engines') or { }
+    local platforms    = states.get('platforms') or { }
+    local repositories = states.get('repositories')      -- minimals
+    local bin          = states.get("rsync.program")     -- rsync
+    local url          = states.get("rsync.server")      -- contextgarden.net
+    local version      = states.get("context.version")   -- current (or beta)
+    local extras       = states.get("extras")            -- extras (like modules)
+    local goodies      = states.get("goodies")           -- goodies (like editors)
+    local force        = environment.argument("force")
+
+    bin = string.gsub(bin,"\\","/")
+
+    if not url:find("::$") then url = url .. "::" end
+    local ok = lfs.attributes(texroot,"mode") == "directory"
+    if not ok and force then
+        dir.mkdirs(texroot)
+        ok = lfs.attributes(texroot,"mode") == "directory"
+    end
+
+    if force then
+        dir.mkdirs(format("%s/%s", texroot, "texmf-cache"))
+        dir.mkdirs(format("%s/%s", texroot, "texmf-local"))
+        dir.mkdirs(format("%s/%s", texroot, "texmf-project"))
+    end
+
+    if ok or not force then
+
+        local fetched, individual, osplatform = { }, { }, os.platform
+
+        -- takes a collection as argument and returns a list of folders
+
+        local function collection_to_list_of_folders(collection, platform)
+            local archives = {}
+            for i=1,#collection do
+                local archive = collection[i][1]
+                archive = archive:gsub("<platform>", platform)
+                archive = archive:gsub("<version>", version)
+                archives[#archives+1] = archive
+            end
+            return archives
+        end
+
+        -- takes a list of folders as argument and returns a string for rsync
+        -- sample input:
+        --     {'bin/common', 'bin/context'}
+        -- output:
+        --     'minimals/current/bin/common minimals/current/bin/context'
+
+        local function list_of_folders_to_rsync_string(list_of_folders)
+            local repository  = 'current'
+            local prefix = format("%s/%s/", states.get('rsync.module'), repository) -- minimals/current/
+
+            return prefix .. concat(list_of_folders, format(" %s", prefix))
+        end
+
+        -- example of usage: print(list_of_folders_to_rsync_string(collection_to_list_of_folders(scripts.update.base, os.platform)))
+
+        -- rename function and add some more functionality:
+        --   * recursive/non-recursive (default: non-recursive)
+        --   * filter folders or regular files only (default: no filter)
+        --   * grep for size of included files (with --stats switch)
+
+        local function get_list_of_files_from_rsync(list_of_folders)
+            -- temporary file to store the output of rsync (could be a more random name; watch for overwrites)
+            local temp_file = "rsync.tmp.txt"
+            -- a set of folders
+            local folders = {}
+            local command = format("%s %s'%s' > %s", bin, url, list_of_folders_to_rsync_string(list_of_folders), temp_file)
+            os.execute(command)
+            -- read output of rsync
+            local data = io.loaddata(temp_file) or ""
+            -- for every line extract the filename
+            for chmod, s in data:gmatch("([d%-][rwx%-]+).-(%S+)[\n\r]") do
+                -- skip "current" folder
+                if s ~= '.' and chmod:len() == 10 then
+                    folders[#folders+1] = s
+                end
+            end
+            -- delete the file to which we have put output of rsync
+            os.remove(temp_file)
+            return folders
+        end
+
+        -- rsync://contextgarden.net/minimals/current/modules/
+
+        if extras and type(extras) == "table" then
+            -- fetch the list of available modules from rsync server
+            local available_modules = get_list_of_files_from_rsync({"modules/"})
+            -- hash of requested modules
+            -- local h = table.tohash(extras:split(","))
+            for i=1,#available_modules do
+                local s = available_modules[i]
+            --  if extras == "all" or h[s] then
+                if extras.all or extras[s] then
+                    scripts.update.modules[#scripts.update.modules+1] = { format("modules/%s/",s), "texmf-context" }
+                end
+            end
+            -- TODO: check if every module from the list has been added and issue warning otherwise
+            -- one idea to do it: remove every value from h once added and then check if anything is left in h
+        end
+
+        local function add_collection(collection,platform)
+            if collection and platform then
+                platform = scripts.update.platforms[platform]
+                if platform then
+                    for i=1,#collection do
+                        local c = collection[i]
+                        local archive = c[1]:gsub("<platform>", platform)
+                        local destination = format("%s/%s", texroot, c[2]:gsub("<platform>", platform))
+                        destination = destination:gsub("\\","/")
+                        archive = archive:gsub("<version>",version)
+                        if osplatform == "windows" or osplatform == "mswin" then
+                            destination = destination:gsub("([a-zA-Z]):/", "/cygdrive/%1/")
+                        end
+                        individual[#individual+1] = { archive, destination }
+                    end
+                end
+            end
+        end
+
+        for platform, _ in next, platforms do
+            add_collection(scripts.update.base,platform)
+        end
+        for platform, _ in next, platforms do
+            add_collection(scripts.update.modules,platform)
+        end
+        for engine, _ in next, engines do
+            for platform, _ in next, platforms do
+                add_collection(scripts.update.engines[engine],platform)
+            end
+        end
+
+        if goodies and type(goodies) == "table" then
+            for goodie, _ in next, goodies do
+                for platform, _ in next, platforms do
+                    add_collection(scripts.update.goodies[goodie],platform)
+                end
+            end
+        end
+
+        local combined = { }
+        local update_repositories = scripts.update.repositories
+        for i=1,#update_repositories do
+            local repository = update_repositories[i]
+            if repositories[repository] then
+                for _, v in next, individual do
+                    local archive, destination = v[1], v[2]
+                    local cd = combined[destination]
+                    if not cd then
+                        cd = { }
+                        combined[destination] = cd
+                    end
+                    cd[#cd+1] = format("%s/%s/%s",states.get('rsync.module'),repository,archive)
+                end
+            end
+        end
+        if logs.verbose then
+            for k, v in next, combined do
+                logs.report("update", k)
+                for i=1,#v do
+                    logs.report("update", "  <= " .. v[i])
+                end
+            end
+        end
+        for destination, archive in next, combined do
+            local archives, command = concat(archive," "), ""
+        --  local normalflags, deleteflags = states.get("rsync.flags.normal"), states.get("rsync.flags.delete")
+        --    if environment.argument("keep") or destination:find("%.$") then
+        --       command = format("%s %s    %s'%s' '%s'", bin, normalflags,              url, archives, destination)
+        --    else
+        --        command = format("%s %s %s %s'%s' '%s'", bin, normalflags, deleteflags, url, archives, destination)
+        --    end
+            local normalflags, deleteflags = states.get("rsync.flags.normal"), ""
+            local dryrunflags = ""
+            if not environment.argument("force") then
+                dryrunflags = "--dry-run"
+            end
+            if (destination:find("texmf$") or destination:find("texmf%-context$")) and (not environment.argument("keep")) then
+                deleteflags = states.get("rsync.flags.delete")
+            end
+            command = format("%s %s %s %s %s'%s' '%s'", bin, normalflags, deleteflags, dryrunflags, url, archives, destination)
+            --logs.report("mtx update", format("running command: %s",command))
+            if not fetched[command] then
+                scripts.update.run(command,true)
+                fetched[command] = command
+            end
+        end
+
+        local function update_script(script, platform)
+            local bin = bin:gsub("\\","/")
+            local texroot = texroot:gsub("\\","/")
+            platform = scripts.update.platforms[platform]
+            if platform then
+                local command
+                if platform == 'mswin' then
+                    bin = bin:gsub("([a-zA-Z]):/", "/cygdrive/%1/")
+                    texroot = texroot:gsub("([a-zA-Z]):/", "/cygdrive/%1/")
+                    command = string.format("%s -t %s/texmf-context/scripts/context/lua/%s.lua %s/texmf-mswin/bin/", bin, texroot, script, texroot)
+                else
+                    command = string.format("%s -tgo --chmod=a+x %s/texmf-context/scripts/context/lua/%s.lua %s/texmf-%s/bin/%s", bin, texroot, script, texroot, platform, script)
+                end
+                logs.report("mtx update", format("updating %s for %s: %s", script, platform, command))
+                scripts.update.run(command)
+            end
+        end
+
+        for platform, _ in next, platforms do
+            update_script('luatools',platform)
+            update_script('mtxrun',platform)
+        end
+
+    else
+        logs.report("mtx update", format("no valid texroot: %s",texroot))
+    end
+    if not force then
+        logs.report("update", "use --force to really update files")
+    end
+
+    resolvers.load_tree(texroot) -- else we operate in the wrong tree
+
+    -- update filename database for pdftex/xetex
+    scripts.update.run("mktexlsr")
+    -- update filename database for luatex
+    scripts.update.run("luatools --generate")
+
+    logs.report("update","done")
+end
+
+function table.fromhash(t)
+    local h = { }
+    for k, v in next, t do -- not indexed
+        if v then h[#h+1] = k end
+    end
+    return h
+end
+
+-- make the ConTeXt formats
+function scripts.update.make()
+
+    logs.report("make","start")
+
+    local force     = environment.argument("force")
+    local texroot   = scripts.update.fullpath(states.get("paths.root"))
+    local engines   = states.get('engines')
+    local goodies   = states.get('goodies')
+    local platforms = states.get('platforms')
+    local formats   = states.get('formats')
+
+    resolvers.load_tree(texroot)
+
+    scripts.update.run("mktexlsr")
+    scripts.update.run("luatools --generate")
+
+    local askedformats = formats
+    local texformats = table.tohash(scripts.update.texformats)
+    local mpformats = table.tohash(scripts.update.mpformats)
+    for k,v in next, texformats do
+        if not askedformats[k] then
+            texformats[k] = nil
+        end
+    end
+    for k,v in next, mpformats do
+        if not askedformats[k] then
+            mpformats[k] = nil
+        end
+    end
+    local formatlist = concat(table.fromhash(texformats), " ")
+    if formatlist ~= "" then
+        for engine in next, engines do
+            if engine == "luatex" then
+                scripts.update.run(format("context --make")) -- maybe also formatlist
+            else
+                -- todo: just handle make here or in mtxrun --script context --make
+                scripts.update.run(format("texexec --make --all --fast --%s %s",engine,formatlist))
+            end
+        end
+    end
+    local formatlist = concat(table.fromhash(mpformats), " ")
+    if formatlist ~= "" then
+        scripts.update.run(format("texexec --make --all --fast %s",formatlist))
+    end
+    if not force then
+        logs.report("make", "use --force to really make formats")
+    end
+    scripts.update.run("mktexlsr")
+    scripts.update.run("luatools --generate")
+    logs.report("make","done")
+end
+
+logs.extendbanner("ConTeXt Minimals Updater 0.21",true)
+
+messages.help = [[
+--platform=string     platform (windows, linux, linux-64, osx-intel, osx-ppc, linux-ppc)
+--server=string       repository url (rsync://contextgarden.net)
+--module=string       repository url (minimals)
+--repository=string   specify version (current, experimental)
+--context=string      specify version (current, latest, yyyy.mm.dd)
+--rsync=string        rsync binary (rsync)
+--texroot=string      installation directory (not guessed for the moment)
+--engine=string       tex engine (luatex, pdftex, xetex)
+--extras=string       extra modules (can be list or 'all')
+--goodies=string      extra binaries (like scite and texworks)
+--force               instead of a dryrun, do the real thing
+--update              update minimal tree
+--make                also make formats and generate file databases
+--keep                don't delete unused or obsolete files
+--state               update tree using saved state
+]]
+
+scripts.savestate = true
+
+if scripts.savestate then
+
+    states.load("status-of-update.lua")
+
+    -- tag, value, default, persistent
+
+    statistics.starttiming(states)
+
+    states.set("info.version",0.1) -- ok
+    states.set("info.count",(states.get("info.count") or 0) + 1,1,false) -- ok
+    states.set("info.comment","this file contains the settings of the last 'mtxrun --script update ' run",false) -- ok
+    states.set("info.date",os.date("!%Y-%m-%d %H:%M:%S")) -- ok
+
+    states.set("rsync.program", environment.argument("rsync"), "rsync", true) -- ok
+    states.set("rsync.server", environment.argument("server"), "contextgarden.net::", true) -- ok
+    states.set("rsync.module", environment.argument("module"), "minimals", true) -- ok
+    states.set("rsync.flags.normal", environment.argument("flags"), "-rpztlv", true) -- ok
+    states.set("rsync.flags.delete", nil, "--delete", true) -- ok
+
+    states.set("paths.root", environment.argument("texroot"), "tex", true) -- ok
+
+    states.set("context.version", environment.argument("context"), "current", true) -- ok
+
+    local valid = table.tohash(scripts.update.repositories)
+    for r in gmatch(environment.argument("repository") or "current","([^, ]+)") do
+        if valid[r] then states.set("repositories." .. r, true) end
+    end
+    local valid = scripts.update.engines
+    for r in gmatch(environment.argument("engine") or "all","([^, ]+)") do
+        if r == "all" then
+            for k, v in next, valid do
+                if k ~= "all" then
+                    states.set("engines." .. k, true)
+                end
+            end
+        elseif valid[r] then
+            states.set("engines." .. r, true)
+        end
+    end
+    local valid = scripts.update.platforms
+    for r in gmatch(environment.argument("platform") or os.platform,"([^, ]+)") do
+        if valid[r] then states.set("platforms." .. r, true) end
+    end
+
+    local valid = table.tohash(scripts.update.texformats)
+    for r in gmatch(environment.argument("formats") or "","([^, ]+)") do
+        if valid[r] then states.set("formats." .. r, true) end
+    end
+    local valid = table.tohash(scripts.update.mpformats)
+    for r in gmatch(environment.argument("formats") or "","([^, ]+)") do
+        if valid[r] then states.set("formats." .. r, true) end
+    end
+
+    states.set("formats.cont-en", true)
+    states.set("formats.cont-nl", true)
+    states.set("formats.metafun", true)
+
+    for r in gmatch(environment.argument("extras") or "","([^, ]+)") do
+        states.set("extras." .. r, true)
+    end
+    for r in gmatch(environment.argument("goodies") or "","([^, ]+)") do
+        states.set("goodies." .. r, true)
+    end
+
+    logs.report("state","loaded")
+
+end
+
+if environment.argument("state") then
+    environment.setargument("update",true)
+    environment.setargument("force",true)
+    environment.setargument("make",true)
+end
+
+if environment.argument("update") then
+    scripts.update.synchronize()
+    if environment.argument("make") then
+        scripts.update.make()
+    end
+elseif environment.argument("make") then
+    scripts.update.make()
+else
+    logs.help(messages.help)
+end
+
+if scripts.savestate then
+    statistics.stoptiming(states)
+    states.set("info.runtime",tonumber(statistics.elapsedtime(states)))
+    if environment.argument("force") then
+        states.save()
+        logs.report("state","saved")
+    end
+end
diff --git a/scripts/context/lua/mtx-watch.lua b/scripts/context/lua/mtx-watch.lua
new file mode 100644
index 000000000..10f01cf86
--- /dev/null
+++ b/scripts/context/lua/mtx-watch.lua
@@ -0,0 +1,382 @@
+if not modules then modules = { } end modules ['mtx-watch'] = {
+    version   = 1.001,
+    comment   = "companion to mtxrun.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+scripts       = scripts       or { }
+scripts.watch = scripts.watch or { }
+
+local format, concat, difftime, time = string.format, table.concat, os.difftime, os.time
+local next, type = next, type
+
+-- the machine/instance matches the server app we use
+
+local machine  = socket.dns.gethostname() or "unknown-machine"
+local instance = string.match(machine,"(%d+)$") or "0"
+
+function scripts.watch.save_exa_modes(joblog,ctmname)
+    local values = joblog and joblog.values
+    if values then
+        local t= { }
+        t[#t+1] = "<?xml version='1.0' standalone='yes'?>\n"
+        t[#t+1] = "<exa:variables xmlns:exa='htpp://www.pragma-ade.com/schemas/exa-variables.rng'>"
+        for k, v in next, joblog.values do
+            t[#t+1] = format("\t<exa:variable label='%s'>%s</exa:variable>", k, tostring(v))
+        end
+        t[#t+1] = "</exa:variables>"
+        io.savedata(ctmname,concat(t,"\n"))
+    else
+        os.remove(ctmname)
+    end
+end
+
+local function toset(t)
+    if type(t) == "table" then
+        return concat(t,",")
+    else
+        return t
+    end
+end
+
+local function noset(t)
+    if type(t) == "table" then
+        return t[1]
+    else
+        return t
+    end
+end
+
+local lfsdir, lfsattributes = lfs.dir, lfs.attributes
+
+local function glob(files,path)
+    for name in lfsdir(path) do
+        if name:find("^%.") then
+            -- skip . and ..
+        else
+            name = path .. "/" .. name
+            local a = lfsattributes(name)
+            if not a then
+                -- weird
+            elseif a.mode == "directory" then
+                if name:find("graphics$") or name:find("figures$") or name:find("resources$") then
+                    -- skip these too
+                else
+                    glob(files,name)
+                end
+            elseif name:find(".%luj$") then
+                files[name] = a.change or a.ctime or a.modification or a.mtime
+            end
+        end
+    end
+end
+
+local clock = os.gettimeofday or os.time -- we cannot trust os.clock on linux
+
+function scripts.watch.watch()
+    local delay   = tonumber(environment.argument("delay") or 5) or 5
+    if delay == 0 then
+        delay = .25
+    end
+    local logpath = environment.argument("logpath") or ""
+    local pipe    = environment.argument("pipe")    or false
+    local watcher = "mtxwatch.run"
+    local paths   = environment.files
+    if #paths > 0 then
+        if environment.argument("automachine") then
+            logpath = string.gsub(logpath,"/machine/","/"..machine.."/")
+            for i=1,#paths do
+                paths[i] = string.gsub(paths[i],"/machine/","/"..machine.."/")
+            end
+        end
+        for i=1,#paths do
+            logs.report("watch", "watching path ".. paths[i])
+        end
+        local function process()
+            local done = false
+            for i=1,#paths do
+                local path = paths[i]
+                lfs.chdir(path)
+                local files = { }
+                glob(files,path)
+                table.sort(files) -- what gets sorted here, todo: by time
+                for name, time in next, files do
+                --~ local ok, joblog = xpcall(function() return dofile(name) end, function() end )
+                    local ok, joblog = pcall(dofile,name)
+                    if ok and joblog then
+                        if joblog.status == "processing" then
+                            logs.report("watch",format("aborted job, %s added to queue",name))
+                            joblog.status = "queued"
+                            io.savedata(name, table.serialize(joblog,true))
+                        elseif joblog.status == "queued" then
+                            local command = joblog.command
+                            if command then
+                                local replacements = {
+                                    inputpath  = toset((joblog.paths and joblog.paths.input ) or "."),
+                                    outputpath = noset((joblog.paths and joblog.paths.output) or "."),
+                                    filename   = joblog.filename or "",
+                                }
+                                -- todo: revision path etc
+                                command = command:gsub("%%(.-)%%", replacements)
+                                if command ~= "" then
+                                    joblog.status = "processing"
+                                    joblog.runtime = clock()
+                                    io.savedata(name, table.serialize(joblog,true))
+                                    logs.report("watch",format("running: %s", command))
+                                    local newpath = file.dirname(name)
+                                    io.flush()
+                                    local result = ""
+                                    local ctmname = file.basename(replacements.filename)
+                                    if ctmname == "" then ctmname = name end -- use self as fallback
+                                    ctmname = file.replacesuffix(ctmname,"ctm")
+                                    if newpath ~= "" and newpath ~= "." then
+                                        local oldpath = lfs.currentdir()
+                                        lfs.chdir(newpath)
+                                        scripts.watch.save_exa_modes(joblog,ctmname)
+                                        if pipe then result = os.resultof(command) else result = os.spawn(command) end
+                                        lfs.chdir(oldpath)
+                                    else
+                                        scripts.watch.save_exa_modes(joblog,ctmname)
+                                        if pipe then result = os.resultof(command) else result = os.spawn(command) end
+                                    end
+                                    logs.report("watch",format("return value: %s", result))
+                                    done = true
+                                    local path, base = replacements.outputpath, file.basename(replacements.filename)
+                                    joblog.runtime = clock() - joblog.runtime
+                                    if base ~= "" then
+                                        joblog.result = file.replacesuffix(file.join(path,base),"pdf")
+                                        joblog.size   = lfs.attributes(joblog.result,"size")
+                                    end
+                                    joblog.status = "finished"
+                                else
+                                    joblog.status = "invalid command"
+                                end
+                            else
+                                joblog.status = "no command"
+                            end
+                            -- pcall, when error sleep + again
+                            -- todo: just one log file and append
+                            io.savedata(name, table.serialize(joblog,true))
+                            if logpath and logpath ~= "" then
+                                local name = file.join(logpath,os.uuid() .. ".lua")
+                                io.savedata(name, table.serialize(joblog,true))
+                                logs.report("watch", "saving joblog in " .. name)
+                            end
+                        end
+                    end
+                end
+            end
+        end
+        local n, start = 0, time()
+--~         local function wait()
+--~             io.flush()
+--~             if not done then
+--~                 n = n + 1
+--~                 if n >= 10 then
+--~                     logs.report("watch", format("run time: %i seconds, memory usage: %0.3g MB", difftime(time(),start), (status.luastate_bytes/1024)/1000))
+--~                     n = 0
+--~                 end
+--~                 os.sleep(delay)
+--~             end
+--~         end
+        local wtime = 0
+        local function wait()
+            io.flush()
+            if not done then
+                n = n + 1
+                if n >= 10 then
+                    logs.report("watch", format("run time: %i seconds, memory usage: %0.3g MB", difftime(time(),start), (status.luastate_bytes/1024)/1000))
+                    n = 0
+                end
+                local ttime = 0
+                while ttime <= delay do
+                    local wt = lfs.attributes(watcher,"mtime")
+                    if wt and wt ~= wtime then
+                        -- fast signal that there is a request
+                        wtime = wt
+                        break
+                    end
+                    ttime = ttime + 0.2
+                    os.sleep(0.2)
+                end
+            end
+        end
+        local cleanupdelay, cleanup = environment.argument("cleanup"), false
+        if cleanupdelay then
+            local lasttime = time()
+            cleanup = function()
+                local currenttime = time()
+                local delta = difftime(currenttime,lasttime)
+                if delta > cleanupdelay then
+                    lasttime = currenttime
+                    for i=1,#paths do
+                        local path = paths[i]
+                        if string.find(path,"%.") then
+                            -- safeguard, we want a fully qualified path
+                        else
+                            local files = dir.glob(file.join(path,"*"))
+                            for i=1,#files do
+                                local name = files[i]
+                                local filetime = lfs.attributes(name,"modification")
+                                local delta = difftime(currenttime,filetime)
+                                if delta > cleanupdelay then
+                                 -- logs.report("watch",format("cleaning up '%s'",name))
+                                    os.remove(name)
+                                end
+                            end
+                        end
+                    end
+                end
+            end
+        else
+            cleanup = function()
+                -- nothing
+            end
+        end
+        while true do
+            if false then
+--~             if true then
+                process()
+                cleanup()
+                wait()
+            else
+                pcall(process)
+                pcall(cleanup)
+                pcall(wait)
+            end
+        end
+    else
+        logs.report("watch", "no paths to watch")
+    end
+end
+
+function scripts.watch.collect_logs(path) -- clean 'm up too
+    path = path or environment.argument("logpath") or ""
+    path = (path == "" and ".") or path
+    local files = dir.globfiles(path,false,"^%d+%.lua$")
+    local collection = { }
+    local valid = table.tohash({"filename","result","runtime","size","status"})
+    for i=1,#files do
+        local name = files[i]
+        local t = dofile(name)
+        if t and type(t) == "table" and t.status then
+            for k, v in next, t do
+                if not valid[k] then
+                    t[k] = nil
+                end
+            end
+            collection[name:gsub("[^%d]","")] = t
+        end
+    end
+    return collection
+end
+
+function scripts.watch.save_logs(collection,path) -- play safe
+    if collection and next(collection) then
+        path = path or environment.argument("logpath") or ""
+        path = (path == "" and ".") or path
+        local filename = format("%s/collected-%s.lua",path,tostring(time()))
+        io.savedata(filename,table.serialize(collection,true))
+        local check = dofile(filename)
+        for k,v in next, check do
+            if not collection[k] then
+                logs.error("watch", "error in saving file")
+                os.remove(filename)
+                return false
+            end
+        end
+        for k,v in next, check do
+            os.remove(format("%s.lua",k))
+        end
+        return true
+    else
+        return false
+    end
+end
+
+function scripts.watch.collect_collections(path) -- removes duplicates
+    path = path or environment.argument("logpath") or ""
+    path = (path == "" and ".") or path
+    local files = dir.globfiles(path,false,"^collected%-%d+%.lua$")
+    local collection = { }
+    for i=1,#files do
+        local name = files[i]
+        local t = dofile(name)
+        if t and type(t) == "table" then
+            for k, v in next, t do
+                collection[k] = v
+            end
+        end
+    end
+    return collection
+end
+
+function scripts.watch.show_logs(path) -- removes duplicates
+    local collection = scripts.watch.collect_collections(path) or { }
+    local max = 0
+    for k,v in next, collection do
+        v = v.filename or "?"
+        if #v > max then max = #v end
+    end
+ -- print(max)
+    local sorted = table.sortedkeys(collection)
+    for k=1,#sorted do
+        local v = sorted[k]
+        local c = collection[v]
+        local f, s, r, n = c.filename or "?", c.status or "?", c.runtime or 0, c.size or 0
+        logs.report("watch", format("%s  %s  %3i  %8i  %s",string.padd(f,max," "),string.padd(s,10," "),r,n,v))
+    end
+end
+
+function scripts.watch.cleanup_stale_files() -- removes duplicates
+    local path  = environment.files[1]
+    local delay = tonumber(environment.argument("cleanup"))
+    local force = environment.argument("force")
+    if not path or path == "." then
+        logs.report("watch","provide qualified path")
+    elseif not delay then
+        logs.report("watch","missing --cleanup=delay")
+    else
+        logs.report("watch","dryrun, use --force for real cleanup")
+        local files = dir.glob(file.join(path,"*"))
+        local rtime = time()
+        for i=1,#files do
+            local name = files[i]
+            local mtime = lfs.attributes(name,"modification")
+            local delta = difftime(rtime,mtime)
+            if delta > delay then
+                logs.report("watch",format("cleaning up '%s'",name))
+                if force then
+                    os.remove(name)
+                end
+            end
+        end
+    end
+end
+
+logs.extendbanner("ConTeXt Request Watchdog 1.00",true)
+
+messages.help = [[
+--logpath             optional path for log files
+--watch               watch given path [--delay]
+--pipe                use pipe instead of execute
+--delay               delay between sweeps
+--automachine         replace /machine/ in path /<servername>/
+--collect             condense log files
+--cleanup=delay       remove files in given path [--force]
+--showlog             show log data
+]]
+
+if environment.argument("watch") then
+    scripts.watch.watch()
+elseif environment.argument("collect") then
+    scripts.watch.save_logs(scripts.watch.collect_logs())
+elseif environment.argument("cleanup") then
+    scripts.watch.save_logs(scripts.watch.cleanup_stale_files())
+elseif environment.argument("showlog") then
+    scripts.watch.show_logs()
+else
+    logs.help(messages.help)
+end
diff --git a/scripts/context/lua/mtxrun.lua b/scripts/context/lua/mtxrun.lua
new file mode 100644
index 000000000..b99327692
--- /dev/null
+++ b/scripts/context/lua/mtxrun.lua
@@ -0,0 +1,12639 @@
+#!/usr/bin/env texlua
+
+if not modules then modules = { } end modules ['mtxrun'] = {
+    version   = 1.001,
+    comment   = "runner, lua replacement for texmfstart.rb",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+
+-- one can make a stub:
+--
+-- #!/bin/sh
+-- env LUATEXDIR=/....../texmf/scripts/context/lua luatex --luaonly mtxrun.lua "$@"
+
+-- filename : mtxrun.lua
+-- comment  : companion to context.tex
+-- author   : Hans Hagen, PRAGMA-ADE, Hasselt NL
+-- copyright: PRAGMA ADE / ConTeXt Development Team
+-- license  : see context related readme files
+
+-- This script is based on texmfstart.rb but does not use kpsewhich to
+-- locate files. Although kpse is a library it never came to opening up
+-- its interface to other programs (esp scripting languages) and so we
+-- do it ourselves. The lua variant evolved out of an experimental ruby
+-- one. Interesting is that using a scripting language instead of c does
+-- not have a speed penalty. Actually the lua variant is more efficient,
+-- especially when multiple calls to kpsewhich are involved. The lua
+-- library also gives way more control.
+
+-- to be done / considered
+--
+-- support for --exec or make it default
+-- support for jar files (or maybe not, never used, too messy)
+-- support for $RUBYINPUTS cum suis (if still needed)
+-- remember for subruns: _CTX_K_V_#{original}_
+-- remember for subruns: _CTX_K_S_#{original}_
+-- remember for subruns: TEXMFSTART.#{original} [tex.rb texmfstart.rb]
+
+texlua = true
+
+-- begin library merge
+
+
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-string'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local sub, gsub, find, match, gmatch, format, char, byte, rep, lower = string.sub, string.gsub, string.find, string.match, string.gmatch, string.format, string.char, string.byte, string.rep, string.lower
+local lpegmatch = lpeg.match
+
+-- some functions may disappear as they are not used anywhere
+
+if not string.split then
+
+    -- this will be overloaded by a faster lpeg variant
+
+    function string:split(pattern)
+        if #self > 0 then
+            local t = { }
+            for s in gmatch(self..pattern,"(.-)"..pattern) do
+                t[#t+1] = s
+            end
+            return t
+        else
+            return { }
+        end
+    end
+
+end
+
+local chr_to_esc = {
+    ["%"] = "%%",
+    ["."] = "%.",
+    ["+"] = "%+", ["-"] = "%-", ["*"] = "%*",
+    ["^"] = "%^", ["$"] = "%$",
+    ["["] = "%[", ["]"] = "%]",
+    ["("] = "%(", [")"] = "%)",
+    ["{"] = "%{", ["}"] = "%}"
+}
+
+string.chr_to_esc = chr_to_esc
+
+function string:esc() -- variant 2
+    return (gsub(self,"(.)",chr_to_esc))
+end
+
+function string:unquote()
+    return (gsub(self,"^([\"\'])(.*)%1$","%2"))
+end
+
+--~ function string:unquote()
+--~     if find(self,"^[\'\"]") then
+--~         return sub(self,2,-2)
+--~     else
+--~         return self
+--~     end
+--~ end
+
+function string:quote() -- we could use format("%q")
+    return format("%q",self)
+end
+
+function string:count(pattern) -- variant 3
+    local n = 0
+    for _ in gmatch(self,pattern) do
+        n = n + 1
+    end
+    return n
+end
+
+function string:limit(n,sentinel)
+    if #self > n then
+        sentinel = sentinel or " ..."
+        return sub(self,1,(n-#sentinel)) .. sentinel
+    else
+        return self
+    end
+end
+
+--~ function string:strip() -- the .- is quite efficient
+--~  -- return match(self,"^%s*(.-)%s*$") or ""
+--~  -- return match(self,'^%s*(.*%S)') or '' -- posted on lua list
+--~     return find(s,'^%s*$') and '' or match(s,'^%s*(.*%S)')
+--~ end
+
+do -- roberto's variant:
+    local space    = lpeg.S(" \t\v\n")
+    local nospace  = 1 - space
+    local stripper = space^0 * lpeg.C((space^0 * nospace^1)^0)
+    function string.strip(str)
+        return lpegmatch(stripper,str) or ""
+    end
+end
+
+function string:is_empty()
+    return not find(self,"%S")
+end
+
+function string:enhance(pattern,action)
+    local ok, n = true, 0
+    while ok do
+        ok = false
+        self = gsub(self,pattern, function(...)
+            ok, n = true, n + 1
+            return action(...)
+        end)
+    end
+    return self, n
+end
+
+local chr_to_hex, hex_to_chr = { }, { }
+
+for i=0,255 do
+    local c, h = char(i), format("%02X",i)
+    chr_to_hex[c], hex_to_chr[h] = h, c
+end
+
+function string:to_hex()
+    return (gsub(self or "","(.)",chr_to_hex))
+end
+
+function string:from_hex()
+    return (gsub(self or "","(..)",hex_to_chr))
+end
+
+if not string.characters then
+
+    local function nextchar(str, index)
+        index = index + 1
+        return (index <= #str) and index or nil, sub(str,index,index)
+    end
+    function string:characters()
+        return nextchar, self, 0
+    end
+    local function nextbyte(str, index)
+        index = index + 1
+        return (index <= #str) and index or nil, byte(sub(str,index,index))
+    end
+    function string:bytes()
+        return nextbyte, self, 0
+    end
+
+end
+
+-- we can use format for this (neg n)
+
+function string:rpadd(n,chr)
+    local m = n-#self
+    if m > 0 then
+        return self .. rep(chr or " ",m)
+    else
+        return self
+    end
+end
+
+function string:lpadd(n,chr)
+    local m = n-#self
+    if m > 0 then
+        return rep(chr or " ",m) .. self
+    else
+        return self
+    end
+end
+
+string.padd = string.rpadd
+
+function is_number(str) -- tonumber
+    return find(str,"^[%-%+]?[%d]-%.?[%d+]$") == 1
+end
+
+--~ print(is_number("1"))
+--~ print(is_number("1.1"))
+--~ print(is_number(".1"))
+--~ print(is_number("-0.1"))
+--~ print(is_number("+0.1"))
+--~ print(is_number("-.1"))
+--~ print(is_number("+.1"))
+
+function string:split_settings() -- no {} handling, see l-aux for lpeg variant
+    if find(self,"=") then
+        local t = { }
+        for k,v in gmatch(self,"(%a+)=([^%,]*)") do
+            t[k] = v
+        end
+        return t
+    else
+        return nil
+    end
+end
+
+local patterns_escapes = {
+    ["-"] = "%-",
+    ["."] = "%.",
+    ["+"] = "%+",
+    ["*"] = "%*",
+    ["%"] = "%%",
+    ["("] = "%)",
+    [")"] = "%)",
+    ["["] = "%[",
+    ["]"] = "%]",
+}
+
+function string:pattesc()
+    return (gsub(self,".",patterns_escapes))
+end
+
+local simple_escapes = {
+    ["-"] = "%-",
+    ["."] = "%.",
+    ["?"] = ".",
+    ["*"] = ".*",
+}
+
+function string:simpleesc()
+    return (gsub(self,".",simple_escapes))
+end
+
+function string:tohash()
+    local t = { }
+    for s in gmatch(self,"([^, ]+)") do -- lpeg
+        t[s] = true
+    end
+    return t
+end
+
+local pattern = lpeg.Ct(lpeg.C(1)^0)
+
+function string:totable()
+    return lpegmatch(pattern,self)
+end
+
+--~ local t = {
+--~     "1234567123456712345671234567",
+--~     "a\tb\tc",
+--~     "aa\tbb\tcc",
+--~     "aaa\tbbb\tccc",
+--~     "aaaa\tbbbb\tcccc",
+--~     "aaaaa\tbbbbb\tccccc",
+--~     "aaaaaa\tbbbbbb\tcccccc",
+--~ }
+--~ for k,v do
+--~     print(string.tabtospace(t[k]))
+--~ end
+
+function string.tabtospace(str,tab)
+    -- we don't handle embedded newlines
+    while true do
+        local s = find(str,"\t")
+        if s then
+            if not tab then tab = 7 end -- only when found
+            local d = tab-(s-1) % tab
+            if d > 0 then
+                str = gsub(str,"\t",rep(" ",d),1)
+            else
+                str = gsub(str,"\t","",1)
+            end
+        else
+            break
+        end
+    end
+    return str
+end
+
+function string:compactlong() -- strips newlines and leading spaces
+    self = gsub(self,"[\n\r]+ *","")
+    self = gsub(self,"^ *","")
+    return self
+end
+
+function string:striplong() -- strips newlines and leading spaces
+    self = gsub(self,"^%s*","")
+    self = gsub(self,"[\n\r]+ *","\n")
+    return self
+end
+
+function string:topattern(lowercase,strict)
+    if lowercase then
+        self = lower(self)
+    end
+    self = gsub(self,".",simple_escapes)
+    if self == "" then
+        self = ".*"
+    elseif strict then
+        self = "^" .. self .. "$"
+    end
+    return self
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-lpeg'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local lpeg = require("lpeg")
+
+lpeg.patterns  = lpeg.patterns or { } -- so that we can share
+local patterns = lpeg.patterns
+
+local P, R, S, Ct, C, Cs, Cc, V = lpeg.P, lpeg.R, lpeg.S, lpeg.Ct, lpeg.C, lpeg.Cs, lpeg.Cc, lpeg.V
+local match = lpeg.match
+
+local digit, sign      = R('09'), S('+-')
+local cr, lf, crlf     = P("\r"), P("\n"), P("\r\n")
+local utf8byte         = R("\128\191")
+
+patterns.utf8byte      = utf8byte
+patterns.utf8one       = R("\000\127")
+patterns.utf8two       = R("\194\223") * utf8byte
+patterns.utf8three     = R("\224\239") * utf8byte * utf8byte
+patterns.utf8four      = R("\240\244") * utf8byte * utf8byte * utf8byte
+
+patterns.digit         = digit
+patterns.sign          = sign
+patterns.cardinal      = sign^0 * digit^1
+patterns.integer       = sign^0 * digit^1
+patterns.float         = sign^0 * digit^0 * P('.') * digit^1
+patterns.number        = patterns.float + patterns.integer
+patterns.oct           = P("0") * R("07")^1
+patterns.octal         = patterns.oct
+patterns.HEX           = P("0x") * R("09","AF")^1
+patterns.hex           = P("0x") * R("09","af")^1
+patterns.hexadecimal   = P("0x") * R("09","AF","af")^1
+patterns.lowercase     = R("az")
+patterns.uppercase     = R("AZ")
+patterns.letter        = patterns.lowercase + patterns.uppercase
+patterns.space         = S(" ")
+patterns.eol           = S("\n\r")
+patterns.spacer        = S(" \t\f\v")  -- + string.char(0xc2, 0xa0) if we want utf (cf mail roberto)
+patterns.newline       = crlf + cr + lf
+patterns.nonspace      = 1 - patterns.space
+patterns.nonspacer     = 1 - patterns.spacer
+patterns.whitespace    = patterns.eol + patterns.spacer
+patterns.nonwhitespace = 1 - patterns.whitespace
+patterns.utf8          = patterns.utf8one + patterns.utf8two + patterns.utf8three + patterns.utf8four
+patterns.utfbom        = P('\000\000\254\255') + P('\255\254\000\000') + P('\255\254') + P('\254\255') + P('\239\187\191')
+
+function lpeg.anywhere(pattern) --slightly adapted from website
+    return P { P(pattern) + 1 * V(1) } -- why so complex?
+end
+
+function lpeg.splitter(pattern, action)
+    return (((1-P(pattern))^1)/action+1)^0
+end
+
+local spacing  = patterns.spacer^0 * patterns.newline -- sort of strip
+local empty    = spacing * Cc("")
+local nonempty = Cs((1-spacing)^1) * spacing^-1
+local content  = (empty + nonempty)^1
+
+local capture = Ct(content^0)
+
+function string:splitlines()
+    return match(capture,self)
+end
+
+patterns.textline = content
+
+--~ local p = lpeg.splitat("->",false)  print(match(p,"oeps->what->more"))  -- oeps what more
+--~ local p = lpeg.splitat("->",true)   print(match(p,"oeps->what->more"))  -- oeps what->more
+--~ local p = lpeg.splitat("->",false)  print(match(p,"oeps"))              -- oeps
+--~ local p = lpeg.splitat("->",true)   print(match(p,"oeps"))              -- oeps
+
+local splitters_s, splitters_m = { }, { }
+
+local function splitat(separator,single)
+    local splitter = (single and splitters_s[separator]) or splitters_m[separator]
+    if not splitter then
+        separator = P(separator)
+        if single then
+            local other, any = C((1 - separator)^0), P(1)
+            splitter = other * (separator * C(any^0) + "") -- ?
+            splitters_s[separator] = splitter
+        else
+            local other = C((1 - separator)^0)
+            splitter = other * (separator * other)^0
+            splitters_m[separator] = splitter
+        end
+    end
+    return splitter
+end
+
+lpeg.splitat = splitat
+
+local cache = { }
+
+function lpeg.split(separator,str)
+    local c = cache[separator]
+    if not c then
+        c = Ct(splitat(separator))
+        cache[separator] = c
+    end
+    return match(c,str)
+end
+
+function string:split(separator)
+    local c = cache[separator]
+    if not c then
+        c = Ct(splitat(separator))
+        cache[separator] = c
+    end
+    return match(c,self)
+end
+
+lpeg.splitters = cache
+
+local cache = { }
+
+function lpeg.checkedsplit(separator,str)
+    local c = cache[separator]
+    if not c then
+        separator = P(separator)
+        local other = C((1 - separator)^0)
+        c = Ct(separator^0 * other * (separator^1 * other)^0)
+        cache[separator] = c
+    end
+    return match(c,str)
+end
+
+function string:checkedsplit(separator)
+    local c = cache[separator]
+    if not c then
+        separator = P(separator)
+        local other = C((1 - separator)^0)
+        c = Ct(separator^0 * other * (separator^1 * other)^0)
+        cache[separator] = c
+    end
+    return match(c,self)
+end
+
+--~ function lpeg.append(list,pp)
+--~     local p = pp
+--~     for l=1,#list do
+--~         if p then
+--~             p = p + P(list[l])
+--~         else
+--~             p = P(list[l])
+--~         end
+--~     end
+--~     return p
+--~ end
+
+--~ from roberto's site:
+
+local f1 = string.byte
+
+local function f2(s) local c1, c2         = f1(s,1,2) return   c1 * 64 + c2                       -    12416 end
+local function f3(s) local c1, c2, c3     = f1(s,1,3) return  (c1 * 64 + c2) * 64 + c3            -   925824 end
+local function f4(s) local c1, c2, c3, c4 = f1(s,1,4) return ((c1 * 64 + c2) * 64 + c3) * 64 + c4 - 63447168 end
+
+patterns.utf8byte = patterns.utf8one/f1 + patterns.utf8two/f2 + patterns.utf8three/f3 + patterns.utf8four/f4
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-table'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+table.join = table.concat
+
+local concat, sort, insert, remove = table.concat, table.sort, table.insert, table.remove
+local format, find, gsub, lower, dump, match = string.format, string.find, string.gsub, string.lower, string.dump, string.match
+local getmetatable, setmetatable = getmetatable, setmetatable
+local type, next, tostring, tonumber, ipairs = type, next, tostring, tonumber, ipairs
+
+-- Starting with version 5.2 Lua no longer provide ipairs, which makes
+-- sense. As we already used the for loop and # in most places the
+-- impact on ConTeXt was not that large; the remaining ipairs already
+-- have been replaced. In a similar fashio we also hardly used pairs.
+--
+-- Just in case, we provide the fallbacks as discussed in Programming
+-- in Lua (http://www.lua.org/pil/7.3.html):
+
+if not ipairs then
+
+    -- for k, v in ipairs(t) do                ... end
+    -- for k=1,#t            do local v = t[k] ... end
+
+    local function iterate(a,i)
+        i = i + 1
+        local v = a[i]
+        if v ~= nil then
+            return i, v --, nil
+        end
+    end
+
+    function ipairs(a)
+        return iterate, a, 0
+    end
+
+end
+
+if not pairs then
+
+    -- for k, v in pairs(t) do ... end
+    -- for k, v in next, t  do ... end
+
+    function pairs(t)
+        return next, t -- , nil
+    end
+
+end
+
+-- Also, unpack has been moved to the table table, and for compatiility
+-- reasons we provide both now.
+
+if not table.unpack then
+    table.unpack = _G.unpack
+elseif not unpack then
+    _G.unpack = table.unpack
+end
+
+-- extra functions, some might go (when not used)
+
+function table.strip(tab)
+    local lst = { }
+    for i=1,#tab do
+        local s = gsub(tab[i],"^%s*(.-)%s*$","%1")
+        if s == "" then
+            -- skip this one
+        else
+            lst[#lst+1] = s
+        end
+    end
+    return lst
+end
+
+function table.keys(t)
+    local k = { }
+    for key, _ in next, t do
+        k[#k+1] = key
+    end
+    return k
+end
+
+local function compare(a,b)
+    return (tostring(a) < tostring(b))
+end
+
+local function sortedkeys(tab)
+    local srt, kind = { }, 0 -- 0=unknown 1=string, 2=number 3=mixed
+    for key,_ in next, tab do
+        srt[#srt+1] = key
+        if kind == 3 then
+            -- no further check
+        else
+            local tkey = type(key)
+            if tkey == "string" then
+            --  if kind == 2 then kind = 3 else kind = 1 end
+                kind = (kind == 2 and 3) or 1
+            elseif tkey == "number" then
+            --  if kind == 1 then kind = 3 else kind = 2 end
+                kind = (kind == 1 and 3) or 2
+            else
+                kind = 3
+            end
+        end
+    end
+    if kind == 0 or kind == 3 then
+        sort(srt,compare)
+    else
+        sort(srt)
+    end
+    return srt
+end
+
+local function sortedhashkeys(tab) -- fast one
+    local srt = { }
+    for key,_ in next, tab do
+        srt[#srt+1] = key
+    end
+    sort(srt)
+    return srt
+end
+
+table.sortedkeys     = sortedkeys
+table.sortedhashkeys = sortedhashkeys
+
+function table.sortedhash(t)
+    local s = sortedhashkeys(t) -- maybe just sortedkeys
+    local n = 0
+    local function kv(s)
+        n = n + 1
+        local k = s[n]
+        return k, t[k]
+    end
+    return kv, s
+end
+
+table.sortedpairs = table.sortedhash
+
+function table.append(t, list)
+    for _,v in next, list do
+        insert(t,v)
+    end
+end
+
+function table.prepend(t, list)
+    for k,v in next, list do
+        insert(t,k,v)
+    end
+end
+
+function table.merge(t, ...) -- first one is target
+    t = t or {}
+    local lst = {...}
+    for i=1,#lst do
+        for k, v in next, lst[i] do
+            t[k] = v
+        end
+    end
+    return t
+end
+
+function table.merged(...)
+    local tmp, lst = { }, {...}
+    for i=1,#lst do
+        for k, v in next, lst[i] do
+            tmp[k] = v
+        end
+    end
+    return tmp
+end
+
+function table.imerge(t, ...)
+    local lst = {...}
+    for i=1,#lst do
+        local nst = lst[i]
+        for j=1,#nst do
+            t[#t+1] = nst[j]
+        end
+    end
+    return t
+end
+
+function table.imerged(...)
+    local tmp, lst = { }, {...}
+    for i=1,#lst do
+        local nst = lst[i]
+        for j=1,#nst do
+            tmp[#tmp+1] = nst[j]
+        end
+    end
+    return tmp
+end
+
+local function fastcopy(old) -- fast one
+    if old then
+        local new = { }
+        for k,v in next, old do
+            if type(v) == "table" then
+                new[k] = fastcopy(v) -- was just table.copy
+            else
+                new[k] = v
+            end
+        end
+        -- optional second arg
+        local mt = getmetatable(old)
+        if mt then
+            setmetatable(new,mt)
+        end
+        return new
+    else
+        return { }
+    end
+end
+
+local function copy(t, tables) -- taken from lua wiki, slightly adapted
+    tables = tables or { }
+    local tcopy = {}
+    if not tables[t] then
+        tables[t] = tcopy
+    end
+    for i,v in next, t do -- brrr, what happens with sparse indexed
+        if type(i) == "table" then
+            if tables[i] then
+                i = tables[i]
+            else
+                i = copy(i, tables)
+            end
+        end
+        if type(v) ~= "table" then
+            tcopy[i] = v
+        elseif tables[v] then
+            tcopy[i] = tables[v]
+        else
+            tcopy[i] = copy(v, tables)
+        end
+    end
+    local mt = getmetatable(t)
+    if mt then
+        setmetatable(tcopy,mt)
+    end
+    return tcopy
+end
+
+table.fastcopy = fastcopy
+table.copy     = copy
+
+-- roughly: copy-loop : unpack : sub == 0.9 : 0.4 : 0.45 (so in critical apps, use unpack)
+
+function table.sub(t,i,j)
+    return { unpack(t,i,j) }
+end
+
+function table.replace(a,b)
+    for k,v in next, b do
+        a[k] = v
+    end
+end
+
+-- slower than #t on indexed tables (#t only returns the size of the numerically indexed slice)
+
+function table.is_empty(t) -- obolete, use inline code instead
+    return not t or not next(t)
+end
+
+function table.one_entry(t) -- obolete, use inline code instead
+    local n = next(t)
+    return n and not next(t,n)
+end
+
+--~ function table.starts_at(t) -- obsolete, not nice anyway
+--~     return ipairs(t,1)(t,0)
+--~ end
+
+function table.tohash(t,value)
+    local h = { }
+    if t then
+        if value == nil then value = true end
+        for _, v in next, t do -- no ipairs here
+            h[v] = value
+        end
+    end
+    return h
+end
+
+function table.fromhash(t)
+    local h = { }
+    for k, v in next, t do -- no ipairs here
+        if v then h[#h+1] = k end
+    end
+    return h
+end
+
+--~ print(table.serialize(t), "\n")
+--~ print(table.serialize(t,"name"), "\n")
+--~ print(table.serialize(t,false), "\n")
+--~ print(table.serialize(t,true), "\n")
+--~ print(table.serialize(t,"name",true), "\n")
+--~ print(table.serialize(t,"name",true,true), "\n")
+
+table.serialize_functions = true
+table.serialize_compact   = true
+table.serialize_inline    = true
+
+local noquotes, hexify, handle, reduce, compact, inline, functions
+
+local reserved = table.tohash { -- intercept a language flaw, no reserved words as key
+    'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'if',
+    'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while',
+}
+
+local function simple_table(t)
+    if #t > 0 then
+        local n = 0
+        for _,v in next, t do
+            n = n + 1
+        end
+        if n == #t then
+            local tt = { }
+            for i=1,#t do
+                local v = t[i]
+                local tv = type(v)
+                if tv == "number" then
+                    if hexify then
+                        tt[#tt+1] = format("0x%04X",v)
+                    else
+                        tt[#tt+1] = tostring(v) -- tostring not needed
+                    end
+                elseif tv == "boolean" then
+                    tt[#tt+1] = tostring(v)
+                elseif tv == "string" then
+                    tt[#tt+1] = format("%q",v)
+                else
+                    tt = nil
+                    break
+                end
+            end
+            return tt
+        end
+    end
+    return nil
+end
+
+-- Because this is a core function of mkiv I moved some function calls
+-- inline.
+--
+-- twice as fast in a test:
+--
+-- local propername = lpeg.P(lpeg.R("AZ","az","__") * lpeg.R("09","AZ","az", "__")^0 * lpeg.P(-1) )
+
+-- problem: there no good number_to_string converter with the best resolution
+
+local function do_serialize(root,name,depth,level,indexed)
+    if level > 0 then
+        depth = depth .. " "
+        if indexed then
+            handle(format("%s{",depth))
+        elseif name then
+        --~ handle(format("%s%s={",depth,key(name)))
+            if type(name) == "number" then -- or find(k,"^%d+$") then
+                if hexify then
+                    handle(format("%s[0x%04X]={",depth,name))
+                else
+                    handle(format("%s[%s]={",depth,name))
+                end
+            elseif noquotes and not reserved[name] and find(name,"^%a[%w%_]*$") then
+                handle(format("%s%s={",depth,name))
+            else
+                handle(format("%s[%q]={",depth,name))
+            end
+        else
+            handle(format("%s{",depth))
+        end
+    end
+    -- we could check for k (index) being number (cardinal)
+    if root and next(root) then
+        local first, last = nil, 0 -- #root cannot be trusted here (will be ok in 5.2 when ipairs is gone)
+        if compact then
+            -- NOT: for k=1,#root do (we need to quit at nil)
+            for k,v in ipairs(root) do -- can we use next?
+                if not first then first = k end
+                last = last + 1
+            end
+        end
+        local sk = sortedkeys(root)
+        for i=1,#sk do
+            local k = sk[i]
+            local v = root[k]
+            --~ if v == root then
+                -- circular
+            --~ else
+            local t = type(v)
+            if compact and first and type(k) == "number" and k >= first and k <= last then
+                if t == "number" then
+                    if hexify then
+                        handle(format("%s 0x%04X,",depth,v))
+                    else
+                        handle(format("%s %s,",depth,v)) -- %.99g
+                    end
+                elseif t == "string" then
+                    if reduce and tonumber(v) then
+                        handle(format("%s %s,",depth,v))
+                    else
+                        handle(format("%s %q,",depth,v))
+                    end
+                elseif t == "table" then
+                    if not next(v) then
+                        handle(format("%s {},",depth))
+                    elseif inline then -- and #t > 0
+                        local st = simple_table(v)
+                        if st then
+                            handle(format("%s { %s },",depth,concat(st,", ")))
+                        else
+                            do_serialize(v,k,depth,level+1,true)
+                        end
+                    else
+                        do_serialize(v,k,depth,level+1,true)
+                    end
+                elseif t == "boolean" then
+                    handle(format("%s %s,",depth,tostring(v)))
+                elseif t == "function" then
+                    if functions then
+                        handle(format('%s loadstring(%q),',depth,dump(v)))
+                    else
+                        handle(format('%s "function",',depth))
+                    end
+                else
+                    handle(format("%s %q,",depth,tostring(v)))
+                end
+            elseif k == "__p__" then -- parent
+                if false then
+                    handle(format("%s __p__=nil,",depth))
+                end
+            elseif t == "number" then
+            --~ if hexify then
+            --~     handle(format("%s %s=0x%04X,",depth,key(k),v))
+            --~ else
+            --~     handle(format("%s %s=%s,",depth,key(k),v)) -- %.99g
+            --~ end
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s [%s]=%s,",depth,k,v)) -- %.99g
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    if hexify then
+                        handle(format("%s %s=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s %s=%s,",depth,k,v)) -- %.99g
+                    end
+                else
+                    if hexify then
+                        handle(format("%s [%q]=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%s,",depth,k,v)) -- %.99g
+                    end
+                end
+            elseif t == "string" then
+                if reduce and tonumber(v) then
+                --~ handle(format("%s %s=%s,",depth,key(k),v))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=%s,",depth,k,v))
+                        else
+                            handle(format("%s [%s]=%s,",depth,k,v))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=%s,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%s,",depth,k,v))
+                    end
+                else
+                --~ handle(format("%s %s=%q,",depth,key(k),v))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=%q,",depth,k,v))
+                        else
+                            handle(format("%s [%s]=%q,",depth,k,v))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=%q,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%q,",depth,k,v))
+                    end
+                end
+            elseif t == "table" then
+                if not next(v) then
+                    --~ handle(format("%s %s={},",depth,key(k)))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]={},",depth,k))
+                        else
+                            handle(format("%s [%s]={},",depth,k))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s={},",depth,k))
+                    else
+                        handle(format("%s [%q]={},",depth,k))
+                    end
+                elseif inline then
+                    local st = simple_table(v)
+                    if st then
+                    --~ handle(format("%s %s={ %s },",depth,key(k),concat(st,", ")))
+                        if type(k) == "number" then -- or find(k,"^%d+$") then
+                            if hexify then
+                                handle(format("%s [0x%04X]={ %s },",depth,k,concat(st,", ")))
+                            else
+                                handle(format("%s [%s]={ %s },",depth,k,concat(st,", ")))
+                            end
+                        elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                            handle(format("%s %s={ %s },",depth,k,concat(st,", ")))
+                        else
+                            handle(format("%s [%q]={ %s },",depth,k,concat(st,", ")))
+                        end
+                    else
+                        do_serialize(v,k,depth,level+1)
+                    end
+                else
+                    do_serialize(v,k,depth,level+1)
+                end
+            elseif t == "boolean" then
+            --~ handle(format("%s %s=%s,",depth,key(k),tostring(v)))
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=%s,",depth,k,tostring(v)))
+                    else
+                        handle(format("%s [%s]=%s,",depth,k,tostring(v)))
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    handle(format("%s %s=%s,",depth,k,tostring(v)))
+                else
+                    handle(format("%s [%q]=%s,",depth,k,tostring(v)))
+                end
+            elseif t == "function" then
+                if functions then
+                    --~ handle(format('%s %s=loadstring(%q),',depth,key(k),dump(v)))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=loadstring(%q),",depth,k,dump(v)))
+                        else
+                            handle(format("%s [%s]=loadstring(%q),",depth,k,dump(v)))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=loadstring(%q),",depth,k,dump(v)))
+                    else
+                        handle(format("%s [%q]=loadstring(%q),",depth,k,dump(v)))
+                    end
+                end
+            else
+                --~ handle(format("%s %s=%q,",depth,key(k),tostring(v)))
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=%q,",depth,k,tostring(v)))
+                    else
+                        handle(format("%s [%s]=%q,",depth,k,tostring(v)))
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    handle(format("%s %s=%q,",depth,k,tostring(v)))
+                else
+                    handle(format("%s [%q]=%q,",depth,k,tostring(v)))
+                end
+            end
+            --~ end
+        end
+    end
+   if level > 0 then
+        handle(format("%s},",depth))
+    end
+end
+
+-- replacing handle by a direct t[#t+1] = ... (plus test) is not much
+-- faster (0.03 on 1.00 for zapfino.tma)
+
+local function serialize(root,name,_handle,_reduce,_noquotes,_hexify)
+    noquotes = _noquotes
+    hexify = _hexify
+    handle = _handle or print
+    reduce = _reduce or false
+    compact = table.serialize_compact
+    inline  = compact and table.serialize_inline
+    functions = table.serialize_functions
+    local tname = type(name)
+    if tname == "string" then
+        if name == "return" then
+            handle("return {")
+        else
+            handle(name .. "={")
+        end
+    elseif tname == "number" then
+        if hexify then
+            handle(format("[0x%04X]={",name))
+        else
+            handle("[" .. name .. "]={")
+        end
+    elseif tname == "boolean" then
+        if name then
+            handle("return {")
+        else
+            handle("{")
+        end
+    else
+        handle("t={")
+    end
+    if root and next(root) then
+        do_serialize(root,name,"",0,indexed)
+    end
+    handle("}")
+end
+
+--~ name:
+--~
+--~ true     : return     { }
+--~ false    :            { }
+--~ nil      : t        = { }
+--~ string   : string   = { }
+--~ 'return' : return     { }
+--~ number   : [number] = { }
+
+function table.serialize(root,name,reduce,noquotes,hexify)
+    local t = { }
+    local function flush(s)
+        t[#t+1] = s
+    end
+    serialize(root,name,flush,reduce,noquotes,hexify)
+    return concat(t,"\n")
+end
+
+function table.tohandle(handle,root,name,reduce,noquotes,hexify)
+    serialize(root,name,handle,reduce,noquotes,hexify)
+end
+
+-- sometimes tables are real use (zapfino extra pro is some 85M) in which
+-- case a stepwise serialization is nice; actually, we could consider:
+--
+-- for line in table.serializer(root,name,reduce,noquotes) do
+--    ...(line)
+-- end
+--
+-- so this is on the todo list
+
+table.tofile_maxtab = 2*1024
+
+function table.tofile(filename,root,name,reduce,noquotes,hexify)
+    local f = io.open(filename,'w')
+    if f then
+        local maxtab = table.tofile_maxtab
+        if maxtab > 1 then
+            local t = { }
+            local function flush(s)
+                t[#t+1] = s
+                if #t > maxtab then
+                    f:write(concat(t,"\n"),"\n") -- hm, write(sometable) should be nice
+                    t = { }
+                end
+            end
+            serialize(root,name,flush,reduce,noquotes,hexify)
+            f:write(concat(t,"\n"),"\n")
+        else
+            local function flush(s)
+                f:write(s,"\n")
+            end
+            serialize(root,name,flush,reduce,noquotes,hexify)
+        end
+        f:close()
+    end
+end
+
+local function flatten(t,f,complete) -- is this used? meybe a variant with next, ...
+    for i=1,#t do
+        local v = t[i]
+        if type(v) == "table" then
+            if complete or type(v[1]) == "table" then
+                flatten(v,f,complete)
+            else
+                f[#f+1] = v
+            end
+        else
+            f[#f+1] = v
+        end
+    end
+end
+
+function table.flatten(t)
+    local f = { }
+    flatten(t,f,true)
+    return f
+end
+
+function table.unnest(t) -- bad name
+    local f = { }
+    flatten(t,f,false)
+    return f
+end
+
+table.flatten_one_level = table.unnest
+
+-- a better one:
+
+local function flattened(t,f)
+    if not f then
+        f = { }
+    end
+    for k, v in next, t do
+        if type(v) == "table" then
+            flattened(v,f)
+        else
+            f[k] = v
+        end
+    end
+    return f
+end
+
+table.flattened = flattened
+
+-- the next three may disappear
+
+function table.remove_value(t,value) -- todo: n
+    if value then
+        for i=1,#t do
+            if t[i] == value then
+                remove(t,i)
+                -- remove all, so no: return
+            end
+        end
+    end
+end
+
+function table.insert_before_value(t,value,str)
+    if str then
+        if value then
+            for i=1,#t do
+                if t[i] == value then
+                    insert(t,i,str)
+                    return
+                end
+            end
+        end
+        insert(t,1,str)
+    elseif value then
+        insert(t,1,value)
+    end
+end
+
+function table.insert_after_value(t,value,str)
+    if str then
+        if value then
+            for i=1,#t do
+                if t[i] == value then
+                    insert(t,i+1,str)
+                    return
+                end
+            end
+        end
+        t[#t+1] = str
+    elseif value then
+        t[#t+1] = value
+    end
+end
+
+local function are_equal(a,b,n,m) -- indexed
+    if a and b and #a == #b then
+        n = n or 1
+        m = m or #a
+        for i=n,m do
+            local ai, bi = a[i], b[i]
+            if ai==bi then
+                -- same
+            elseif type(ai)=="table" and type(bi)=="table" then
+                if not are_equal(ai,bi) then
+                    return false
+                end
+            else
+                return false
+            end
+        end
+        return true
+    else
+        return false
+    end
+end
+
+local function identical(a,b) -- assumes same structure
+    for ka, va in next, a do
+        local vb = b[k]
+        if va == vb then
+            -- same
+        elseif type(va) == "table" and  type(vb) == "table" then
+            if not identical(va,vb) then
+                return false
+            end
+        else
+            return false
+        end
+    end
+    return true
+end
+
+table.are_equal = are_equal
+table.identical = identical
+
+-- maybe also make a combined one
+
+function table.compact(t)
+    if t then
+        for k,v in next, t do
+            if not next(v) then
+                t[k] = nil
+            end
+        end
+    end
+end
+
+function table.contains(t, v)
+    if t then
+        for i=1, #t do
+            if t[i] == v then
+                return i
+            end
+        end
+    end
+    return false
+end
+
+function table.count(t)
+    local n, e = 0, next(t)
+    while e do
+        n, e = n + 1, next(t,e)
+    end
+    return n
+end
+
+function table.swapped(t)
+    local s = { }
+    for k, v in next, t do
+        s[v] = k
+    end
+    return s
+end
+
+--~ function table.are_equal(a,b)
+--~     return table.serialize(a) == table.serialize(b)
+--~ end
+
+function table.clone(t,p) -- t is optional or nil or table
+    if not p then
+        t, p = { }, t or { }
+    elseif not t then
+        t = { }
+    end
+    setmetatable(t, { __index = function(_,key) return p[key] end }) -- why not __index = p ?
+    return t
+end
+
+function table.hexed(t,seperator)
+    local tt = { }
+    for i=1,#t do tt[i] = format("0x%04X",t[i]) end
+    return concat(tt,seperator or " ")
+end
+
+function table.reverse_hash(h)
+    local r = { }
+    for k,v in next, h do
+        r[v] = lower(gsub(k," ",""))
+    end
+    return r
+end
+
+function table.reverse(t)
+    local tt = { }
+    if #t > 0 then
+        for i=#t,1,-1 do
+            tt[#tt+1] = t[i]
+        end
+    end
+    return tt
+end
+
+function table.insert_before_value(t,value,extra)
+    for i=1,#t do
+        if t[i] == extra then
+            remove(t,i)
+        end
+    end
+    for i=1,#t do
+        if t[i] == value then
+            insert(t,i,extra)
+            return
+        end
+    end
+    insert(t,1,extra)
+end
+
+function table.insert_after_value(t,value,extra)
+    for i=1,#t do
+        if t[i] == extra then
+            remove(t,i)
+        end
+    end
+    for i=1,#t do
+        if t[i] == value then
+            insert(t,i+1,extra)
+            return
+        end
+    end
+    insert(t,#t+1,extra)
+end
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-io'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local byte, find, gsub = string.byte, string.find, string.gsub
+
+if string.find(os.getenv("PATH"),";") then
+    io.fileseparator, io.pathseparator = "\\", ";"
+else
+    io.fileseparator, io.pathseparator = "/" , ":"
+end
+
+function io.loaddata(filename,textmode)
+    local f = io.open(filename,(textmode and 'r') or 'rb')
+    if f then
+    --  collectgarbage("step") -- sometimes makes a big difference in mem consumption
+        local data = f:read('*all')
+    --  garbagecollector.check(data)
+        f:close()
+        return data
+    else
+        return nil
+    end
+end
+
+function io.savedata(filename,data,joiner)
+    local f = io.open(filename,"wb")
+    if f then
+        if type(data) == "table" then
+            f:write(table.join(data,joiner or ""))
+        elseif type(data) == "function" then
+            data(f)
+        else
+            f:write(data or "")
+        end
+        f:close()
+        return true
+    else
+        return false
+    end
+end
+
+function io.exists(filename)
+    local f = io.open(filename)
+    if f == nil then
+        return false
+    else
+        assert(f:close())
+        return true
+    end
+end
+
+function io.size(filename)
+    local f = io.open(filename)
+    if f == nil then
+        return 0
+    else
+        local s = f:seek("end")
+        assert(f:close())
+        return s
+    end
+end
+
+function io.noflines(f)
+    local n = 0
+    for _ in f:lines() do
+        n = n + 1
+    end
+    f:seek('set',0)
+    return n
+end
+
+local nextchar = {
+    [ 4] = function(f)
+        return f:read(1,1,1,1)
+    end,
+    [ 2] = function(f)
+        return f:read(1,1)
+    end,
+    [ 1] = function(f)
+        return f:read(1)
+    end,
+    [-2] = function(f)
+        local a, b = f:read(1,1)
+        return b, a
+    end,
+    [-4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        return d, c, b, a
+    end
+}
+
+function io.characters(f,n)
+    if f then
+        return nextchar[n or 1], f
+    else
+        return nil, nil
+    end
+end
+
+local nextbyte = {
+    [4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        if d then
+            return byte(a), byte(b), byte(c), byte(d)
+        else
+            return nil, nil, nil, nil
+        end
+    end,
+    [2] = function(f)
+        local a, b = f:read(1,1)
+        if b then
+            return byte(a), byte(b)
+        else
+            return nil, nil
+        end
+    end,
+    [1] = function (f)
+        local a = f:read(1)
+        if a then
+            return byte(a)
+        else
+            return nil
+        end
+    end,
+    [-2] = function (f)
+        local a, b = f:read(1,1)
+        if b then
+            return byte(b), byte(a)
+        else
+            return nil, nil
+        end
+    end,
+    [-4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        if d then
+            return byte(d), byte(c), byte(b), byte(a)
+        else
+            return nil, nil, nil, nil
+        end
+    end
+}
+
+function io.bytes(f,n)
+    if f then
+        return nextbyte[n or 1], f
+    else
+        return nil, nil
+    end
+end
+
+function io.ask(question,default,options)
+    while true do
+        io.write(question)
+        if options then
+            io.write(string.format(" [%s]",table.concat(options,"|")))
+        end
+        if default then
+            io.write(string.format(" [%s]",default))
+        end
+        io.write(string.format(" "))
+        local answer = io.read()
+        answer = gsub(answer,"^%s*(.*)%s*$","%1")
+        if answer == "" and default then
+            return default
+        elseif not options then
+            return answer
+        else
+            for k=1,#options do
+                if options[k] == answer then
+                    return answer
+                end
+            end
+            local pattern = "^" .. answer
+            for k=1,#options do
+                local v = options[k]
+                if find(v,pattern) then
+                    return v
+                end
+            end
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-number'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local tostring = tostring
+local format, floor, insert, match = string.format, math.floor, table.insert, string.match
+local lpegmatch = lpeg.match
+
+number = number or { }
+
+-- a,b,c,d,e,f = number.toset(100101)
+
+function number.toset(n)
+    return match(tostring(n),"(.?)(.?)(.?)(.?)(.?)(.?)(.?)(.?)")
+end
+
+function number.toevenhex(n)
+    local s = format("%X",n)
+    if #s % 2 == 0 then
+        return s
+    else
+        return "0" .. s
+    end
+end
+
+-- the lpeg way is slower on 8 digits, but faster on 4 digits, some 7.5%
+-- on
+--
+-- for i=1,1000000 do
+--     local a,b,c,d,e,f,g,h = number.toset(12345678)
+--     local a,b,c,d         = number.toset(1234)
+--     local a,b,c           = number.toset(123)
+-- end
+--
+-- of course dedicated "(.)(.)(.)(.)" matches are even faster
+
+local one = lpeg.C(1-lpeg.S(''))^1
+
+function number.toset(n)
+    return lpegmatch(one,tostring(n))
+end
+
+function number.bits(n,zero)
+    local t, i = { }, (zero and 0) or 1
+    while n > 0 do
+        local m = n % 2
+        if m > 0 then
+            insert(t,1,i)
+        end
+        n = floor(n/2)
+        i = i + 1
+    end
+    return t
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-set'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+set = set or { }
+
+local nums   = { }
+local tabs   = { }
+local concat = table.concat
+local next, type = next, type
+
+set.create = table.tohash
+
+function set.tonumber(t)
+    if next(t) then
+        local s = ""
+    --  we could save mem by sorting, but it slows down
+        for k, v in next, t do
+            if v then
+            --  why bother about the leading space
+                s = s .. " " .. k
+            end
+        end
+        local n = nums[s]
+        if not n then
+            n = #tabs + 1
+            tabs[n] = t
+            nums[s] = n
+        end
+        return n
+    else
+        return 0
+    end
+end
+
+function set.totable(n)
+    if n == 0 then
+        return { }
+    else
+        return tabs[n] or { }
+    end
+end
+
+function set.tolist(n)
+    if n == 0 or not tabs[n] then
+        return ""
+    else
+        local t = { }
+        for k, v in next, tabs[n] do
+            if v then
+                t[#t+1] = k
+            end
+        end
+        return concat(t," ")
+    end
+end
+
+function set.contains(n,s)
+    if type(n) == "table" then
+        return n[s]
+    elseif n == 0 then
+        return false
+    else
+        local t = tabs[n]
+        return t and t[s]
+    end
+end
+
+--~ local c = set.create{'aap','noot','mies'}
+--~ local s = set.tonumber(c)
+--~ local t = set.totable(s)
+--~ print(t['aap'])
+--~ local c = set.create{'zus','wim','jet'}
+--~ local s = set.tonumber(c)
+--~ local t = set.totable(s)
+--~ print(t['aap'])
+--~ print(t['jet'])
+--~ print(set.contains(t,'jet'))
+--~ print(set.contains(t,'aap'))
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-os'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- maybe build io.flush in os.execute
+
+local find, format, gsub = string.find, string.format, string.gsub
+local random, ceil = math.random, math.ceil
+
+local execute, spawn, exec, ioflush = os.execute, os.spawn or os.execute, os.exec or os.execute, io.flush
+
+function os.execute(...) ioflush() return execute(...) end
+function os.spawn  (...) ioflush() return spawn  (...) end
+function os.exec   (...) ioflush() return exec   (...) end
+
+function os.resultof(command)
+    ioflush() -- else messed up logging
+    local handle = io.popen(command,"r")
+    if not handle then
+    --  print("unknown command '".. command .. "' in os.resultof")
+        return ""
+    else
+        return handle:read("*all") or ""
+    end
+end
+
+--~ os.type     : windows | unix (new, we already guessed os.platform)
+--~ os.name     : windows | msdos | linux | macosx | solaris | .. | generic (new)
+--~ os.platform : extended os.name with architecture
+
+if not io.fileseparator then
+    if find(os.getenv("PATH"),";") then
+        io.fileseparator, io.pathseparator, os.type = "\\", ";", os.type or "mswin"
+    else
+        io.fileseparator, io.pathseparator, os.type = "/" , ":", os.type or "unix"
+    end
+end
+
+os.type = os.type or (io.pathseparator == ";"       and "windows") or "unix"
+os.name = os.name or (os.type          == "windows" and "mswin"  ) or "linux"
+
+if os.type == "windows" then
+    os.libsuffix, os.binsuffix = 'dll', 'exe'
+else
+    os.libsuffix, os.binsuffix = 'so', ''
+end
+
+function os.launch(str)
+    if os.type == "windows" then
+        os.execute("start " .. str) -- os.spawn ?
+    else
+        os.execute(str .. " &")     -- os.spawn ?
+    end
+end
+
+if not os.times then
+    -- utime  = user time
+    -- stime  = system time
+    -- cutime = children user time
+    -- cstime = children system time
+    function os.times()
+        return {
+            utime  = os.gettimeofday(), -- user
+            stime  = 0,                 -- system
+            cutime = 0,                 -- children user
+            cstime = 0,                 -- children system
+        }
+    end
+end
+
+os.gettimeofday = os.gettimeofday or os.clock
+
+local startuptime = os.gettimeofday()
+
+function os.runtime()
+    return os.gettimeofday() - startuptime
+end
+
+--~ print(os.gettimeofday()-os.time())
+--~ os.sleep(1.234)
+--~ print (">>",os.runtime())
+--~ print(os.date("%H:%M:%S",os.gettimeofday()))
+--~ print(os.date("%H:%M:%S",os.time()))
+
+-- no need for function anymore as we have more clever code and helpers now
+-- this metatable trickery might as well disappear
+
+os.resolvers = os.resolvers or { }
+
+local resolvers = os.resolvers
+
+local osmt = getmetatable(os) or { __index = function(t,k) t[k] = "unset" return "unset" end } -- maybe nil
+local osix = osmt.__index
+
+osmt.__index = function(t,k)
+    return (resolvers[k] or osix)(t,k)
+end
+
+setmetatable(os,osmt)
+
+if not os.setenv then
+
+    -- we still store them but they won't be seen in
+    -- child processes although we might pass them some day
+    -- using command concatination
+
+    local env, getenv = { }, os.getenv
+
+    function os.setenv(k,v)
+        env[k] = v
+    end
+
+    function os.getenv(k)
+        return env[k] or getenv(k)
+    end
+
+end
+
+-- we can use HOSTTYPE on some platforms
+
+local name, platform = os.name or "linux", os.getenv("MTX_PLATFORM") or ""
+
+local function guess()
+    local architecture = os.resultof("uname -m") or ""
+    if architecture ~= "" then
+        return architecture
+    end
+    architecture = os.getenv("HOSTTYPE") or ""
+    if architecture ~= "" then
+        return architecture
+    end
+    return os.resultof("echo $HOSTTYPE") or ""
+end
+
+if platform ~= "" then
+
+    os.platform = platform
+
+elseif os.type == "windows" then
+
+    -- we could set the variable directly, no function needed here
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.getenv("PROCESSOR_ARCHITECTURE") or ""
+        if find(architecture,"AMD64") then
+            platform = "mswin-64"
+        else
+            platform = "mswin"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "linux" then
+
+    function os.resolvers.platform(t,k)
+        -- we sometims have HOSTTYPE set so let's check that first
+        local platform, architecture = "", os.getenv("HOSTTYPE") or os.resultof("uname -m") or ""
+        if find(architecture,"x86_64") then
+            platform = "linux-64"
+        elseif find(architecture,"ppc") then
+            platform = "linux-ppc"
+        else
+            platform = "linux"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "macosx" then
+
+    --[[
+        Identifying the architecture of OSX is quite a mess and this
+        is the best we can come up with. For some reason $HOSTTYPE is
+        a kind of pseudo environment variable, not known to the current
+        environment. And yes, uname cannot be trusted either, so there
+        is a change that you end up with a 32 bit run on a 64 bit system.
+        Also, some proper 64 bit intel macs are too cheap (low-end) and
+        therefore not permitted to run the 64 bit kernel.
+      ]]--
+
+    function os.resolvers.platform(t,k)
+     -- local platform, architecture = "", os.getenv("HOSTTYPE") or ""
+     -- if architecture == "" then
+     --     architecture = os.resultof("echo $HOSTTYPE") or ""
+     -- end
+        local platform, architecture = "", os.resultof("echo $HOSTTYPE") or ""
+        if architecture == "" then
+         -- print("\nI have no clue what kind of OSX you're running so let's assume an 32 bit intel.\n")
+            platform = "osx-intel"
+        elseif find(architecture,"i386") then
+            platform = "osx-intel"
+        elseif find(architecture,"x86_64") then
+            platform = "osx-64"
+        else
+            platform = "osx-ppc"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "sunos" then
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.resultof("uname -m") or ""
+        if find(architecture,"sparc") then
+            platform = "solaris-sparc"
+        else -- if architecture == 'i86pc'
+            platform = "solaris-intel"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "freebsd" then
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.resultof("uname -m") or ""
+        if find(architecture,"amd64") then
+            platform = "freebsd-amd64"
+        else
+            platform = "freebsd"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "kfreebsd" then
+
+    function os.resolvers.platform(t,k)
+        -- we sometims have HOSTTYPE set so let's check that first
+        local platform, architecture = "", os.getenv("HOSTTYPE") or os.resultof("uname -m") or ""
+        if find(architecture,"x86_64") then
+            platform = "kfreebsd-64"
+        else
+            platform = "kfreebsd-i386"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+else
+
+    -- platform = "linux"
+    -- os.setenv("MTX_PLATFORM",platform)
+    -- os.platform = platform
+
+    function os.resolvers.platform(t,k)
+        local platform = "linux"
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+end
+
+-- beware, we set the randomseed
+
+-- from wikipedia: Version 4 UUIDs use a scheme relying only on random numbers. This algorithm sets the
+-- version number as well as two reserved bits. All other bits are set using a random or pseudorandom
+-- data source. Version 4 UUIDs have the form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx with hexadecimal
+-- digits x and hexadecimal digits 8, 9, A, or B for y. e.g. f47ac10b-58cc-4372-a567-0e02b2c3d479.
+--
+-- as we don't call this function too often there is not so much risk on repetition
+
+local t = { 8, 9, "a", "b" }
+
+function os.uuid()
+    return format("%04x%04x-4%03x-%s%03x-%04x-%04x%04x%04x",
+        random(0xFFFF),random(0xFFFF),
+        random(0x0FFF),
+        t[ceil(random(4))] or 8,random(0x0FFF),
+        random(0xFFFF),
+        random(0xFFFF),random(0xFFFF),random(0xFFFF)
+    )
+end
+
+local d
+
+function os.timezone(delta)
+    d = d or tonumber(tonumber(os.date("%H")-os.date("!%H")))
+    if delta then
+        if d > 0 then
+            return format("+%02i:00",d)
+        else
+            return format("-%02i:00",-d)
+        end
+    else
+        return 1
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-file'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- needs a cleanup
+
+file = file or { }
+
+local concat = table.concat
+local find, gmatch, match, gsub, sub, char = string.find, string.gmatch, string.match, string.gsub, string.sub, string.char
+local lpegmatch = lpeg.match
+
+function file.removesuffix(filename)
+    return (gsub(filename,"%.[%a%d]+$",""))
+end
+
+function file.addsuffix(filename, suffix)
+    if not suffix or suffix == "" then
+        return filename
+    elseif not find(filename,"%.[%a%d]+$") then
+        return filename .. "." .. suffix
+    else
+        return filename
+    end
+end
+
+function file.replacesuffix(filename, suffix)
+    return (gsub(filename,"%.[%a%d]+$","")) .. "." .. suffix
+end
+
+function file.dirname(name,default)
+    return match(name,"^(.+)[/\\].-$") or (default or "")
+end
+
+function file.basename(name)
+    return match(name,"^.+[/\\](.-)$") or name
+end
+
+function file.nameonly(name)
+    return (gsub(match(name,"^.+[/\\](.-)$") or name,"%..*$",""))
+end
+
+function file.extname(name,default)
+    return match(name,"^.+%.([^/\\]-)$") or default or ""
+end
+
+file.suffix = file.extname
+
+--~ function file.join(...)
+--~     local pth = concat({...},"/")
+--~     pth = gsub(pth,"\\","/")
+--~     local a, b = match(pth,"^(.*://)(.*)$")
+--~     if a and b then
+--~         return a .. gsub(b,"//+","/")
+--~     end
+--~     a, b = match(pth,"^(//)(.*)$")
+--~     if a and b then
+--~         return a .. gsub(b,"//+","/")
+--~     end
+--~     return (gsub(pth,"//+","/"))
+--~ end
+
+local trick_1 = char(1)
+local trick_2 = "^" .. trick_1 .. "/+"
+
+function file.join(...)
+    local lst = { ... }
+    local a, b = lst[1], lst[2]
+    if a == "" then
+        lst[1] = trick_1
+    elseif b and find(a,"^/+$") and find(b,"^/") then
+        lst[1] = ""
+        lst[2] = gsub(b,"^/+","")
+    end
+    local pth = concat(lst,"/")
+    pth = gsub(pth,"\\","/")
+    local a, b = match(pth,"^(.*://)(.*)$")
+    if a and b then
+        return a .. gsub(b,"//+","/")
+    end
+    a, b = match(pth,"^(//)(.*)$")
+    if a and b then
+        return a .. gsub(b,"//+","/")
+    end
+    pth = gsub(pth,trick_2,"")
+    return (gsub(pth,"//+","/"))
+end
+
+--~ print(file.join("//","/y"))
+--~ print(file.join("/","/y"))
+--~ print(file.join("","/y"))
+--~ print(file.join("/x/","/y"))
+--~ print(file.join("x/","/y"))
+--~ print(file.join("http://","/y"))
+--~ print(file.join("http://a","/y"))
+--~ print(file.join("http:///a","/y"))
+--~ print(file.join("//nas-1","/y"))
+
+function file.iswritable(name)
+    local a = lfs.attributes(name) or lfs.attributes(file.dirname(name,"."))
+    return a and sub(a.permissions,2,2) == "w"
+end
+
+function file.isreadable(name)
+    local a = lfs.attributes(name)
+    return a and sub(a.permissions,1,1) == "r"
+end
+
+file.is_readable = file.isreadable
+file.is_writable = file.iswritable
+
+-- todo: lpeg
+
+--~ function file.split_path(str)
+--~     local t = { }
+--~     str = gsub(str,"\\", "/")
+--~     str = gsub(str,"(%a):([;/])", "%1\001%2")
+--~     for name in gmatch(str,"([^;:]+)") do
+--~         if name ~= "" then
+--~             t[#t+1] = gsub(name,"\001",":")
+--~         end
+--~     end
+--~     return t
+--~ end
+
+local checkedsplit = string.checkedsplit
+
+function file.split_path(str,separator)
+    str = gsub(str,"\\","/")
+    return checkedsplit(str,separator or io.pathseparator)
+end
+
+function file.join_path(tab)
+    return concat(tab,io.pathseparator) -- can have trailing //
+end
+
+-- we can hash them weakly
+
+function file.collapse_path(str)
+    str = gsub(str,"\\","/")
+    if find(str,"/") then
+        str = gsub(str,"^%./",(gsub(lfs.currentdir(),"\\","/")) .. "/") -- ./xx in qualified
+        str = gsub(str,"/%./","/")
+        local n, m = 1, 1
+        while n > 0 or m > 0 do
+            str, n = gsub(str,"[^/%.]+/%.%.$","")
+            str, m = gsub(str,"[^/%.]+/%.%./","")
+        end
+        str = gsub(str,"([^/])/$","%1")
+    --  str = gsub(str,"^%./","") -- ./xx in qualified
+        str = gsub(str,"/%.$","")
+    end
+    if str == "" then str = "." end
+    return str
+end
+
+--~ print(file.collapse_path("/a"))
+--~ print(file.collapse_path("a/./b/.."))
+--~ print(file.collapse_path("a/aa/../b/bb"))
+--~ print(file.collapse_path("a/../.."))
+--~ print(file.collapse_path("a/.././././b/.."))
+--~ print(file.collapse_path("a/./././b/.."))
+--~ print(file.collapse_path("a/b/c/../.."))
+
+function file.robustname(str)
+    return (gsub(str,"[^%a%d%/%-%.\\]+","-"))
+end
+
+file.readdata = io.loaddata
+file.savedata = io.savedata
+
+function file.copy(oldname,newname)
+    file.savedata(newname,io.loaddata(oldname))
+end
+
+-- lpeg variants, slightly faster, not always
+
+--~ local period    = lpeg.P(".")
+--~ local slashes   = lpeg.S("\\/")
+--~ local noperiod  = 1-period
+--~ local noslashes = 1-slashes
+--~ local name      = noperiod^1
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.C(noperiod^1) * -1
+
+--~ function file.extname(name)
+--~     return lpegmatch(pattern,name) or ""
+--~ end
+
+--~ local pattern = lpeg.Cs(((period * noperiod^1 * -1)/"" + 1)^1)
+
+--~ function file.removesuffix(name)
+--~     return lpegmatch(pattern,name)
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^1 * lpeg.C(noslashes^1) * -1
+
+--~ function file.basename(name)
+--~     return lpegmatch(pattern,name) or name
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^1 * lpeg.Cp() * noslashes^1 * -1
+
+--~ function file.dirname(name)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return sub(name,1,p-2)
+--~     else
+--~         return ""
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.Cp() * noperiod^1 * -1
+
+--~ function file.addsuffix(name, suffix)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return name
+--~     else
+--~         return name .. "." .. suffix
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.Cp() * noperiod^1 * -1
+
+--~ function file.replacesuffix(name,suffix)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return sub(name,1,p-2) .. "." .. suffix
+--~     else
+--~         return name .. "." .. suffix
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * lpeg.Cp() * ((noperiod^1 * period)^1 * lpeg.Cp() + lpeg.P(true)) * noperiod^1 * -1
+
+--~ function file.nameonly(name)
+--~     local a, b = lpegmatch(pattern,name)
+--~     if b then
+--~         return sub(name,a,b-2)
+--~     elseif a then
+--~         return sub(name,a)
+--~     else
+--~         return name
+--~     end
+--~ end
+
+--~ local test = file.extname
+--~ local test = file.basename
+--~ local test = file.dirname
+--~ local test = file.addsuffix
+--~ local test = file.replacesuffix
+--~ local test = file.nameonly
+
+--~ print(1,test("./a/b/c/abd.def.xxx","!!!"))
+--~ print(2,test("./../b/c/abd.def.xxx","!!!"))
+--~ print(3,test("a/b/c/abd.def.xxx","!!!"))
+--~ print(4,test("a/b/c/def.xxx","!!!"))
+--~ print(5,test("a/b/c/def","!!!"))
+--~ print(6,test("def","!!!"))
+--~ print(7,test("def.xxx","!!!"))
+
+--~ local tim = os.clock() for i=1,250000 do local ext = test("abd.def.xxx","!!!") end print(os.clock()-tim)
+
+-- also rewrite previous
+
+local letter    = lpeg.R("az","AZ") + lpeg.S("_-+")
+local separator = lpeg.P("://")
+
+local qualified = lpeg.P(".")^0 * lpeg.P("/") + letter*lpeg.P(":") + letter^1*separator + letter^1 * lpeg.P("/")
+local rootbased = lpeg.P("/") + letter*lpeg.P(":")
+
+-- ./name ../name  /name c: :// name/name
+
+function file.is_qualified_path(filename)
+    return lpegmatch(qualified,filename) ~= nil
+end
+
+function file.is_rootbased_path(filename)
+    return lpegmatch(rootbased,filename) ~= nil
+end
+
+local slash  = lpeg.S("\\/")
+local period = lpeg.P(".")
+local drive  = lpeg.C(lpeg.R("az","AZ")) * lpeg.P(":")
+local path   = lpeg.C(((1-slash)^0 * slash)^0)
+local suffix = period * lpeg.C(lpeg.P(1-period)^0 * lpeg.P(-1))
+local base   = lpeg.C((1-suffix)^0)
+
+local pattern = (drive + lpeg.Cc("")) * (path + lpeg.Cc("")) * (base + lpeg.Cc("")) * (suffix + lpeg.Cc(""))
+
+function file.splitname(str) -- returns drive, path, base, suffix
+    return lpegmatch(pattern,str)
+end
+
+-- function test(t) for k, v in next, t do print(v, "=>", file.splitname(v)) end end
+--
+-- test { "c:", "c:/aa", "c:/aa/bb", "c:/aa/bb/cc", "c:/aa/bb/cc.dd", "c:/aa/bb/cc.dd.ee" }
+-- test { "c:", "c:aa", "c:aa/bb", "c:aa/bb/cc", "c:aa/bb/cc.dd", "c:aa/bb/cc.dd.ee" }
+-- test { "/aa", "/aa/bb", "/aa/bb/cc", "/aa/bb/cc.dd", "/aa/bb/cc.dd.ee" }
+-- test { "aa", "aa/bb", "aa/bb/cc", "aa/bb/cc.dd", "aa/bb/cc.dd.ee" }
+
+--~ -- todo:
+--~
+--~ if os.type == "windows" then
+--~     local currentdir = lfs.currentdir
+--~     function lfs.currentdir()
+--~         return (gsub(currentdir(),"\\","/"))
+--~     end
+--~ end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-md5'] = {
+    version   = 1.001,
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- This also provides file checksums and checkers.
+
+local gsub, format, byte = string.gsub, string.format, string.byte
+
+local function convert(str,fmt)
+    return (gsub(md5.sum(str),".",function(chr) return format(fmt,byte(chr)) end))
+end
+
+if not md5.HEX then function md5.HEX(str) return convert(str,"%02X") end end
+if not md5.hex then function md5.hex(str) return convert(str,"%02x") end end
+if not md5.dec then function md5.dec(str) return convert(str,"%03i") end end
+
+--~ if not md5.HEX then
+--~     local function remap(chr) return format("%02X",byte(chr)) end
+--~     function md5.HEX(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+--~ if not md5.hex then
+--~     local function remap(chr) return format("%02x",byte(chr)) end
+--~     function md5.hex(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+--~ if not md5.dec then
+--~     local function remap(chr) return format("%03i",byte(chr)) end
+--~     function md5.dec(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+
+file.needs_updating_threshold = 1
+
+function file.needs_updating(oldname,newname) -- size modification access change
+    local oldtime = lfs.attributes(oldname, modification)
+    local newtime = lfs.attributes(newname, modification)
+    if newtime >= oldtime then
+        return false
+    elseif oldtime - newtime < file.needs_updating_threshold then
+        return false
+    else
+        return true
+    end
+end
+
+function file.checksum(name)
+    if md5 then
+        local data = io.loaddata(name)
+        if data then
+            return md5.HEX(data)
+        end
+    end
+    return nil
+end
+
+function file.loadchecksum(name)
+    if md5 then
+        local data = io.loaddata(name .. ".md5")
+        return data and (gsub(data,"%s",""))
+    end
+    return nil
+end
+
+function file.savechecksum(name, checksum)
+    if not checksum then checksum = file.checksum(name) end
+    if checksum then
+        io.savedata(name .. ".md5",checksum)
+        return checksum
+    end
+    return nil
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-url'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local char, gmatch, gsub = string.char, string.gmatch, string.gsub
+local tonumber, type = tonumber, type
+local lpegmatch = lpeg.match
+
+-- from the spec (on the web):
+--
+--     foo://example.com:8042/over/there?name=ferret#nose
+--     \_/   \______________/\_________/ \_________/ \__/
+--      |           |            |            |        |
+--   scheme     authority       path        query   fragment
+--      |   _____________________|__
+--     / \ /                        \
+--     urn:example:animal:ferret:nose
+
+url = url or { }
+
+local function tochar(s)
+    return char(tonumber(s,16))
+end
+
+local colon, qmark, hash, slash, percent, endofstring = lpeg.P(":"), lpeg.P("?"), lpeg.P("#"), lpeg.P("/"), lpeg.P("%"), lpeg.P(-1)
+
+local hexdigit  = lpeg.R("09","AF","af")
+local plus      = lpeg.P("+")
+local escaped   = (plus / " ") + (percent * lpeg.C(hexdigit * hexdigit) / tochar)
+
+-- we assume schemes with more than 1 character (in order to avoid problems with windows disks)
+
+local scheme    =                 lpeg.Cs((escaped+(1-colon-slash-qmark-hash))^2) * colon + lpeg.Cc("")
+local authority = slash * slash * lpeg.Cs((escaped+(1-      slash-qmark-hash))^0)         + lpeg.Cc("")
+local path      = slash *         lpeg.Cs((escaped+(1-            qmark-hash))^0)         + lpeg.Cc("")
+local query     = qmark         * lpeg.Cs((escaped+(1-                  hash))^0)         + lpeg.Cc("")
+local fragment  = hash          * lpeg.Cs((escaped+(1-           endofstring))^0)         + lpeg.Cc("")
+
+local parser = lpeg.Ct(scheme * authority * path * query * fragment)
+
+-- todo: reconsider Ct as we can as well have five return values (saves a table)
+-- so we can have two parsers, one with and one without
+
+function url.split(str)
+    return (type(str) == "string" and lpegmatch(parser,str)) or str
+end
+
+-- todo: cache them
+
+function url.hashed(str)
+    local s = url.split(str)
+    local somescheme = s[1] ~= ""
+    return {
+        scheme    = (somescheme and s[1]) or "file",
+        authority = s[2],
+        path      = s[3],
+        query     = s[4],
+        fragment  = s[5],
+        original  = str,
+        noscheme  = not somescheme,
+    }
+end
+
+function url.hasscheme(str)
+    return url.split(str)[1] ~= ""
+end
+
+function url.addscheme(str,scheme)
+    return (url.hasscheme(str) and str) or ((scheme or "file:///") .. str)
+end
+
+function url.construct(hash)
+    local fullurl = hash.sheme .. "://".. hash.authority .. hash.path
+    if hash.query then
+        fullurl = fullurl .. "?".. hash.query
+    end
+    if hash.fragment then
+        fullurl = fullurl .. "?".. hash.fragment
+    end
+    return fullurl
+end
+
+function url.filename(filename)
+    local t = url.hashed(filename)
+    return (t.scheme == "file" and (gsub(t.path,"^/([a-zA-Z])([:|])/)","%1:"))) or filename
+end
+
+function url.query(str)
+    if type(str) == "string" then
+        local t = { }
+        for k, v in gmatch(str,"([^&=]*)=([^&=]*)") do
+            t[k] = v
+        end
+        return t
+    else
+        return str
+    end
+end
+
+--~ print(url.filename("file:///c:/oeps.txt"))
+--~ print(url.filename("c:/oeps.txt"))
+--~ print(url.filename("file:///oeps.txt"))
+--~ print(url.filename("file:///etc/test.txt"))
+--~ print(url.filename("/oeps.txt"))
+
+--~ from the spec on the web (sort of):
+--~
+--~ function test(str)
+--~     print(table.serialize(url.hashed(str)))
+--~ end
+--~
+--~ test("%56pass%20words")
+--~ test("file:///c:/oeps.txt")
+--~ test("file:///c|/oeps.txt")
+--~ test("file:///etc/oeps.txt")
+--~ test("file://./etc/oeps.txt")
+--~ test("file:////etc/oeps.txt")
+--~ test("ftp://ftp.is.co.za/rfc/rfc1808.txt")
+--~ test("http://www.ietf.org/rfc/rfc2396.txt")
+--~ test("ldap://[2001:db8::7]/c=GB?objectClass?one#what")
+--~ test("mailto:John.Doe@example.com")
+--~ test("news:comp.infosystems.www.servers.unix")
+--~ test("tel:+1-816-555-1212")
+--~ test("telnet://192.0.2.16:80/")
+--~ test("urn:oasis:names:specification:docbook:dtd:xml:4.1.2")
+--~ test("/etc/passwords")
+--~ test("http://www.pragma-ade.com/spaced%20name")
+
+--~ test("zip:///oeps/oeps.zip#bla/bla.tex")
+--~ test("zip:///oeps/oeps.zip?bla/bla.tex")
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-dir'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- dir.expand_name will be merged with cleanpath and collapsepath
+
+local type = type
+local find, gmatch, match, gsub = string.find, string.gmatch, string.match, string.gsub
+local lpegmatch = lpeg.match
+
+dir = dir or { }
+
+-- handy
+
+function dir.current()
+    return (gsub(lfs.currentdir(),"\\","/"))
+end
+
+-- optimizing for no string.find (*) does not save time
+
+local attributes = lfs.attributes
+local walkdir    = lfs.dir
+
+local function glob_pattern(path,patt,recurse,action)
+    local ok, scanner
+    if path == "/" then
+        ok, scanner = xpcall(function() return walkdir(path..".") end, function() end) -- kepler safe
+    else
+        ok, scanner = xpcall(function() return walkdir(path)      end, function() end) -- kepler safe
+    end
+    if ok and type(scanner) == "function" then
+        if not find(path,"/$") then path = path .. '/' end
+        for name in scanner do
+            local full = path .. name
+            local mode = attributes(full,'mode')
+            if mode == 'file' then
+                if find(full,patt) then
+                    action(full)
+                end
+            elseif recurse and (mode == "directory") and (name ~= '.') and (name ~= "..") then
+                glob_pattern(full,patt,recurse,action)
+            end
+        end
+    end
+end
+
+dir.glob_pattern = glob_pattern
+
+local function collect_pattern(path,patt,recurse,result)
+    local ok, scanner
+    result = result or { }
+    if path == "/" then
+        ok, scanner = xpcall(function() return walkdir(path..".") end, function() end) -- kepler safe
+    else
+        ok, scanner = xpcall(function() return walkdir(path)      end, function() end) -- kepler safe
+    end
+    if ok and type(scanner) == "function" then
+        if not find(path,"/$") then path = path .. '/' end
+        for name in scanner do
+            local full = path .. name
+            local attr = attributes(full)
+            local mode = attr.mode
+            if mode == 'file' then
+                if find(full,patt) then
+                    result[name] = attr
+                end
+            elseif recurse and (mode == "directory") and (name ~= '.') and (name ~= "..") then
+                attr.list = collect_pattern(full,patt,recurse)
+                result[name] = attr
+            end
+        end
+    end
+    return result
+end
+
+dir.collect_pattern = collect_pattern
+
+local P, S, R, C, Cc, Cs, Ct, Cv, V = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.Cc, lpeg.Cs, lpeg.Ct, lpeg.Cv, lpeg.V
+
+local pattern = Ct {
+    [1] = (C(P(".") + P("/")^1) + C(R("az","AZ") * P(":") * P("/")^0) + Cc("./")) * V(2) * V(3),
+    [2] = C(((1-S("*?/"))^0 * P("/"))^0),
+    [3] = C(P(1)^0)
+}
+
+local filter = Cs ( (
+    P("**") / ".*" +
+    P("*")  / "[^/]*" +
+    P("?")  / "[^/]" +
+    P(".")  / "%%." +
+    P("+")  / "%%+" +
+    P("-")  / "%%-" +
+    P(1)
+)^0 )
+
+local function glob(str,t)
+    if type(t) == "function" then
+        if type(str) == "table" then
+            for s=1,#str do
+                glob(str[s],t)
+            end
+        elseif lfs.isfile(str) then
+            t(str)
+        else
+            local split = lpegmatch(pattern,str)
+            if split then
+                local root, path, base = split[1], split[2], split[3]
+                local recurse = find(base,"%*%*")
+                local start = root .. path
+                local result = lpegmatch(filter,start .. base)
+                glob_pattern(start,result,recurse,t)
+            end
+        end
+    else
+        if type(str) == "table" then
+            local t = t or { }
+            for s=1,#str do
+                glob(str[s],t)
+            end
+            return t
+        elseif lfs.isfile(str) then
+            local t = t or { }
+            t[#t+1] = str
+            return t
+        else
+            local split = lpegmatch(pattern,str)
+            if split then
+                local t = t or { }
+                local action = action or function(name) t[#t+1] = name end
+                local root, path, base = split[1], split[2], split[3]
+                local recurse = find(base,"%*%*")
+                local start = root .. path
+                local result = lpegmatch(filter,start .. base)
+                glob_pattern(start,result,recurse,action)
+                return t
+            else
+                return { }
+            end
+        end
+    end
+end
+
+dir.glob = glob
+
+--~ list = dir.glob("**/*.tif")
+--~ list = dir.glob("/**/*.tif")
+--~ list = dir.glob("./**/*.tif")
+--~ list = dir.glob("oeps/**/*.tif")
+--~ list = dir.glob("/oeps/**/*.tif")
+
+local function globfiles(path,recurse,func,files) -- func == pattern or function
+    if type(func) == "string" then
+        local s = func -- alas, we need this indirect way
+        func = function(name) return find(name,s) end
+    end
+    files = files or { }
+    for name in walkdir(path) do
+        if find(name,"^%.") then
+            --- skip
+        else
+            local mode = attributes(name,'mode')
+            if mode == "directory" then
+                if recurse then
+                    globfiles(path .. "/" .. name,recurse,func,files)
+                end
+            elseif mode == "file" then
+                if func then
+                    if func(name) then
+                        files[#files+1] = path .. "/" .. name
+                    end
+                else
+                    files[#files+1] = path .. "/" .. name
+                end
+            end
+        end
+    end
+    return files
+end
+
+dir.globfiles = globfiles
+
+-- t = dir.glob("c:/data/develop/context/sources/**/????-*.tex")
+-- t = dir.glob("c:/data/develop/tex/texmf/**/*.tex")
+-- t = dir.glob("c:/data/develop/context/texmf/**/*.tex")
+-- t = dir.glob("f:/minimal/tex/**/*")
+-- print(dir.ls("f:/minimal/tex/**/*"))
+-- print(dir.ls("*.tex"))
+
+function dir.ls(pattern)
+    return table.concat(glob(pattern),"\n")
+end
+
+--~ mkdirs("temp")
+--~ mkdirs("a/b/c")
+--~ mkdirs(".","/a/b/c")
+--~ mkdirs("a","b","c")
+
+local make_indeed = true -- false
+
+if string.find(os.getenv("PATH"),";") then -- os.type == "windows"
+
+    function dir.mkdirs(...)
+        local str, pth, t = "", "", { ... }
+        for i=1,#t do
+            local s = t[i]
+            if s ~= "" then
+                if str ~= "" then
+                    str = str .. "/" .. s
+                else
+                    str = s
+                end
+            end
+        end
+        local first, middle, last
+        local drive = false
+        first, middle, last = match(str,"^(//)(//*)(.*)$")
+        if first then
+            -- empty network path == local path
+        else
+            first, last = match(str,"^(//)/*(.-)$")
+            if first then
+                middle, last = match(str,"([^/]+)/+(.-)$")
+                if middle then
+                    pth = "//" .. middle
+                else
+                    pth = "//" .. last
+                    last = ""
+                end
+            else
+                first, middle, last = match(str,"^([a-zA-Z]:)(/*)(.-)$")
+                if first then
+                    pth, drive = first .. middle, true
+                else
+                    middle, last = match(str,"^(/*)(.-)$")
+                    if not middle then
+                        last = str
+                    end
+                end
+            end
+        end
+        for s in gmatch(last,"[^/]+") do
+            if pth == "" then
+                pth = s
+            elseif drive then
+                pth, drive = pth .. s, false
+            else
+                pth = pth .. "/" .. s
+            end
+            if make_indeed and not lfs.isdir(pth) then
+                lfs.mkdir(pth)
+            end
+        end
+        return pth, (lfs.isdir(pth) == true)
+    end
+
+--~         print(dir.mkdirs("","","a","c"))
+--~         print(dir.mkdirs("a"))
+--~         print(dir.mkdirs("a:"))
+--~         print(dir.mkdirs("a:/b/c"))
+--~         print(dir.mkdirs("a:b/c"))
+--~         print(dir.mkdirs("a:/bbb/c"))
+--~         print(dir.mkdirs("/a/b/c"))
+--~         print(dir.mkdirs("/aaa/b/c"))
+--~         print(dir.mkdirs("//a/b/c"))
+--~         print(dir.mkdirs("///a/b/c"))
+--~         print(dir.mkdirs("a/bbb//ccc/"))
+
+    function dir.expand_name(str) -- will be merged with cleanpath and collapsepath
+        local first, nothing, last = match(str,"^(//)(//*)(.*)$")
+        if first then
+            first = dir.current() .. "/"
+        end
+        if not first then
+            first, last = match(str,"^(//)/*(.*)$")
+        end
+        if not first then
+            first, last = match(str,"^([a-zA-Z]:)(.*)$")
+            if first and not find(last,"^/") then
+                local d = lfs.currentdir()
+                if lfs.chdir(first) then
+                    first = dir.current()
+                end
+                lfs.chdir(d)
+            end
+        end
+        if not first then
+            first, last = dir.current(), str
+        end
+        last = gsub(last,"//","/")
+        last = gsub(last,"/%./","/")
+        last = gsub(last,"^/*","")
+        first = gsub(first,"/*$","")
+        if last == "" then
+            return first
+        else
+            return first .. "/" .. last
+        end
+    end
+
+else
+
+    function dir.mkdirs(...)
+        local str, pth, t = "", "", { ... }
+        for i=1,#t do
+            local s = t[i]
+            if s ~= "" then
+                if str ~= "" then
+                    str = str .. "/" .. s
+                else
+                    str = s
+                end
+            end
+        end
+        str = gsub(str,"/+","/")
+        if find(str,"^/") then
+            pth = "/"
+            for s in gmatch(str,"[^/]+") do
+                local first = (pth == "/")
+                if first then
+                    pth = pth .. s
+                else
+                    pth = pth .. "/" .. s
+                end
+                if make_indeed and not first and not lfs.isdir(pth) then
+                    lfs.mkdir(pth)
+                end
+            end
+        else
+            pth = "."
+            for s in gmatch(str,"[^/]+") do
+                pth = pth .. "/" .. s
+                if make_indeed and not lfs.isdir(pth) then
+                    lfs.mkdir(pth)
+                end
+            end
+        end
+        return pth, (lfs.isdir(pth) == true)
+    end
+
+--~         print(dir.mkdirs("","","a","c"))
+--~         print(dir.mkdirs("a"))
+--~         print(dir.mkdirs("/a/b/c"))
+--~         print(dir.mkdirs("/aaa/b/c"))
+--~         print(dir.mkdirs("//a/b/c"))
+--~         print(dir.mkdirs("///a/b/c"))
+--~         print(dir.mkdirs("a/bbb//ccc/"))
+
+    function dir.expand_name(str) -- will be merged with cleanpath and collapsepath
+        if not find(str,"^/") then
+            str = lfs.currentdir() .. "/" .. str
+        end
+        str = gsub(str,"//","/")
+        str = gsub(str,"/%./","/")
+        return str
+    end
+
+end
+
+dir.makedirs = dir.mkdirs
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-boolean'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+boolean = boolean or { }
+
+local type, tonumber = type, tonumber
+
+function boolean.tonumber(b)
+    if b then return 1 else return 0 end
+end
+
+function toboolean(str,tolerant)
+    if tolerant then
+        local tstr = type(str)
+        if tstr == "string" then
+            return str == "true" or str == "yes" or str == "on" or str == "1" or str == "t"
+        elseif tstr == "number" then
+            return tonumber(str) ~= 0
+        elseif tstr == "nil" then
+            return false
+        else
+            return str
+        end
+    elseif str == "true" then
+        return true
+    elseif str == "false" then
+        return false
+    else
+        return str
+    end
+end
+
+function string.is_boolean(str)
+    if type(str) == "string" then
+        if str == "true" or str == "yes" or str == "on" or str == "t" then
+            return true
+        elseif str == "false" or str == "no" or str == "off" or str == "f" then
+            return false
+        end
+    end
+    return nil
+end
+
+function boolean.alwaystrue()
+    return true
+end
+
+function boolean.falsetrue()
+    return false
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-math'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local floor, sin, cos, tan = math.floor, math.sin, math.cos, math.tan
+
+if not math.round then
+    function math.round(x)
+        return floor(x + 0.5)
+    end
+end
+
+if not math.div then
+    function math.div(n,m)
+        return floor(n/m)
+    end
+end
+
+if not math.mod then
+    function math.mod(n,m)
+        return n % m
+    end
+end
+
+local pipi = 2*math.pi/360
+
+function math.sind(d)
+    return sin(d*pipi)
+end
+
+function math.cosd(d)
+    return cos(d*pipi)
+end
+
+function math.tand(d)
+    return tan(d*pipi)
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-utils'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- hm, quite unreadable
+
+local gsub = string.gsub
+local concat = table.concat
+local type, next = type, next
+
+if not utils        then utils        = { } end
+if not utils.merger then utils.merger = { } end
+if not utils.lua    then utils.lua    = { } end
+
+utils.merger.m_begin = "begin library merge"
+utils.merger.m_end   = "end library merge"
+utils.merger.pattern =
+    "%c+" ..
+    "%-%-%s+" .. utils.merger.m_begin ..
+    "%c+(.-)%c+" ..
+    "%-%-%s+" .. utils.merger.m_end ..
+    "%c+"
+
+function utils.merger._self_fake_()
+    return
+        "-- " .. "created merged file" .. "\n\n" ..
+        "-- " .. utils.merger.m_begin  .. "\n\n" ..
+        "-- " .. utils.merger.m_end    .. "\n\n"
+end
+
+function utils.report(...)
+    print(...)
+end
+
+utils.merger.strip_comment = true
+
+function utils.merger._self_load_(name)
+    local f, data = io.open(name), ""
+    if f then
+        utils.report("reading merge from %s",name)
+        data = f:read("*all")
+        f:close()
+    else
+        utils.report("unknown file to merge %s",name)
+    end
+    if data and utils.merger.strip_comment then
+        -- saves some 20K
+        data = gsub(data,"%-%-~[^\n\r]*[\r\n]", "")
+    end
+    return data or ""
+end
+
+function utils.merger._self_save_(name, data)
+    if data ~= "" then
+        local f = io.open(name,'w')
+        if f then
+            utils.report("saving merge from %s",name)
+            f:write(data)
+            f:close()
+        end
+    end
+end
+
+function utils.merger._self_swap_(data,code)
+    if data ~= "" then
+        return (gsub(data,utils.merger.pattern, function(s)
+            return "\n\n" .. "-- "..utils.merger.m_begin .. "\n" .. code .. "\n" .. "-- "..utils.merger.m_end .. "\n\n"
+        end, 1))
+    else
+        return ""
+    end
+end
+
+--~ stripper:
+--~
+--~ data = gsub(data,"%-%-~[^\n]*\n","")
+--~ data = gsub(data,"\n\n+","\n")
+
+function utils.merger._self_libs_(libs,list)
+    local result, f, frozen = { }, nil, false
+    result[#result+1] = "\n"
+    if type(libs) == 'string' then libs = { libs } end
+    if type(list) == 'string' then list = { list } end
+    local foundpath = nil
+    for i=1,#libs do
+        local lib = libs[i]
+        for j=1,#list do
+            local pth = gsub(list[j],"\\","/") -- file.clean_path
+            utils.report("checking library path %s",pth)
+            local name = pth .. "/" .. lib
+            if lfs.isfile(name) then
+                foundpath = pth
+            end
+        end
+        if foundpath then break end
+    end
+    if foundpath then
+        utils.report("using library path %s",foundpath)
+        local right, wrong = { }, { }
+        for i=1,#libs do
+            local lib = libs[i]
+            local fullname = foundpath .. "/" .. lib
+            if lfs.isfile(fullname) then
+            --  right[#right+1] = lib
+                utils.report("merging library %s",fullname)
+                result[#result+1] = "do -- create closure to overcome 200 locals limit"
+                result[#result+1] = io.loaddata(fullname,true)
+                result[#result+1] = "end -- of closure"
+            else
+            --  wrong[#wrong+1] = lib
+                utils.report("no library %s",fullname)
+            end
+        end
+        if #right > 0 then
+            utils.report("merged libraries: %s",concat(right," "))
+        end
+        if #wrong > 0 then
+            utils.report("skipped libraries: %s",concat(wrong," "))
+        end
+    else
+        utils.report("no valid library path found")
+    end
+    return concat(result, "\n\n")
+end
+
+function utils.merger.selfcreate(libs,list,target)
+    if target then
+        utils.merger._self_save_(
+            target,
+            utils.merger._self_swap_(
+                utils.merger._self_fake_(),
+                utils.merger._self_libs_(libs,list)
+            )
+        )
+    end
+end
+
+function utils.merger.selfmerge(name,libs,list,target)
+    utils.merger._self_save_(
+        target or name,
+        utils.merger._self_swap_(
+            utils.merger._self_load_(name),
+            utils.merger._self_libs_(libs,list)
+        )
+    )
+end
+
+function utils.merger.selfclean(name)
+    utils.merger._self_save_(
+        name,
+        utils.merger._self_swap_(
+            utils.merger._self_load_(name),
+            ""
+        )
+    )
+end
+
+function utils.lua.compile(luafile, lucfile, cleanup, strip) -- defaults: cleanup=false strip=true
+ -- utils.report("compiling",luafile,"into",lucfile)
+    os.remove(lucfile)
+    local command = "-o " .. string.quote(lucfile) .. " " .. string.quote(luafile)
+    if strip ~= false then
+        command = "-s " .. command
+    end
+    local done = (os.spawn("texluac " .. command) == 0) or (os.spawn("luac " .. command) == 0)
+    if done and cleanup == true and lfs.isfile(lucfile) and lfs.isfile(luafile) then
+     -- utils.report("removing",luafile)
+        os.remove(luafile)
+    end
+    return done
+end
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-aux'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- for inline, no store split : for s in string.gmatch(str,",* *([^,]+)") do .. end
+
+aux = aux or { }
+
+local concat, format, gmatch = table.concat, string.format, string.gmatch
+local tostring, type = tostring, type
+local lpegmatch = lpeg.match
+
+local P, R, V = lpeg.P, lpeg.R, lpeg.V
+
+local escape, left, right = P("\\"), P('{'), P('}')
+
+lpeg.patterns.balanced = P {
+    [1] = ((escape * (left+right)) + (1 - (left+right)) + V(2))^0,
+    [2] = left * V(1) * right
+}
+
+local space     = lpeg.P(' ')
+local equal     = lpeg.P("=")
+local comma     = lpeg.P(",")
+local lbrace    = lpeg.P("{")
+local rbrace    = lpeg.P("}")
+local nobrace   = 1 - (lbrace+rbrace)
+local nested    = lpeg.P { lbrace * (nobrace + lpeg.V(1))^0 * rbrace }
+local spaces    = space^0
+
+local value     = lpeg.P(lbrace * lpeg.C((nobrace + nested)^0) * rbrace) + lpeg.C((nested + (1-comma))^0)
+
+local key       = lpeg.C((1-equal-comma)^1)
+local pattern_a = (space+comma)^0 * (key * equal * value + key * lpeg.C(""))
+local pattern_c = (space+comma)^0 * (key * equal * value)
+
+local key       = lpeg.C((1-space-equal-comma)^1)
+local pattern_b = spaces * comma^0 * spaces * (key * ((spaces * equal * spaces * value) + lpeg.C("")))
+
+-- "a=1, b=2, c=3, d={a{b,c}d}, e=12345, f=xx{a{b,c}d}xx, g={}" : outer {} removes, leading spaces ignored
+
+local hash = { }
+
+local function set(key,value) -- using Carg is slower here
+    hash[key] = value
+end
+
+local pattern_a_s = (pattern_a/set)^1
+local pattern_b_s = (pattern_b/set)^1
+local pattern_c_s = (pattern_c/set)^1
+
+aux.settings_to_hash_pattern_a = pattern_a_s
+aux.settings_to_hash_pattern_b = pattern_b_s
+aux.settings_to_hash_pattern_c = pattern_c_s
+
+function aux.make_settings_to_hash_pattern(set,how)
+    if how == "strict" then
+        return (pattern_c/set)^1
+    elseif how == "tolerant" then
+        return (pattern_b/set)^1
+    else
+        return (pattern_a/set)^1
+    end
+end
+
+function aux.settings_to_hash(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        if moretolerant then
+            lpegmatch(pattern_b_s,str)
+        else
+            lpegmatch(pattern_a_s,str)
+        end
+        return hash
+    else
+        return { }
+    end
+end
+
+function aux.settings_to_hash_tolerant(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        lpegmatch(pattern_b_s,str)
+        return hash
+    else
+        return { }
+    end
+end
+
+function aux.settings_to_hash_strict(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        lpegmatch(pattern_c_s,str)
+        return next(hash) and hash
+    else
+        return nil
+    end
+end
+
+local separator = comma * space^0
+local value     = lpeg.P(lbrace * lpeg.C((nobrace + nested)^0) * rbrace) + lpeg.C((nested + (1-comma))^0)
+local pattern   = lpeg.Ct(value*(separator*value)^0)
+
+-- "aap, {noot}, mies" : outer {} removes, leading spaces ignored
+
+aux.settings_to_array_pattern = pattern
+
+-- we could use a weak table as cache
+
+function aux.settings_to_array(str)
+    if not str or str == "" then
+        return { }
+    else
+        return lpegmatch(pattern,str)
+    end
+end
+
+local function set(t,v)
+    t[#t+1] = v
+end
+
+local value   = lpeg.P(lpeg.Carg(1)*value) / set
+local pattern = value*(separator*value)^0 * lpeg.Carg(1)
+
+function aux.add_settings_to_array(t,str)
+    return lpegmatch(pattern,str,nil,t)
+end
+
+function aux.hash_to_string(h,separator,yes,no,strict,omit)
+    if h then
+        local t, s = { }, table.sortedkeys(h)
+        omit = omit and table.tohash(omit)
+        for i=1,#s do
+            local key = s[i]
+            if not omit or not omit[key] then
+                local value = h[key]
+                if type(value) == "boolean" then
+                    if yes and no then
+                        if value then
+                            t[#t+1] = key .. '=' .. yes
+                        elseif not strict then
+                            t[#t+1] = key .. '=' .. no
+                        end
+                    elseif value or not strict then
+                        t[#t+1] = key .. '=' .. tostring(value)
+                    end
+                else
+                    t[#t+1] = key .. '=' .. value
+                end
+            end
+        end
+        return concat(t,separator or ",")
+    else
+        return ""
+    end
+end
+
+function aux.array_to_string(a,separator)
+    if a then
+        return concat(a,separator or ",")
+    else
+        return ""
+    end
+end
+
+function aux.settings_to_set(str,t)
+    t = t or { }
+    for s in gmatch(str,"%s*([^,]+)") do
+        t[s] = true
+    end
+    return t
+end
+
+local value     = lbrace * lpeg.C((nobrace + nested)^0) * rbrace
+local pattern   = lpeg.Ct((space + value)^0)
+
+function aux.arguments_to_table(str)
+    return lpegmatch(pattern,str)
+end
+
+-- temporary here
+
+function aux.getparameters(self,class,parentclass,settings)
+    local sc = self[class]
+    if not sc then
+        sc = table.clone(self[parent])
+        self[class] = sc
+    end
+    aux.settings_to_hash(settings,sc)
+end
+
+-- temporary here
+
+local digit         = lpeg.R("09")
+local period        = lpeg.P(".")
+local zero          = lpeg.P("0")
+local trailingzeros = zero^0 * -digit -- suggested by Roberto R
+local case_1        = period * trailingzeros / ""
+local case_2        = period * (digit - trailingzeros)^1 * (trailingzeros / "")
+local number        = digit^1 * (case_1 + case_2)
+local stripper      = lpeg.Cs((number + 1)^0)
+
+--~ local sample = "bla 11.00 bla 11 bla 0.1100 bla 1.00100 bla 0.00 bla 0.001 bla 1.1100 bla 0.100100100 bla 0.00100100100"
+--~ collectgarbage("collect")
+--~ str = string.rep(sample,10000)
+--~ local ts = os.clock()
+--~ lpegmatch(stripper,str)
+--~ print(#str, os.clock()-ts, lpegmatch(stripper,sample))
+
+lpeg.patterns.strip_zeros = stripper
+
+function aux.strip_zeros(str)
+    return lpegmatch(stripper,str)
+end
+
+function aux.definetable(target) -- defines undefined tables
+    local composed, t = nil, { }
+    for name in gmatch(target,"([^%.]+)") do
+        if composed then
+            composed = composed .. "." .. name
+        else
+            composed = name
+        end
+        t[#t+1] = format("%s = %s or { }",composed,composed)
+    end
+    return concat(t,"\n")
+end
+
+function aux.accesstable(target)
+    local t = _G
+    for name in gmatch(target,"([^%.]+)") do
+        t = t[name]
+    end
+    return t
+end
+
+--~ function string.commaseparated(str)
+--~     return gmatch(str,"([^,%s]+)")
+--~ end
+
+-- as we use this a lot ...
+
+--~ function aux.cachefunction(action,weak)
+--~     local cache = { }
+--~     if weak then
+--~         setmetatable(cache, { __mode = "kv" } )
+--~     end
+--~     local function reminder(str)
+--~         local found = cache[str]
+--~         if not found then
+--~             found = action(str)
+--~             cache[str] = found
+--~         end
+--~         return found
+--~     end
+--~     return reminder, cache
+--~ end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-tra'] = {
+    version   = 1.001,
+    comment   = "companion to trac-tra.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- the <anonymous> tag is kind of generic and used for functions that are not
+-- bound to a variable, like node.new, node.copy etc (contrary to for instance
+-- node.has_attribute which is bound to a has_attribute local variable in mkiv)
+
+local debug = require "debug"
+
+local getinfo = debug.getinfo
+local type, next = type, next
+local concat = table.concat
+local format, find, lower, gmatch, gsub = string.format, string.find, string.lower, string.gmatch, string.gsub
+
+debugger = debugger or { }
+
+local counters = { }
+local names = { }
+
+-- one
+
+local function hook()
+    local f = getinfo(2,"f").func
+    local n = getinfo(2,"Sn")
+--  if n.what == "C" and n.name then print (n.namewhat .. ': ' .. n.name) end
+    if f then
+        local cf = counters[f]
+        if cf == nil then
+            counters[f] = 1
+            names[f] = n
+        else
+            counters[f] = cf + 1
+        end
+    end
+end
+local function getname(func)
+    local n = names[func]
+    if n then
+        if n.what == "C" then
+            return n.name or '<anonymous>'
+        else
+            -- source short_src linedefined what name namewhat nups func
+            local name = n.name or n.namewhat or n.what
+            if not name or name == "" then name = "?" end
+            return format("%s : %s : %s", n.short_src or "unknown source", n.linedefined or "--", name)
+        end
+    else
+        return "unknown"
+    end
+end
+function debugger.showstats(printer,threshold)
+    printer   = printer or texio.write or print
+    threshold = threshold or 0
+    local total, grandtotal, functions = 0, 0, 0
+    printer("\n") -- ugly but ok
+ -- table.sort(counters)
+    for func, count in next, counters do
+        if count > threshold then
+            local name = getname(func)
+            if not find(name,"for generator") then
+                printer(format("%8i  %s", count, name))
+                total = total + count
+            end
+        end
+        grandtotal = grandtotal + count
+        functions = functions + 1
+    end
+    printer(format("functions: %s, total: %s, grand total: %s, threshold: %s\n", functions, total, grandtotal, threshold))
+end
+
+-- two
+
+--~ local function hook()
+--~     local n = getinfo(2)
+--~     if n.what=="C" and not n.name then
+--~         local f = tostring(debug.traceback())
+--~         local cf = counters[f]
+--~         if cf == nil then
+--~             counters[f] = 1
+--~             names[f] = n
+--~         else
+--~             counters[f] = cf + 1
+--~         end
+--~     end
+--~ end
+--~ function debugger.showstats(printer,threshold)
+--~     printer   = printer or texio.write or print
+--~     threshold = threshold or 0
+--~     local total, grandtotal, functions = 0, 0, 0
+--~     printer("\n") -- ugly but ok
+--~  -- table.sort(counters)
+--~     for func, count in next, counters do
+--~         if count > threshold then
+--~             printer(format("%8i  %s", count, func))
+--~             total = total + count
+--~         end
+--~         grandtotal = grandtotal + count
+--~         functions = functions + 1
+--~     end
+--~     printer(format("functions: %s, total: %s, grand total: %s, threshold: %s\n", functions, total, grandtotal, threshold))
+--~ end
+
+-- rest
+
+function debugger.savestats(filename,threshold)
+    local f = io.open(filename,'w')
+    if f then
+        debugger.showstats(function(str) f:write(str) end,threshold)
+        f:close()
+    end
+end
+
+function debugger.enable()
+    debug.sethook(hook,"c")
+end
+
+function debugger.disable()
+    debug.sethook()
+--~ counters[debug.getinfo(2,"f").func] = nil
+end
+
+function debugger.tracing()
+    local n = tonumber(os.env['MTX.TRACE.CALLS']) or tonumber(os.env['MTX_TRACE_CALLS']) or 0
+    if n > 0 then
+        function debugger.tracing() return true  end ; return true
+    else
+        function debugger.tracing() return false end ; return false
+    end
+end
+
+--~ debugger.enable()
+
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+
+--~ debugger.disable()
+
+--~ print("")
+--~ debugger.showstats()
+--~ print("")
+--~ debugger.showstats(print,3)
+
+setters      = setters      or { }
+setters.data = setters.data or { }
+
+--~ local function set(t,what,value)
+--~     local data, done = t.data, t.done
+--~     if type(what) == "string" then
+--~         what = aux.settings_to_array(what) -- inefficient but ok
+--~     end
+--~     for i=1,#what do
+--~         local w = what[i]
+--~         for d, f in next, data do
+--~             if done[d] then
+--~                 -- prevent recursion due to wildcards
+--~             elseif find(d,w) then
+--~                 done[d] = true
+--~                 for i=1,#f do
+--~                     f[i](value)
+--~                 end
+--~             end
+--~         end
+--~     end
+--~ end
+
+local function set(t,what,value)
+    local data, done = t.data, t.done
+    if type(what) == "string" then
+        what = aux.settings_to_hash(what) -- inefficient but ok
+    end
+    for w, v in next, what do
+        if v == "" then
+            v = value
+        else
+            v = toboolean(v)
+        end
+        for d, f in next, data do
+            if done[d] then
+                -- prevent recursion due to wildcards
+            elseif find(d,w) then
+                done[d] = true
+                for i=1,#f do
+                    f[i](v)
+                end
+            end
+        end
+    end
+end
+
+local function reset(t)
+    for d, f in next, t.data do
+        for i=1,#f do
+            f[i](false)
+        end
+    end
+end
+
+local function enable(t,what)
+    set(t,what,true)
+end
+
+local function disable(t,what)
+    local data = t.data
+    if not what or what == "" then
+        t.done = { }
+        reset(t)
+    else
+        set(t,what,false)
+    end
+end
+
+function setters.register(t,what,...)
+    local data = t.data
+    what = lower(what)
+    local w = data[what]
+    if not w then
+        w = { }
+        data[what] = w
+    end
+    for _, fnc in next, { ... } do
+        local typ = type(fnc)
+        if typ == "function" then
+            w[#w+1] = fnc
+        elseif typ == "string" then
+            w[#w+1] = function(value) set(t,fnc,value,nesting) end
+        end
+    end
+end
+
+function setters.enable(t,what)
+    local e = t.enable
+    t.enable, t.done = enable, { }
+    enable(t,string.simpleesc(tostring(what)))
+    t.enable, t.done = e, { }
+end
+
+function setters.disable(t,what)
+    local e = t.disable
+    t.disable, t.done = disable, { }
+    disable(t,string.simpleesc(tostring(what)))
+    t.disable, t.done = e, { }
+end
+
+function setters.reset(t)
+    t.done = { }
+    reset(t)
+end
+
+function setters.list(t) -- pattern
+    local list = table.sortedkeys(t.data)
+    local user, system = { }, { }
+    for l=1,#list do
+        local what = list[l]
+        if find(what,"^%*") then
+            system[#system+1] = what
+        else
+            user[#user+1] = what
+        end
+    end
+    return user, system
+end
+
+function setters.show(t)
+    commands.writestatus("","")
+    local list = setters.list(t)
+    for k=1,#list do
+        commands.writestatus(t.name,list[k])
+    end
+    commands.writestatus("","")
+end
+
+-- we could have used a bit of oo and the trackers:enable syntax but
+-- there is already a lot of code around using the singular tracker
+
+-- we could make this into a module
+
+function setters.new(name)
+    local t
+    t = {
+        data     = { },
+        name     = name,
+        enable   = function(...) setters.enable  (t,...) end,
+        disable  = function(...) setters.disable (t,...) end,
+        register = function(...) setters.register(t,...) end,
+        list     = function(...) setters.list    (t,...) end,
+        show     = function(...) setters.show    (t,...) end,
+    }
+    setters.data[name] = t
+    return t
+end
+
+trackers    = setters.new("trackers")
+directives  = setters.new("directives")
+experiments = setters.new("experiments")
+
+-- nice trick: we overload two of the directives related functions with variants that
+-- do tracing (itself using a tracker) .. proof of concept
+
+local trace_directives  = false local trace_directives  = false  trackers.register("system.directives",  function(v) trace_directives  = v end)
+local trace_experiments = false local trace_experiments = false  trackers.register("system.experiments", function(v) trace_experiments = v end)
+
+local e = directives.enable
+local d = directives.disable
+
+function directives.enable(...)
+    commands.writestatus("directives","enabling: %s",concat({...}," "))
+    e(...)
+end
+
+function directives.disable(...)
+    commands.writestatus("directives","disabling: %s",concat({...}," "))
+    d(...)
+end
+
+local e = experiments.enable
+local d = experiments.disable
+
+function experiments.enable(...)
+    commands.writestatus("experiments","enabling: %s",concat({...}," "))
+    e(...)
+end
+
+function experiments.disable(...)
+    commands.writestatus("experiments","disabling: %s",concat({...}," "))
+    d(...)
+end
+
+-- a useful example
+
+directives.register("system.nostatistics", function(v)
+    statistics.enable = not v
+end)
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['lxml-tab'] = {
+    version   = 1.001,
+    comment   = "this module is the basis for the lxml-* ones",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- this module needs a cleanup: check latest lpeg, passing args, (sub)grammar, etc etc
+-- stripping spaces from e.g. cont-en.xml saves .2 sec runtime so it's not worth the
+-- trouble
+
+local trace_entities = false  trackers.register("xml.entities", function(v) trace_entities = v end)
+
+--[[ldx--
+<p>The parser used here is inspired by the variant discussed in the lua book, but
+handles comment and processing instructions, has a different structure, provides
+parent access; a first version used different trickery but was less optimized to we
+went this route. First we had a find based parser, now we have an <l n='lpeg'/> based one.
+The find based parser can be found in l-xml-edu.lua along with other older code.</p>
+
+<p>Beware, the interface may change. For instance at, ns, tg, dt may get more
+verbose names. Once the code is stable we will also remove some tracing and
+optimize the code.</p>
+--ldx]]--
+
+xml = xml or { }
+
+--~ local xml = xml
+
+local concat, remove, insert = table.concat, table.remove, table.insert
+local type, next, setmetatable, getmetatable, tonumber = type, next, setmetatable, getmetatable, tonumber
+local format, lower, find, match, gsub = string.format, string.lower, string.find, string.match, string.gsub
+local utfchar = unicode.utf8.char
+local lpegmatch = lpeg.match
+local P, S, R, C, V, C, Cs = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.V, lpeg.C, lpeg.Cs
+
+--[[ldx--
+<p>First a hack to enable namespace resolving. A namespace is characterized by
+a <l n='url'/>. The following function associates a namespace prefix with a
+pattern. We use <l n='lpeg'/>, which in this case is more than twice as fast as a
+find based solution where we loop over an array of patterns. Less code and
+much cleaner.</p>
+--ldx]]--
+
+xml.xmlns = xml.xmlns or { }
+
+local check = P(false)
+local parse = check
+
+--[[ldx--
+<p>The next function associates a namespace prefix with an <l n='url'/>. This
+normally happens independent of parsing.</p>
+
+<typing>
+xml.registerns("mml","mathml")
+</typing>
+--ldx]]--
+
+function xml.registerns(namespace, pattern) -- pattern can be an lpeg
+    check = check + C(P(lower(pattern))) / namespace
+    parse = P { P(check) + 1 * V(1) }
+end
+
+--[[ldx--
+<p>The next function also registers a namespace, but this time we map a
+given namespace prefix onto a registered one, using the given
+<l n='url'/>. This used for attributes like <t>xmlns:m</t>.</p>
+
+<typing>
+xml.checkns("m","http://www.w3.org/mathml")
+</typing>
+--ldx]]--
+
+function xml.checkns(namespace,url)
+    local ns = lpegmatch(parse,lower(url))
+    if ns and namespace ~= ns then
+        xml.xmlns[namespace] = ns
+    end
+end
+
+--[[ldx--
+<p>Next we provide a way to turn an <l n='url'/> into a registered
+namespace. This used for the <t>xmlns</t> attribute.</p>
+
+<typing>
+resolvedns = xml.resolvens("http://www.w3.org/mathml")
+</typing>
+
+This returns <t>mml</t>.
+--ldx]]--
+
+function xml.resolvens(url)
+     return lpegmatch(parse,lower(url)) or ""
+end
+
+--[[ldx--
+<p>A namespace in an element can be remapped onto the registered
+one efficiently by using the <t>xml.xmlns</t> table.</p>
+--ldx]]--
+
+--[[ldx--
+<p>This version uses <l n='lpeg'/>. We follow the same approach as before, stack and top and
+such. This version is about twice as fast which is mostly due to the fact that
+we don't have to prepare the stream for cdata, doctype etc etc. This variant is
+is dedicated to Luigi Scarso, who challenged me with 40 megabyte <l n='xml'/> files that
+took 12.5 seconds to load (1.5 for file io and the rest for tree building). With
+the <l n='lpeg'/> implementation we got that down to less 7.3 seconds. Loading the 14
+<l n='context'/> interface definition files (2.6 meg) went down from 1.05 seconds to 0.55.</p>
+
+<p>Next comes the parser. The rather messy doctype definition comes in many
+disguises so it is no surprice that later on have to dedicate quite some
+<l n='lpeg'/> code to it.</p>
+
+<typing>
+<!DOCTYPE Something PUBLIC "... ..." "..." [ ... ] >
+<!DOCTYPE Something PUBLIC "... ..." "..." >
+<!DOCTYPE Something SYSTEM "... ..." [ ... ] >
+<!DOCTYPE Something SYSTEM "... ..." >
+<!DOCTYPE Something [ ... ] >
+<!DOCTYPE Something >
+</typing>
+
+<p>The code may look a bit complex but this is mostly due to the fact that we
+resolve namespaces and attach metatables. There is only one public function:</p>
+
+<typing>
+local x = xml.convert(somestring)
+</typing>
+
+<p>An optional second boolean argument tells this function not to create a root
+element.</p>
+
+<p>Valid entities are:</p>
+
+<typing>
+<!ENTITY xxxx SYSTEM "yyyy" NDATA zzzz>
+<!ENTITY xxxx PUBLIC "yyyy" >
+<!ENTITY xxxx "yyyy" >
+</typing>
+--ldx]]--
+
+-- not just one big nested table capture (lpeg overflow)
+
+local nsremap, resolvens = xml.xmlns, xml.resolvens
+
+local stack, top, dt, at, xmlns, errorstr, entities = { }, { }, { }, { }, { }, nil, { }
+local strip, cleanup, utfize, resolve, resolve_predefined, unify_predefined = false, false, false, false, false, false
+local dcache, hcache, acache = { }, { }, { }
+
+local mt = { }
+
+function initialize_mt(root)
+    mt = { __index = root } -- will be redefined later
+end
+
+function xml.setproperty(root,k,v)
+    getmetatable(root).__index[k] = v
+end
+
+function xml.check_error(top,toclose)
+    return ""
+end
+
+local function add_attribute(namespace,tag,value)
+    if cleanup and #value > 0 then
+        value = cleanup(value) -- new
+    end
+    if tag == "xmlns" then
+        xmlns[#xmlns+1] = resolvens(value)
+        at[tag] = value
+    elseif namespace == "" then
+        at[tag] = value
+    elseif namespace == "xmlns" then
+        xml.checkns(tag,value)
+        at["xmlns:" .. tag] = value
+    else
+        -- for the moment this way:
+        at[namespace .. ":" .. tag] = value
+    end
+end
+
+local function add_empty(spacing, namespace, tag)
+    if #spacing > 0 then
+        dt[#dt+1] = spacing
+    end
+    local resolved = (namespace == "" and xmlns[#xmlns]) or nsremap[namespace] or namespace
+    top = stack[#stack]
+    dt = top.dt
+    local t = { ns=namespace or "", rn=resolved, tg=tag, at=at, dt={}, __p__ = top }
+    dt[#dt+1] = t
+    setmetatable(t, mt)
+    if at.xmlns then
+        remove(xmlns)
+    end
+    at = { }
+end
+
+local function add_begin(spacing, namespace, tag)
+    if #spacing > 0 then
+        dt[#dt+1] = spacing
+    end
+    local resolved = (namespace == "" and xmlns[#xmlns]) or nsremap[namespace] or namespace
+    top = { ns=namespace or "", rn=resolved, tg=tag, at=at, dt={}, __p__ = stack[#stack] }
+    setmetatable(top, mt)
+    dt = top.dt
+    stack[#stack+1] = top
+    at = { }
+end
+
+local function add_end(spacing, namespace, tag)
+    if #spacing > 0 then
+        dt[#dt+1] = spacing
+    end
+    local toclose = remove(stack)
+    top = stack[#stack]
+    if #stack < 1 then
+        errorstr = format("nothing to close with %s %s", tag, xml.check_error(top,toclose) or "")
+    elseif toclose.tg ~= tag then -- no namespace check
+        errorstr = format("unable to close %s with %s %s", toclose.tg, tag, xml.check_error(top,toclose) or "")
+    end
+    dt = top.dt
+    dt[#dt+1] = toclose
+ -- dt[0] = top -- nasty circular reference when serializing table
+    if toclose.at.xmlns then
+        remove(xmlns)
+    end
+end
+
+local function add_text(text)
+    if cleanup and #text > 0 then
+        dt[#dt+1] = cleanup(text)
+    else
+        dt[#dt+1] = text
+    end
+end
+
+local function add_special(what, spacing, text)
+    if #spacing > 0 then
+        dt[#dt+1] = spacing
+    end
+    if strip and (what == "@cm@" or what == "@dt@") then
+        -- forget it
+    else
+        dt[#dt+1] = { special=true, ns="", tg=what, dt={ text } }
+    end
+end
+
+local function set_message(txt)
+    errorstr = "garbage at the end of the file: " .. gsub(txt,"([ \n\r\t]*)","")
+end
+
+local reported_attribute_errors = { }
+
+local function attribute_value_error(str)
+    if not reported_attribute_errors[str] then
+        logs.report("xml","invalid attribute value: %q",str)
+        reported_attribute_errors[str] = true
+        at._error_ = str
+    end
+    return str
+end
+local function attribute_specification_error(str)
+    if not reported_attribute_errors[str] then
+        logs.report("xml","invalid attribute specification: %q",str)
+        reported_attribute_errors[str] = true
+        at._error_ = str
+    end
+    return str
+end
+
+function xml.unknown_dec_entity_format(str) return (str == "" and "&error;") or format("&%s;",str) end
+function xml.unknown_hex_entity_format(str) return format("&#x%s;",str) end
+function xml.unknown_any_entity_format(str) return format("&#x%s;",str) end
+
+local function fromhex(s)
+    local n = tonumber(s,16)
+    if n then
+        return utfchar(n)
+    else
+        return format("h:%s",s), true
+    end
+end
+
+local function fromdec(s)
+    local n = tonumber(s)
+    if n then
+        return utfchar(n)
+    else
+        return format("d:%s",s), true
+    end
+end
+
+-- one level expansion (simple case), no checking done
+
+local rest = (1-P(";"))^0
+local many = P(1)^0
+
+local parsedentity =
+    P("&") * (P("#x")*(rest/fromhex) + P("#")*(rest/fromdec)) * P(";") * P(-1) +
+             (P("#x")*(many/fromhex) + P("#")*(many/fromdec))
+
+-- parsing in the xml file
+
+local predefined_unified = {
+    [38] = "&amp;",
+    [42] = "&quot;",
+    [47] = "&apos;",
+    [74] = "&lt;",
+    [76] = "&gr;",
+}
+
+local predefined_simplified = {
+    [38] = "&", amp  = "&",
+    [42] = '"', quot = '"',
+    [47] = "'", apos = "'",
+    [74] = "<", lt   = "<",
+    [76] = ">", gt   = ">",
+}
+
+local function handle_hex_entity(str)
+    local h = hcache[str]
+    if not h then
+        local n = tonumber(str,16)
+        h = unify_predefined and predefined_unified[n]
+        if h then
+            if trace_entities then
+                logs.report("xml","utfize, converting hex entity &#x%s; into %s",str,h)
+            end
+        elseif utfize then
+            h = (n and utfchar(n)) or xml.unknown_hex_entity_format(str) or ""
+            if not n then
+                logs.report("xml","utfize, ignoring hex entity &#x%s;",str)
+            elseif trace_entities then
+                logs.report("xml","utfize, converting hex entity &#x%s; into %s",str,h)
+            end
+        else
+            if trace_entities then
+                logs.report("xml","found entity &#x%s;",str)
+            end
+            h = "&#x" .. str .. ";"
+        end
+        hcache[str] = h
+    end
+    return h
+end
+
+local function handle_dec_entity(str)
+    local d = dcache[str]
+    if not d then
+        local n = tonumber(str)
+        d = unify_predefined and predefined_unified[n]
+        if d then
+            if trace_entities then
+                logs.report("xml","utfize, converting dec entity &#%s; into %s",str,d)
+            end
+        elseif utfize then
+            d = (n and utfchar(n)) or xml.unknown_dec_entity_format(str) or ""
+            if not n then
+                logs.report("xml","utfize, ignoring dec entity &#%s;",str)
+            elseif trace_entities then
+                logs.report("xml","utfize, converting dec entity &#%s; into %s",str,h)
+            end
+        else
+            if trace_entities then
+                logs.report("xml","found entity &#%s;",str)
+            end
+            d = "&#" .. str .. ";"
+        end
+        dcache[str] = d
+    end
+    return d
+end
+
+xml.parsedentitylpeg = parsedentity
+
+local function handle_any_entity(str)
+    if resolve then
+        local a = acache[str] -- per instance ! todo
+        if not a then
+            a = resolve_predefined and predefined_simplified[str]
+            if a then
+                -- one of the predefined
+            elseif type(resolve) == "function" then
+                a = resolve(str) or entities[str]
+            else
+                a = entities[str]
+            end
+            if a then
+                if trace_entities then
+                    logs.report("xml","resolved entity &%s; -> %s (internal)",str,a)
+                end
+                a = lpegmatch(parsedentity,a) or a
+            else
+                if xml.unknown_any_entity_format then
+                    a = xml.unknown_any_entity_format(str) or ""
+                end
+                if a then
+                    if trace_entities then
+                        logs.report("xml","resolved entity &%s; -> %s (external)",str,a)
+                    end
+                else
+                    if trace_entities then
+                        logs.report("xml","keeping entity &%s;",str)
+                    end
+                    if str == "" then
+                        a = "&error;"
+                    else
+                        a = "&" .. str .. ";"
+                    end
+                end
+            end
+            acache[str] = a
+        elseif trace_entities then
+            if not acache[str] then
+                logs.report("xml","converting entity &%s; into %s",str,a)
+                acache[str] = a
+            end
+        end
+        return a
+    else
+        local a = acache[str]
+        if not a then
+            if trace_entities then
+                logs.report("xml","found entity &%s;",str)
+            end
+            a = resolve_predefined and predefined_simplified[str]
+            if a then
+                -- one of the predefined
+                acache[str] = a
+            elseif str == "" then
+                a = "&error;"
+                acache[str] = a
+            else
+                a = "&" .. str .. ";"
+                acache[str] = a
+            end
+        end
+        return a
+    end
+end
+
+local function handle_end_entity(chr)
+    logs.report("xml","error in entity, %q found instead of ';'",chr)
+end
+
+local space            = S(' \r\n\t')
+local open             = P('<')
+local close            = P('>')
+local squote           = S("'")
+local dquote           = S('"')
+local equal            = P('=')
+local slash            = P('/')
+local colon            = P(':')
+local semicolon        = P(';')
+local ampersand        = P('&')
+local valid            = R('az', 'AZ', '09') + S('_-.')
+local name_yes         = C(valid^1) * colon * C(valid^1)
+local name_nop         = C(P(true)) * C(valid^1)
+local name             = name_yes + name_nop
+local utfbom           = lpeg.patterns.utfbom -- no capture
+local spacing          = C(space^0)
+
+----- entitycontent    = (1-open-semicolon)^0
+local anyentitycontent = (1-open-semicolon-space-close)^0
+local hexentitycontent = R("AF","af","09")^0
+local decentitycontent = R("09")^0
+local parsedentity     = P("#")/"" * (
+                                P("x")/"" * (hexentitycontent/handle_hex_entity) +
+                                            (decentitycontent/handle_dec_entity)
+                            ) +             (anyentitycontent/handle_any_entity)
+local entity           = ampersand/"" * parsedentity * ( (semicolon/"") + #(P(1)/handle_end_entity))
+
+local text_unparsed    = C((1-open)^1)
+local text_parsed      = Cs(((1-open-ampersand)^1 + entity)^1)
+
+local somespace        = space^1
+local optionalspace    = space^0
+
+----- value            = (squote * C((1 - squote)^0) * squote) + (dquote * C((1 - dquote)^0) * dquote) -- ampersand and < also invalid in value
+local value            = (squote * Cs((entity + (1 - squote))^0) * squote) + (dquote * Cs((entity + (1 - dquote))^0) * dquote) -- ampersand and < also invalid in value
+
+local endofattributes  = slash * close + close -- recovery of flacky html
+local whatever         = space * name * optionalspace * equal
+local wrongvalue       = C(P(1-whatever-close)^1 + P(1-close)^1) / attribute_value_error
+----- wrongvalue       = C(P(1-whatever-endofattributes)^1 + P(1-endofattributes)^1) / attribute_value_error
+----- wrongvalue       = C(P(1-space-endofattributes)^1) / attribute_value_error
+local wrongvalue       = Cs(P(entity + (1-space-endofattributes))^1) / attribute_value_error
+
+local attributevalue   = value + wrongvalue
+
+local attribute        = (somespace * name * optionalspace * equal * optionalspace * attributevalue) / add_attribute
+----- attributes       = (attribute)^0
+
+local attributes       = (attribute + somespace^-1 * (((1-endofattributes)^1)/attribute_specification_error))^0
+
+local parsedtext       = text_parsed   / add_text
+local unparsedtext     = text_unparsed / add_text
+local balanced         = P { "[" * ((1 - S"[]") + V(1))^0 * "]" } -- taken from lpeg manual, () example
+
+local emptyelement     = (spacing * open         * name * attributes * optionalspace * slash * close) / add_empty
+local beginelement     = (spacing * open         * name * attributes * optionalspace         * close) / add_begin
+local endelement       = (spacing * open * slash * name              * optionalspace         * close) / add_end
+
+local begincomment     = open * P("!--")
+local endcomment       = P("--") * close
+local begininstruction = open * P("?")
+local endinstruction   = P("?") * close
+local begincdata       = open * P("![CDATA[")
+local endcdata         = P("]]") * close
+
+local someinstruction  = C((1 - endinstruction)^0)
+local somecomment      = C((1 - endcomment    )^0)
+local somecdata        = C((1 - endcdata      )^0)
+
+local function normalentity(k,v  ) entities[k] = v end
+local function systementity(k,v,n) entities[k] = v end
+local function publicentity(k,v,n) entities[k] = v end
+
+local begindoctype     = open * P("!DOCTYPE")
+local enddoctype       = close
+local beginset         = P("[")
+local endset           = P("]")
+local doctypename      = C((1-somespace-close)^0)
+local elementdoctype   = optionalspace * P("<!ELEMENT") * (1-close)^0 * close
+
+local normalentitytype = (doctypename * somespace * value)/normalentity
+local publicentitytype = (doctypename * somespace * P("PUBLIC") * somespace * value)/publicentity
+local systementitytype = (doctypename * somespace * P("SYSTEM") * somespace * value * somespace * P("NDATA") * somespace * doctypename)/systementity
+local entitydoctype    = optionalspace * P("<!ENTITY") * somespace * (systementitytype + publicentitytype + normalentitytype) * optionalspace * close
+
+local doctypeset       = beginset * optionalspace * P(elementdoctype + entitydoctype + space)^0 * optionalspace * endset
+local definitiondoctype= doctypename * somespace * doctypeset
+local publicdoctype    = doctypename * somespace * P("PUBLIC") * somespace * value * somespace * value * somespace * doctypeset
+local systemdoctype    = doctypename * somespace * P("SYSTEM") * somespace * value * somespace * doctypeset
+local simpledoctype    = (1-close)^1 -- * balanced^0
+local somedoctype      = C((somespace * (publicdoctype + systemdoctype + definitiondoctype + simpledoctype) * optionalspace)^0)
+
+local instruction      = (spacing * begininstruction * someinstruction * endinstruction) / function(...) add_special("@pi@",...) end
+local comment          = (spacing * begincomment     * somecomment     * endcomment    ) / function(...) add_special("@cm@",...) end
+local cdata            = (spacing * begincdata       * somecdata       * endcdata      ) / function(...) add_special("@cd@",...) end
+local doctype          = (spacing * begindoctype     * somedoctype     * enddoctype    ) / function(...) add_special("@dt@",...) end
+
+--  nicer but slower:
+--
+--  local instruction = (Cc("@pi@") * spacing * begininstruction * someinstruction * endinstruction) / add_special
+--  local comment     = (Cc("@cm@") * spacing * begincomment     * somecomment     * endcomment    ) / add_special
+--  local cdata       = (Cc("@cd@") * spacing * begincdata       * somecdata       * endcdata      ) / add_special
+--  local doctype     = (Cc("@dt@") * spacing * begindoctype     * somedoctype     * enddoctype    ) / add_special
+
+local trailer = space^0 * (text_unparsed/set_message)^0
+
+--  comment + emptyelement + text + cdata + instruction + V("parent"), -- 6.5 seconds on 40 MB database file
+--  text + comment + emptyelement + cdata + instruction + V("parent"), -- 5.8
+--  text + V("parent") + emptyelement + comment + cdata + instruction, -- 5.5
+
+local grammar_parsed_text = P { "preamble",
+    preamble = utfbom^0 * instruction^0 * (doctype + comment + instruction)^0 * V("parent") * trailer,
+    parent   = beginelement * V("children")^0 * endelement,
+    children = parsedtext + V("parent") + emptyelement + comment + cdata + instruction,
+}
+
+local grammar_unparsed_text = P { "preamble",
+    preamble = utfbom^0 * instruction^0 * (doctype + comment + instruction)^0 * V("parent") * trailer,
+    parent   = beginelement * V("children")^0 * endelement,
+    children = unparsedtext + V("parent") + emptyelement + comment + cdata + instruction,
+}
+
+-- maybe we will add settinsg to result as well
+
+local function xmlconvert(data, settings)
+    settings = settings or { } -- no_root strip_cm_and_dt given_entities parent_root error_handler
+    strip = settings.strip_cm_and_dt
+    utfize = settings.utfize_entities
+    resolve = settings.resolve_entities
+    resolve_predefined = settings.resolve_predefined_entities -- in case we have escaped entities
+    unify_predefined = settings.unify_predefined_entities -- &#038; -> &amp;
+    cleanup = settings.text_cleanup
+    stack, top, at, xmlns, errorstr, result, entities = { }, { }, { }, { }, nil, nil, settings.entities or { }
+    acache, hcache, dcache = { }, { }, { } -- not stored
+    reported_attribute_errors = { }
+    if settings.parent_root then
+        mt = getmetatable(settings.parent_root)
+    else
+        initialize_mt(top)
+    end
+    stack[#stack+1] = top
+    top.dt = { }
+    dt = top.dt
+    if not data or data == "" then
+        errorstr = "empty xml file"
+    elseif utfize or resolve then
+        if lpegmatch(grammar_parsed_text,data) then
+            errorstr = ""
+        else
+            errorstr = "invalid xml file - parsed text"
+        end
+    elseif type(data) == "string" then
+        if lpegmatch(grammar_unparsed_text,data) then
+            errorstr = ""
+        else
+            errorstr = "invalid xml file - unparsed text"
+        end
+    else
+        errorstr = "invalid xml file - no text at all"
+    end
+    if errorstr and errorstr ~= "" then
+        result = { dt = { { ns = "", tg = "error", dt = { errorstr }, at={ }, er = true } } }
+        setmetatable(stack, mt)
+        local error_handler = settings.error_handler
+        if error_handler == false then
+            -- no error message
+        else
+            error_handler = error_handler or xml.error_handler
+            if error_handler then
+                xml.error_handler("load",errorstr)
+            end
+        end
+    else
+        result = stack[1]
+    end
+    if not settings.no_root then
+        result = { special = true, ns = "", tg = '@rt@', dt = result.dt, at={ }, entities = entities, settings = settings }
+        setmetatable(result, mt)
+        local rdt = result.dt
+        for k=1,#rdt do
+            local v = rdt[k]
+            if type(v) == "table" and not v.special then -- always table -)
+                result.ri = k -- rootindex
+v.__p__ = result  -- new, experiment, else we cannot go back to settings, we need to test this !
+                break
+            end
+        end
+    end
+    if errorstr and errorstr ~= "" then
+        result.error = true
+    end
+    return result
+end
+
+xml.convert = xmlconvert
+
+function xml.inheritedconvert(data,xmldata)
+    local settings = xmldata.settings
+    settings.parent_root = xmldata -- to be tested
+ -- settings.no_root = true
+    local xc = xmlconvert(data,settings)
+ -- xc.settings = nil
+ -- xc.entities = nil
+ -- xc.special = nil
+ -- xc.ri = nil
+ -- print(xc.tg)
+    return xc
+end
+
+--[[ldx--
+<p>Packaging data in an xml like table is done with the following
+function. Maybe it will go away (when not used).</p>
+--ldx]]--
+
+function xml.is_valid(root)
+    return root and root.dt and root.dt[1] and type(root.dt[1]) == "table" and not root.dt[1].er
+end
+
+function xml.package(tag,attributes,data)
+    local ns, tg = match(tag,"^(.-):?([^:]+)$")
+    local t = { ns = ns, tg = tg, dt = data or "", at = attributes or {} }
+    setmetatable(t, mt)
+    return t
+end
+
+function xml.is_valid(root)
+    return root and not root.error
+end
+
+xml.error_handler = (logs and logs.report) or (input and logs.report) or print
+
+--[[ldx--
+<p>We cannot load an <l n='lpeg'/> from a filehandle so we need to load
+the whole file first. The function accepts a string representing
+a filename or a file handle.</p>
+--ldx]]--
+
+function xml.load(filename,settings)
+    local data = ""
+    if type(filename) == "string" then
+     -- local data = io.loaddata(filename) - -todo: check type in io.loaddata
+        local f = io.open(filename,'r')
+        if f then
+            data = f:read("*all")
+            f:close()
+        end
+    elseif filename then -- filehandle
+        data = filename:read("*all")
+    end
+    return xmlconvert(data,settings)
+end
+
+--[[ldx--
+<p>When we inject new elements, we need to convert strings to
+valid trees, which is what the next function does.</p>
+--ldx]]--
+
+local no_root = { no_root = true }
+
+function xml.toxml(data)
+    if type(data) == "string" then
+        local root = { xmlconvert(data,no_root) }
+        return (#root > 1 and root) or root[1]
+    else
+        return data
+    end
+end
+
+--[[ldx--
+<p>For copying a tree we use a dedicated function instead of the
+generic table copier. Since we know what we're dealing with we
+can speed up things a bit. The second argument is not to be used!</p>
+--ldx]]--
+
+local function copy(old,tables)
+    if old then
+        tables = tables or { }
+        local new = { }
+        if not tables[old] then
+            tables[old] = new
+        end
+        for k,v in next, old do
+            new[k] = (type(v) == "table" and (tables[v] or copy(v, tables))) or v
+        end
+        local mt = getmetatable(old)
+        if mt then
+            setmetatable(new,mt)
+        end
+        return new
+    else
+        return { }
+    end
+end
+
+xml.copy = copy
+
+--[[ldx--
+<p>In <l n='context'/> serializing the tree or parts of the tree is a major
+actitivity which is why the following function is pretty optimized resulting
+in a few more lines of code than needed. The variant that uses the formatting
+function for all components is about 15% slower than the concatinating
+alternative.</p>
+--ldx]]--
+
+-- todo: add <?xml version='1.0' standalone='yes'?> when not present
+
+function xml.checkbom(root) -- can be made faster
+    if root.ri then
+        local dt, found = root.dt, false
+        for k=1,#dt do
+            local v = dt[k]
+            if type(v) == "table" and v.special and v.tg == "@pi@" and find(v.dt[1],"xml.*version=") then
+                found = true
+                break
+            end
+        end
+        if not found then
+            insert(dt, 1, { special=true, ns="", tg="@pi@", dt = { "xml version='1.0' standalone='yes'"} } )
+            insert(dt, 2, "\n" )
+        end
+    end
+end
+
+--[[ldx--
+<p>At the cost of some 25% runtime overhead you can first convert the tree to a string
+and then handle the lot.</p>
+--ldx]]--
+
+-- new experimental reorganized serialize
+
+local function verbose_element(e,handlers)
+    local handle = handlers.handle
+    local serialize = handlers.serialize
+    local ens, etg, eat, edt, ern = e.ns, e.tg, e.at, e.dt, e.rn
+    local ats = eat and next(eat) and { }
+    if ats then
+        for k,v in next, eat do
+            ats[#ats+1] = format('%s=%q',k,v)
+        end
+    end
+    if ern and trace_remap and ern ~= ens then
+        ens = ern
+    end
+    if ens ~= "" then
+        if edt and #edt > 0 then
+            if ats then
+                handle("<",ens,":",etg," ",concat(ats," "),">")
+            else
+                handle("<",ens,":",etg,">")
+            end
+            for i=1,#edt do
+                local e = edt[i]
+                if type(e) == "string" then
+                    handle(e)
+                else
+                    serialize(e,handlers)
+                end
+            end
+            handle("</",ens,":",etg,">")
+        else
+            if ats then
+                handle("<",ens,":",etg," ",concat(ats," "),"/>")
+            else
+                handle("<",ens,":",etg,"/>")
+            end
+        end
+    else
+        if edt and #edt > 0 then
+            if ats then
+                handle("<",etg," ",concat(ats," "),">")
+            else
+                handle("<",etg,">")
+            end
+            for i=1,#edt do
+                local ei = edt[i]
+                if type(ei) == "string" then
+                    handle(ei)
+                else
+                    serialize(ei,handlers)
+                end
+            end
+            handle("</",etg,">")
+        else
+            if ats then
+                handle("<",etg," ",concat(ats," "),"/>")
+            else
+                handle("<",etg,"/>")
+            end
+        end
+    end
+end
+
+local function verbose_pi(e,handlers)
+    handlers.handle("<?",e.dt[1],"?>")
+end
+
+local function verbose_comment(e,handlers)
+    handlers.handle("<!--",e.dt[1],"-->")
+end
+
+local function verbose_cdata(e,handlers)
+    handlers.handle("<![CDATA[", e.dt[1],"]]>")
+end
+
+local function verbose_doctype(e,handlers)
+    handlers.handle("<!DOCTYPE ",e.dt[1],">")
+end
+
+local function verbose_root(e,handlers)
+    handlers.serialize(e.dt,handlers)
+end
+
+local function verbose_text(e,handlers)
+    handlers.handle(e)
+end
+
+local function verbose_document(e,handlers)
+    local serialize = handlers.serialize
+    local functions = handlers.functions
+    for i=1,#e do
+        local ei = e[i]
+        if type(ei) == "string" then
+            functions["@tx@"](ei,handlers)
+        else
+            serialize(ei,handlers)
+        end
+    end
+end
+
+local function serialize(e,handlers,...)
+    local initialize = handlers.initialize
+    local finalize   = handlers.finalize
+    local functions  = handlers.functions
+    if initialize then
+        local state = initialize(...)
+        if not state == true then
+            return state
+        end
+    end
+    local etg = e.tg
+    if etg then
+        (functions[etg] or functions["@el@"])(e,handlers)
+ -- elseif type(e) == "string" then
+ --     functions["@tx@"](e,handlers)
+    else
+        functions["@dc@"](e,handlers)
+    end
+    if finalize then
+        return finalize()
+    end
+end
+
+local function xserialize(e,handlers)
+    local functions = handlers.functions
+    local etg = e.tg
+    if etg then
+        (functions[etg] or functions["@el@"])(e,handlers)
+ -- elseif type(e) == "string" then
+ --     functions["@tx@"](e,handlers)
+    else
+        functions["@dc@"](e,handlers)
+    end
+end
+
+local handlers = { }
+
+local function newhandlers(settings)
+    local t = table.copy(handlers.verbose or { }) -- merge
+    if settings then
+        for k,v in next, settings do
+            if type(v) == "table" then
+                tk = t[k] if not tk then tk = { } t[k] = tk end
+                for kk,vv in next, v do
+                    tk[kk] = vv
+                end
+            else
+                t[k] = v
+            end
+        end
+        if settings.name then
+            handlers[settings.name] = t
+        end
+    end
+    return t
+end
+
+local nofunction = function() end
+
+function xml.sethandlersfunction(handler,name,fnc)
+    handler.functions[name] = fnc or nofunction
+end
+
+function xml.gethandlersfunction(handler,name)
+    return handler.functions[name]
+end
+
+function xml.gethandlers(name)
+    return handlers[name]
+end
+
+newhandlers {
+    name       = "verbose",
+    initialize = false, -- faster than nil and mt lookup
+    finalize   = false, -- faster than nil and mt lookup
+    serialize  = xserialize,
+    handle     = print,
+    functions  = {
+        ["@dc@"]   = verbose_document,
+        ["@dt@"]   = verbose_doctype,
+        ["@rt@"]   = verbose_root,
+        ["@el@"]   = verbose_element,
+        ["@pi@"]   = verbose_pi,
+        ["@cm@"]   = verbose_comment,
+        ["@cd@"]   = verbose_cdata,
+        ["@tx@"]   = verbose_text,
+    }
+}
+
+--[[ldx--
+<p>How you deal with saving data depends on your preferences. For a 40 MB database
+file the timing on a 2.3 Core Duo are as follows (time in seconds):</p>
+
+<lines>
+1.3 : load data from file to string
+6.1 : convert string into tree
+5.3 : saving in file using xmlsave
+6.8 : converting to string using xml.tostring
+3.6 : saving converted string in file
+</lines>
+
+<p>Beware, these were timing with the old routine but measurements will not be that
+much different I guess.</p>
+--ldx]]--
+
+-- maybe this will move to lxml-xml
+
+local result
+
+local xmlfilehandler = newhandlers {
+    name       = "file",
+    initialize = function(name) result = io.open(name,"wb") return result end,
+    finalize   = function() result:close() return true end,
+    handle     = function(...) result:write(...) end,
+}
+
+-- no checking on writeability here but not faster either
+--
+-- local xmlfilehandler = newhandlers {
+--     initialize = function(name) io.output(name,"wb") return true end,
+--     finalize   = function() io.close() return true end,
+--     handle     = io.write,
+-- }
+
+
+function xml.save(root,name)
+    serialize(root,xmlfilehandler,name)
+end
+
+local result
+
+local xmlstringhandler = newhandlers {
+    name       = "string",
+    initialize = function() result = { } return result end,
+    finalize   = function() return concat(result) end,
+    handle     = function(...) result[#result+1] = concat { ... } end
+}
+
+local function xmltostring(root) -- 25% overhead due to collecting
+    if root then
+        if type(root) == 'string' then
+            return root
+        else -- if next(root) then -- next is faster than type (and >0 test)
+            return serialize(root,xmlstringhandler) or ""
+        end
+    end
+    return ""
+end
+
+local function xmltext(root) -- inline
+    return (root and xmltostring(root)) or ""
+end
+
+function initialize_mt(root)
+    mt = { __tostring = xmltext, __index = root }
+end
+
+xml.defaulthandlers = handlers
+xml.newhandlers     = newhandlers
+xml.serialize       = serialize
+xml.tostring        = xmltostring
+
+--[[ldx--
+<p>The next function operated on the content only and needs a handle function
+that accepts a string.</p>
+--ldx]]--
+
+local function xmlstring(e,handle)
+    if not handle or (e.special and e.tg ~= "@rt@") then
+        -- nothing
+    elseif e.tg then
+        local edt = e.dt
+        if edt then
+            for i=1,#edt do
+                xmlstring(edt[i],handle)
+            end
+        end
+    else
+        handle(e)
+    end
+end
+
+xml.string = xmlstring
+
+--[[ldx--
+<p>A few helpers:</p>
+--ldx]]--
+
+--~ xmlsetproperty(root,"settings",settings)
+
+function xml.settings(e)
+    while e do
+        local s = e.settings
+        if s then
+            return s
+        else
+            e = e.__p__
+        end
+    end
+    return nil
+end
+
+function xml.root(e)
+    local r = e
+    while e do
+        e = e.__p__
+        if e then
+            r = e
+        end
+    end
+    return r
+end
+
+function xml.parent(root)
+    return root.__p__
+end
+
+function xml.body(root)
+    return (root.ri and root.dt[root.ri]) or root -- not ok yet
+end
+
+function xml.name(root)
+    if not root then
+        return ""
+    elseif root.ns == "" then
+        return root.tg
+    else
+        return root.ns .. ":" .. root.tg
+    end
+end
+
+--[[ldx--
+<p>The next helper erases an element but keeps the table as it is,
+and since empty strings are not serialized (effectively) it does
+not harm. Copying the table would take more time. Usage:</p>
+--ldx]]--
+
+function xml.erase(dt,k)
+    if dt then
+        if k then
+            dt[k] = ""
+        else for k=1,#dt do
+            dt[1] = { "" }
+        end end
+    end
+end
+
+--[[ldx--
+<p>The next helper assigns a tree (or string). Usage:</p>
+
+<typing>
+dt[k] = xml.assign(root) or xml.assign(dt,k,root)
+</typing>
+--ldx]]--
+
+function xml.assign(dt,k,root)
+    if dt and k then
+        dt[k] = (type(root) == "table" and xml.body(root)) or root
+        return dt[k]
+    else
+        return xml.body(root)
+    end
+end
+
+-- the following helpers may move
+
+--[[ldx--
+<p>The next helper assigns a tree (or string). Usage:</p>
+<typing>
+xml.tocdata(e)
+xml.tocdata(e,"error")
+</typing>
+--ldx]]--
+
+function xml.tocdata(e,wrapper)
+    local whatever = xmltostring(e.dt)
+    if wrapper then
+        whatever = format("<%s>%s</%s>",wrapper,whatever,wrapper)
+    end
+    local t = { special = true, ns = "", tg = "@cd@", at = {}, rn = "", dt = { whatever }, __p__ = e }
+    setmetatable(t,getmetatable(e))
+    e.dt = { t }
+end
+
+function xml.makestandalone(root)
+    if root.ri then
+        local dt = root.dt
+        for k=1,#dt do
+            local v = dt[k]
+            if type(v) == "table" and v.special and v.tg == "@pi@" then
+                local txt = v.dt[1]
+                if find(txt,"xml.*version=") then
+                    v.dt[1] = txt .. " standalone='yes'"
+                    break
+                end
+            end
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['lxml-pth'] = {
+    version   = 1.001,
+    comment   = "this module is the basis for the lxml-* ones",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- e.ni is only valid after a filter run
+
+local concat, remove, insert = table.concat, table.remove, table.insert
+local type, next, tonumber, tostring, setmetatable, loadstring = type, next, tonumber, tostring, setmetatable, loadstring
+local format, upper, lower, gmatch, gsub, find, rep = string.format, string.upper, string.lower, string.gmatch, string.gsub, string.find, string.rep
+local lpegmatch = lpeg.match
+
+-- beware, this is not xpath ... e.g. position is different (currently) and
+-- we have reverse-sibling as reversed preceding sibling
+
+--[[ldx--
+<p>This module can be used stand alone but also inside <l n='mkiv'/> in
+which case it hooks into the tracker code. Therefore we provide a few
+functions that set the tracers. Here we overload a previously defined
+function.</p>
+<p>If I can get in the mood I will make a variant that is XSLT compliant
+but I wonder if it makes sense.</P>
+--ldx]]--
+
+--[[ldx--
+<p>Expecially the lpath code is experimental, we will support some of xpath, but
+only things that make sense for us; as compensation it is possible to hook in your
+own functions. Apart from preprocessing content for <l n='context'/> we also need
+this module for process management, like handling <l n='ctx'/> and <l n='rlx'/>
+files.</p>
+
+<typing>
+a/b/c /*/c
+a/b/c/first() a/b/c/last() a/b/c/index(n) a/b/c/index(-n)
+a/b/c/text() a/b/c/text(1) a/b/c/text(-1) a/b/c/text(n)
+</typing>
+--ldx]]--
+
+local trace_lpath    = false  if trackers then trackers.register("xml.path",    function(v) trace_lpath  = v end) end
+local trace_lparse   = false  if trackers then trackers.register("xml.parse",   function(v) trace_lparse = v end) end
+local trace_lprofile = false  if trackers then trackers.register("xml.profile", function(v) trace_lpath  = v trace_lparse = v trace_lprofile = v end) end
+
+--[[ldx--
+<p>We've now arrived at an interesting part: accessing the tree using a subset
+of <l n='xpath'/> and since we're not compatible we call it <l n='lpath'/>. We
+will explain more about its usage in other documents.</p>
+--ldx]]--
+
+local lpathcalls  = 0  function xml.lpathcalls () return lpathcalls  end
+local lpathcached = 0  function xml.lpathcached() return lpathcached end
+
+xml.functions      = xml.functions      or { } -- internal
+xml.expressions    = xml.expressions    or { } -- in expressions
+xml.finalizers     = xml.finalizers     or { } -- fast do-with ... (with return value other than collection)
+xml.specialhandler = xml.specialhandler or { }
+
+local functions   = xml.functions
+local expressions = xml.expressions
+local finalizers  = xml.finalizers
+
+finalizers.xml = finalizers.xml or { }
+finalizers.tex = finalizers.tex or { }
+
+local function fallback (t, name)
+    local fn = finalizers[name]
+    if fn then
+        t[name] = fn
+    else
+        logs.report("xml","unknown sub finalizer '%s'",tostring(name))
+        fn = function() end
+    end
+    return fn
+end
+
+setmetatable(finalizers.xml, { __index = fallback })
+setmetatable(finalizers.tex, { __index = fallback })
+
+xml.defaultprotocol = "xml"
+
+-- as xsl does not follow xpath completely here we will also
+-- be more liberal especially with regards to the use of | and
+-- the rootpath:
+--
+-- test    : all 'test' under current
+-- /test   : 'test' relative to current
+-- a|b|c   : set of names
+-- (a|b|c) : idem
+-- !       : not
+--
+-- after all, we're not doing transformations but filtering. in
+-- addition we provide filter functions (last bit)
+--
+-- todo: optimizer
+--
+-- .. : parent
+-- *  : all kids
+-- /  : anchor here
+-- // : /**/
+-- ** : all in between
+--
+-- so far we had (more practical as we don't transform)
+--
+-- {/test}   : kids 'test' under current node
+-- {test}    : any kid with tag 'test'
+-- {//test}  : same as above
+
+-- evaluator (needs to be redone, for the moment copied)
+
+-- todo: apply_axis(list,notable) and collection vs single
+
+local apply_axis = { }
+
+apply_axis['root'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        local rt = ll
+        while ll do
+            ll = ll.__p__
+            if ll then
+                rt = ll
+            end
+        end
+        collected[#collected+1] = rt
+    end
+    return collected
+end
+
+apply_axis['self'] = function(list)
+--~     local collected = { }
+--~     for l=1,#list do
+--~         collected[#collected+1] = list[l]
+--~     end
+--~     return collected
+    return list
+end
+
+apply_axis['child'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        local dt = ll.dt
+        local en = 0
+        for k=1,#dt do
+            local dk = dt[k]
+            if dk.tg then
+                collected[#collected+1] = dk
+                dk.ni = k -- refresh
+            en = en + 1
+            dk.ei = en
+            end
+        end
+        ll.en = en
+    end
+    return collected
+end
+
+local function collect(list,collected)
+    local dt = list.dt
+    if dt then
+        local en = 0
+        for k=1,#dt do
+            local dk = dt[k]
+            if dk.tg then
+                collected[#collected+1] = dk
+                dk.ni = k -- refresh
+                en = en + 1
+                dk.ei = en
+                collect(dk,collected)
+            end
+        end
+        list.en = en
+    end
+end
+apply_axis['descendant'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        collect(list[l],collected)
+    end
+    return collected
+end
+
+local function collect(list,collected)
+    local dt = list.dt
+    if dt then
+        local en = 0
+        for k=1,#dt do
+            local dk = dt[k]
+            if dk.tg then
+                collected[#collected+1] = dk
+                dk.ni = k -- refresh
+                en = en + 1
+                dk.ei = en
+                collect(dk,collected)
+            end
+        end
+        list.en = en
+    end
+end
+apply_axis['descendant-or-self'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        if ll.special ~= true then -- catch double root
+            collected[#collected+1] = ll
+        end
+        collect(ll,collected)
+    end
+    return collected
+end
+
+apply_axis['ancestor'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        while ll do
+            ll = ll.__p__
+            if ll then
+                collected[#collected+1] = ll
+            end
+        end
+    end
+    return collected
+end
+
+apply_axis['ancestor-or-self'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        collected[#collected+1] = ll
+        while ll do
+            ll = ll.__p__
+            if ll then
+                collected[#collected+1] = ll
+            end
+        end
+    end
+    return collected
+end
+
+apply_axis['parent'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local pl = list[l].__p__
+        if pl then
+            collected[#collected+1] = pl
+        end
+    end
+    return collected
+end
+
+apply_axis['attribute'] = function(list)
+    return { }
+end
+
+apply_axis['namespace'] = function(list)
+    return { }
+end
+
+apply_axis['following'] = function(list) -- incomplete
+--~     local collected = { }
+--~     for l=1,#list do
+--~         local ll = list[l]
+--~         local p = ll.__p__
+--~         local d = p.dt
+--~         for i=ll.ni+1,#d do
+--~             local di = d[i]
+--~             if type(di) == "table" then
+--~                 collected[#collected+1] = di
+--~                 break
+--~             end
+--~         end
+--~     end
+--~     return collected
+    return { }
+end
+
+apply_axis['preceding'] = function(list) -- incomplete
+--~     local collected = { }
+--~     for l=1,#list do
+--~         local ll = list[l]
+--~         local p = ll.__p__
+--~         local d = p.dt
+--~         for i=ll.ni-1,1,-1 do
+--~             local di = d[i]
+--~             if type(di) == "table" then
+--~                 collected[#collected+1] = di
+--~                 break
+--~             end
+--~         end
+--~     end
+--~     return collected
+    return { }
+end
+
+apply_axis['following-sibling'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        local p = ll.__p__
+        local d = p.dt
+        for i=ll.ni+1,#d do
+            local di = d[i]
+            if type(di) == "table" then
+                collected[#collected+1] = di
+            end
+        end
+    end
+    return collected
+end
+
+apply_axis['preceding-sibling'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        local p = ll.__p__
+        local d = p.dt
+        for i=1,ll.ni-1 do
+            local di = d[i]
+            if type(di) == "table" then
+                collected[#collected+1] = di
+            end
+        end
+    end
+    return collected
+end
+
+apply_axis['reverse-sibling'] = function(list) -- reverse preceding
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        local p = ll.__p__
+        local d = p.dt
+        for i=ll.ni-1,1,-1 do
+            local di = d[i]
+            if type(di) == "table" then
+                collected[#collected+1] = di
+            end
+        end
+    end
+    return collected
+end
+
+apply_axis['auto-descendant-or-self'] = apply_axis['descendant-or-self']
+apply_axis['auto-descendant']         = apply_axis['descendant']
+apply_axis['auto-child']              = apply_axis['child']
+apply_axis['auto-self']               = apply_axis['self']
+apply_axis['initial-child']           = apply_axis['child']
+
+local function apply_nodes(list,directive,nodes)
+    -- todo: nodes[1] etc ... negated node name in set ... when needed
+    -- ... currently ignored
+    local maxn = #nodes
+    if maxn == 3 then --optimized loop
+        local nns, ntg = nodes[2], nodes[3]
+        if not nns and not ntg then -- wildcard
+            if directive then
+                return list
+            else
+                return { }
+            end
+        else
+            local collected, m, p = { }, 0, nil
+            if not nns then -- only check tag
+                for l=1,#list do
+                    local ll = list[l]
+                    local ltg = ll.tg
+                    if ltg then
+                        if directive then
+                            if ntg == ltg then
+                                local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                                collected[#collected+1], ll.mi = ll, m
+                            end
+                        elseif ntg ~= ltg then
+                            local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                            collected[#collected+1], ll.mi = ll, m
+                        end
+                    end
+                end
+            elseif not ntg then -- only check namespace
+                for l=1,#list do
+                    local ll = list[l]
+                    local lns = ll.rn or ll.ns
+                    if lns then
+                        if directive then
+                            if lns == nns then
+                                local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                                collected[#collected+1], ll.mi = ll, m
+                            end
+                        elseif lns ~= nns then
+                            local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                            collected[#collected+1], ll.mi = ll, m
+                        end
+                    end
+                end
+            else -- check both
+                for l=1,#list do
+                    local ll = list[l]
+                    local ltg = ll.tg
+                    if ltg then
+                        local lns = ll.rn or ll.ns
+                        local ok = ltg == ntg and lns == nns
+                        if directive then
+                            if ok then
+                                local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                                collected[#collected+1], ll.mi = ll, m
+                            end
+                        elseif not ok then
+                            local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                            collected[#collected+1], ll.mi = ll, m
+                        end
+                    end
+                end
+            end
+            return collected
+        end
+    else
+        local collected, m, p = { }, 0, nil
+        for l=1,#list do
+            local ll = list[l]
+            local ltg = ll.tg
+            if ltg then
+                local lns = ll.rn or ll.ns
+                local ok = false
+                for n=1,maxn,3 do
+                    local nns, ntg = nodes[n+1], nodes[n+2]
+                    ok = (not ntg or ltg == ntg) and (not nns or lns == nns)
+                    if ok then
+                        break
+                    end
+                end
+                if directive then
+                    if ok then
+                        local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                        collected[#collected+1], ll.mi = ll, m
+                    end
+                elseif not ok then
+                    local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                    collected[#collected+1], ll.mi = ll, m
+                end
+            end
+        end
+        return collected
+    end
+end
+
+local quit_expression = false
+
+local function apply_expression(list,expression,order)
+    local collected = { }
+    quit_expression = false
+    for l=1,#list do
+        local ll = list[l]
+        if expression(list,ll,l,order) then -- nasty, order alleen valid als n=1
+            collected[#collected+1] = ll
+        end
+        if quit_expression then
+            break
+        end
+    end
+    return collected
+end
+
+local P, V, C, Cs, Cc, Ct, R, S, Cg, Cb = lpeg.P, lpeg.V, lpeg.C, lpeg.Cs, lpeg.Cc, lpeg.Ct, lpeg.R, lpeg.S, lpeg.Cg, lpeg.Cb
+
+local spaces     = S(" \n\r\t\f")^0
+local lp_space   = S(" \n\r\t\f")
+local lp_any     = P(1)
+local lp_noequal = P("!=") / "~=" + P("<=") + P(">=") + P("==")
+local lp_doequal = P("=")  / "=="
+local lp_or      = P("|")  / " or "
+local lp_and     = P("&")  / " and "
+
+local lp_builtin = P (
+        P("firstindex")   / "1" +
+        P("lastindex")    / "(#ll.__p__.dt or 1)" +
+        P("firstelement") / "1" +
+        P("lastelement")  / "(ll.__p__.en or 1)" +
+        P("first")        / "1" +
+        P("last")         / "#list" +
+        P("rootposition") / "order" +
+        P("position")     / "l" + -- is element in finalizer
+        P("order")        / "order" +
+        P("element")      / "(ll.ei or 1)" +
+        P("index")        / "(ll.ni or 1)" +
+        P("match")        / "(ll.mi or 1)" +
+        P("text")         / "(ll.dt[1] or '')" +
+    --  P("name")         / "(ll.ns~='' and ll.ns..':'..ll.tg)" +
+        P("name")         / "((ll.ns~='' and ll.ns..':'..ll.tg) or ll.tg)" +
+        P("tag")          / "ll.tg" +
+        P("ns")           / "ll.ns"
+    ) * ((spaces * P("(") * spaces * P(")"))/"")
+
+local lp_attribute = (P("@") + P("attribute::")) / "" * Cc("(ll.at and ll.at['") * R("az","AZ","--","__")^1 * Cc("'])")
+local lp_fastpos_p = ((P("+")^0 * R("09")^1 * P(-1)) / function(s) return "l==" .. s end)
+local lp_fastpos_n = ((P("-")   * R("09")^1 * P(-1)) / function(s) return "(" .. s .. "<0 and (#list+".. s .. "==l))" end)
+local lp_fastpos   = lp_fastpos_n + lp_fastpos_p
+local lp_reserved  = C("and") + C("or") + C("not") + C("div") + C("mod") + C("true") + C("false")
+
+local lp_lua_function  = C(R("az","AZ","__")^1 * (P(".") * R("az","AZ","__")^1)^1) * ("(") / function(t) -- todo: better . handling
+    return t .. "("
+end
+
+local lp_function  = C(R("az","AZ","__")^1) * P("(") / function(t) -- todo: better . handling
+    if expressions[t] then
+        return "expr." .. t .. "("
+    else
+        return "expr.error("
+    end
+end
+
+local lparent  = lpeg.P("(")
+local rparent  = lpeg.P(")")
+local noparent = 1 - (lparent+rparent)
+local nested   = lpeg.P{lparent * (noparent + lpeg.V(1))^0 * rparent}
+local value    = lpeg.P(lparent * lpeg.C((noparent + nested)^0) * rparent) -- lpeg.P{"("*C(((1-S("()"))+V(1))^0)*")"}
+
+local lp_child   = Cc("expr.child(ll,'") * R("az","AZ","--","__")^1 * Cc("')")
+local lp_number  = S("+-") * R("09")^1
+local lp_string  = Cc("'") * R("az","AZ","--","__")^1 * Cc("'")
+local lp_content = (P("'") * (1-P("'"))^0 * P("'") + P('"') * (1-P('"'))^0 * P('"'))
+
+local cleaner
+
+local lp_special = (C(P("name")+P("text")+P("tag")+P("count")+P("child"))) * value / function(t,s)
+    if expressions[t] then
+        s = s and s ~= "" and lpegmatch(cleaner,s)
+        if s and s ~= "" then
+            return "expr." .. t .. "(ll," .. s ..")"
+        else
+            return "expr." .. t .. "(ll)"
+        end
+    else
+        return "expr.error(" .. t .. ")"
+    end
+end
+
+local content =
+    lp_builtin +
+    lp_attribute +
+    lp_special +
+    lp_noequal + lp_doequal +
+    lp_or + lp_and +
+    lp_reserved +
+    lp_lua_function + lp_function +
+    lp_content + -- too fragile
+    lp_child +
+    lp_any
+
+local converter = Cs (
+    lp_fastpos + (P { lparent * (V(1))^0 * rparent + content } )^0
+)
+
+cleaner = Cs ( (
+--~     lp_fastpos +
+    lp_reserved +
+    lp_number +
+    lp_string +
+1 )^1 )
+
+
+--~ expr
+
+local template_e = [[
+    local expr = xml.expressions
+    return function(list,ll,l,order)
+        return %s
+    end
+]]
+
+local template_f_y = [[
+    local finalizer = xml.finalizers['%s']['%s']
+    return function(collection)
+        return finalizer(collection,%s)
+    end
+]]
+
+local template_f_n = [[
+    return xml.finalizers['%s']['%s']
+]]
+
+--
+
+local register_self                    = { kind = "axis", axis = "self"                    } -- , apply = apply_axis["self"]               }
+local register_parent                  = { kind = "axis", axis = "parent"                  } -- , apply = apply_axis["parent"]             }
+local register_descendant              = { kind = "axis", axis = "descendant"              } -- , apply = apply_axis["descendant"]         }
+local register_child                   = { kind = "axis", axis = "child"                   } -- , apply = apply_axis["child"]              }
+local register_descendant_or_self      = { kind = "axis", axis = "descendant-or-self"      } -- , apply = apply_axis["descendant-or-self"] }
+local register_root                    = { kind = "axis", axis = "root"                    } -- , apply = apply_axis["root"]               }
+local register_ancestor                = { kind = "axis", axis = "ancestor"                } -- , apply = apply_axis["ancestor"]           }
+local register_ancestor_or_self        = { kind = "axis", axis = "ancestor-or-self"        } -- , apply = apply_axis["ancestor-or-self"]   }
+local register_attribute               = { kind = "axis", axis = "attribute"               } -- , apply = apply_axis["attribute"]          }
+local register_namespace               = { kind = "axis", axis = "namespace"               } -- , apply = apply_axis["namespace"]          }
+local register_following               = { kind = "axis", axis = "following"               } -- , apply = apply_axis["following"]          }
+local register_following_sibling       = { kind = "axis", axis = "following-sibling"       } -- , apply = apply_axis["following-sibling"]  }
+local register_preceding               = { kind = "axis", axis = "preceding"               } -- , apply = apply_axis["preceding"]          }
+local register_preceding_sibling       = { kind = "axis", axis = "preceding-sibling"       } -- , apply = apply_axis["preceding-sibling"]  }
+local register_reverse_sibling         = { kind = "axis", axis = "reverse-sibling"         } -- , apply = apply_axis["reverse-sibling"]    }
+
+local register_auto_descendant_or_self = { kind = "axis", axis = "auto-descendant-or-self" } -- , apply = apply_axis["auto-descendant-or-self"] }
+local register_auto_descendant         = { kind = "axis", axis = "auto-descendant"         } -- , apply = apply_axis["auto-descendant"] }
+local register_auto_self               = { kind = "axis", axis = "auto-self"               } -- , apply = apply_axis["auto-self"] }
+local register_auto_child              = { kind = "axis", axis = "auto-child"              } -- , apply = apply_axis["auto-child"] }
+
+local register_initial_child           = { kind = "axis", axis = "initial-child"           } -- , apply = apply_axis["initial-child"] }
+
+local register_all_nodes               = { kind = "nodes", nodetest = true, nodes = { true, false, false } }
+
+local skip = { }
+
+local function errorrunner_e(str,cnv)
+    if not skip[str] then
+        logs.report("lpath","error in expression: %s => %s",str,cnv)
+        skip[str] = cnv or str
+    end
+    return false
+end
+local function errorrunner_f(str,arg)
+    logs.report("lpath","error in finalizer: %s(%s)",str,arg or "")
+    return false
+end
+
+local function register_nodes(nodetest,nodes)
+    return { kind = "nodes", nodetest = nodetest, nodes = nodes }
+end
+
+local function register_expression(expression)
+    local converted = lpegmatch(converter,expression)
+    local runner = loadstring(format(template_e,converted))
+    runner = (runner and runner()) or function() errorrunner_e(expression,converted) end
+    return { kind = "expression", expression = expression, converted = converted, evaluator = runner }
+end
+
+local function register_finalizer(protocol,name,arguments)
+    local runner
+    if arguments and arguments ~= "" then
+        runner = loadstring(format(template_f_y,protocol or xml.defaultprotocol,name,arguments))
+    else
+        runner = loadstring(format(template_f_n,protocol or xml.defaultprotocol,name))
+    end
+    runner = (runner and runner()) or function() errorrunner_f(name,arguments) end
+    return { kind = "finalizer", name = name, arguments = arguments, finalizer = runner }
+end
+
+local expression = P { "ex",
+    ex = "[" * C((V("sq") + V("dq") + (1 - S("[]")) + V("ex"))^0) * "]",
+    sq = "'" * (1 - S("'"))^0 * "'",
+    dq = '"' * (1 - S('"'))^0 * '"',
+}
+
+local arguments = P { "ar",
+    ar = "(" * Cs((V("sq") + V("dq") + V("nq") + P(1-P(")")))^0) * ")",
+    nq = ((1 - S("),'\""))^1) / function(s) return format("%q",s) end,
+    sq = P("'") * (1 - P("'"))^0 * P("'"),
+    dq = P('"') * (1 - P('"'))^0 * P('"'),
+}
+
+-- todo: better arg parser
+
+local function register_error(str)
+    return { kind = "error", error = format("unparsed: %s",str) }
+end
+
+-- there is a difference in * and /*/ and so we need to catch a few special cases
+
+local special_1 = P("*")  * Cc(register_auto_descendant) * Cc(register_all_nodes) -- last one not needed
+local special_2 = P("/")  * Cc(register_auto_self)
+local special_3 = P("")   * Cc(register_auto_self)
+
+local parser = Ct { "patterns", -- can be made a bit faster by moving pattern outside
+
+    patterns             = spaces * V("protocol") * spaces * (
+                              ( V("special") * spaces * P(-1)                                                         ) +
+                              ( V("initial") * spaces * V("step") * spaces * (P("/") * spaces * V("step") * spaces)^0 )
+                           ),
+
+    protocol             = Cg(V("letters"),"protocol") * P("://") + Cg(Cc(nil),"protocol"),
+
+ -- the / is needed for // as descendant or self is somewhat special
+ -- step                 = (V("shortcuts") + V("axis") * spaces * V("nodes")^0 + V("error")) * spaces * V("expressions")^0 * spaces * V("finalizer")^0,
+    step                 = ((V("shortcuts") + P("/") + V("axis")) * spaces * V("nodes")^0 + V("error")) * spaces * V("expressions")^0 * spaces * V("finalizer")^0,
+
+    axis                 = V("descendant") + V("child") + V("parent") + V("self") + V("root") + V("ancestor") +
+                           V("descendant_or_self") + V("following_sibling") + V("following") +
+                           V("reverse_sibling") + V("preceding_sibling") + V("preceding") + V("ancestor_or_self") +
+                           #(1-P(-1)) * Cc(register_auto_child),
+
+    special              = special_1 + special_2 + special_3,
+
+    initial              = (P("/") * spaces * Cc(register_initial_child))^-1,
+
+    error                = (P(1)^1) / register_error,
+
+    shortcuts_a          = V("s_descendant_or_self") + V("s_descendant") + V("s_child") + V("s_parent") + V("s_self") + V("s_root") + V("s_ancestor"),
+
+    shortcuts            = V("shortcuts_a") * (spaces * "/" * spaces * V("shortcuts_a"))^0,
+
+    s_descendant_or_self = (P("***/") + P("/"))  * Cc(register_descendant_or_self), --- *** is a bonus
+ -- s_descendant_or_self = P("/")                * Cc(register_descendant_or_self),
+    s_descendant         = P("**")               * Cc(register_descendant),
+    s_child              = P("*") * #(1-P(":"))  * Cc(register_child     ),
+--  s_child              = P("*") * #(P("/")+P(-1)) * Cc(register_child     ),
+    s_parent             = P("..")               * Cc(register_parent    ),
+    s_self               = P("." )               * Cc(register_self      ),
+    s_root               = P("^^")               * Cc(register_root      ),
+    s_ancestor           = P("^")                * Cc(register_ancestor  ),
+
+    descendant           = P("descendant::")         * Cc(register_descendant         ),
+    child                = P("child::")              * Cc(register_child              ),
+    parent               = P("parent::")             * Cc(register_parent             ),
+    self                 = P("self::")               * Cc(register_self               ),
+    root                 = P('root::')               * Cc(register_root               ),
+    ancestor             = P('ancestor::')           * Cc(register_ancestor           ),
+    descendant_or_self   = P('descendant-or-self::') * Cc(register_descendant_or_self ),
+    ancestor_or_self     = P('ancestor-or-self::')   * Cc(register_ancestor_or_self   ),
+ -- attribute            = P('attribute::')          * Cc(register_attribute          ),
+ -- namespace            = P('namespace::')          * Cc(register_namespace          ),
+    following            = P('following::')          * Cc(register_following          ),
+    following_sibling    = P('following-sibling::')  * Cc(register_following_sibling  ),
+    preceding            = P('preceding::')          * Cc(register_preceding          ),
+    preceding_sibling    = P('preceding-sibling::')  * Cc(register_preceding_sibling  ),
+    reverse_sibling      = P('reverse-sibling::')    * Cc(register_reverse_sibling    ),
+
+    nodes                = (V("nodefunction") * spaces * P("(") * V("nodeset") * P(")") + V("nodetest") * V("nodeset")) / register_nodes,
+
+    expressions          = expression / register_expression,
+
+    letters              = R("az")^1,
+    name                 = (1-lpeg.S("/[]()|:*!"))^1,
+    negate               = P("!") * Cc(false),
+
+    nodefunction         = V("negate") + P("not") * Cc(false) + Cc(true),
+    nodetest             = V("negate") + Cc(true),
+    nodename             = (V("negate") + Cc(true)) * spaces * ((V("wildnodename") * P(":") * V("wildnodename")) + (Cc(false) * V("wildnodename"))),
+    wildnodename         = (C(V("name")) + P("*") * Cc(false)) * #(1-P("(")),
+    nodeset              = spaces * Ct(V("nodename") * (spaces * P("|") * spaces * V("nodename"))^0) * spaces,
+
+    finalizer            = (Cb("protocol") * P("/")^-1 * C(V("name")) * arguments * P(-1)) / register_finalizer,
+
+}
+
+local cache = { }
+
+local function nodesettostring(set,nodetest)
+    local t = { }
+    for i=1,#set,3 do
+        local directive, ns, tg = set[i], set[i+1], set[i+2]
+        if not ns or ns == "" then ns = "*" end
+        if not tg or tg == "" then tg = "*" end
+        tg = (tg == "@rt@" and "[root]") or format("%s:%s",ns,tg)
+        t[#t+1] = (directive and tg) or format("not(%s)",tg)
+    end
+    if nodetest == false then
+        return format("not(%s)",concat(t,"|"))
+    else
+        return concat(t,"|")
+    end
+end
+
+local function tagstostring(list)
+    if #list == 0 then
+        return "no elements"
+    else
+        local t = { }
+        for i=1, #list do
+            local li = list[i]
+            local ns, tg = li.ns, li.tg
+            if not ns or ns == "" then ns = "*" end
+            if not tg or tg == "" then tg = "*" end
+            t[#t+1] = (tg == "@rt@" and "[root]") or format("%s:%s",ns,tg)
+        end
+        return concat(t," ")
+    end
+end
+
+xml.nodesettostring = nodesettostring
+
+local parse_pattern -- we have a harmless kind of circular reference
+
+local function lshow(parsed)
+    if type(parsed) == "string" then
+        parsed = parse_pattern(parsed)
+    end
+    local s = table.serialize_functions -- ugly
+    table.serialize_functions = false -- ugly
+    logs.report("lpath","%s://%s => %s",parsed.protocol or xml.defaultprotocol,parsed.pattern,table.serialize(parsed,false))
+    table.serialize_functions = s -- ugly
+end
+
+xml.lshow = lshow
+
+local function add_comment(p,str)
+    local pc = p.comment
+    if not pc then
+        p.comment = { str }
+    else
+        pc[#pc+1] = str
+    end
+end
+
+parse_pattern = function (pattern) -- the gain of caching is rather minimal
+    lpathcalls = lpathcalls + 1
+    if type(pattern) == "table" then
+        return pattern
+    else
+        local parsed = cache[pattern]
+        if parsed then
+            lpathcached = lpathcached + 1
+        else
+            parsed = lpegmatch(parser,pattern)
+            if parsed then
+                parsed.pattern = pattern
+                local np = #parsed
+                if np == 0 then
+                    parsed = { pattern = pattern, register_self, state = "parsing error" }
+                    logs.report("lpath","parsing error in '%s'",pattern)
+                    lshow(parsed)
+                else
+                    -- we could have done this with a more complex parser but this
+                    -- is cleaner
+                    local pi = parsed[1]
+                    if pi.axis == "auto-child" then
+                        if false then
+                            add_comment(parsed, "auto-child replaced by auto-descendant-or-self")
+                            parsed[1] = register_auto_descendant_or_self
+                        else
+                            add_comment(parsed, "auto-child replaced by auto-descendant")
+                            parsed[1] = register_auto_descendant
+                        end
+                    elseif pi.axis == "initial-child" and np > 1 and parsed[2].axis then
+                        add_comment(parsed, "initial-child removed") -- we could also make it a auto-self
+                        remove(parsed,1)
+                    end
+                    local np = #parsed -- can have changed
+                    if np > 1 then
+                        local pnp = parsed[np]
+                        if pnp.kind == "nodes" and pnp.nodetest == true then
+                            local nodes = pnp.nodes
+                            if nodes[1] == true and nodes[2] == false and nodes[3] == false then
+                                add_comment(parsed, "redundant final wildcard filter removed")
+                                remove(parsed,np)
+                            end
+                        end
+                    end
+                end
+            else
+                parsed = { pattern = pattern }
+            end
+            cache[pattern] = parsed
+            if trace_lparse and not trace_lprofile then
+                lshow(parsed)
+            end
+        end
+        return parsed
+    end
+end
+
+-- we can move all calls inline and then merge the trace back
+-- technically we can combine axis and the next nodes which is
+-- what we did before but this a bit cleaner (but slower too)
+-- but interesting is that it's not that much faster when we
+-- go inline
+--
+-- beware: we need to return a collection even when we filter
+-- else the (simple) cache gets messed up
+
+-- caching found lookups saves not that much (max .1 sec on a 8 sec run)
+-- and it also messes up finalizers
+
+-- watch out: when there is a finalizer, it's always called as there
+-- can be cases that a finalizer returns (or does) something in case
+-- there is no match; an example of this is count()
+
+local profiled = { }  xml.profiled = profiled
+
+local function profiled_apply(list,parsed,nofparsed,order)
+    local p = profiled[parsed.pattern]
+    if p then
+        p.tested = p.tested + 1
+    else
+        p = { tested = 1, matched = 0, finalized = 0 }
+        profiled[parsed.pattern] = p
+    end
+    local collected = list
+    for i=1,nofparsed do
+        local pi = parsed[i]
+        local kind = pi.kind
+        if kind == "axis" then
+            collected = apply_axis[pi.axis](collected)
+        elseif kind == "nodes" then
+            collected = apply_nodes(collected,pi.nodetest,pi.nodes)
+        elseif kind == "expression" then
+            collected = apply_expression(collected,pi.evaluator,order)
+        elseif kind == "finalizer" then
+            collected = pi.finalizer(collected)
+            p.matched = p.matched + 1
+            p.finalized = p.finalized + 1
+            return collected
+        end
+        if not collected or #collected == 0 then
+            local pn = i < nofparsed and parsed[nofparsed]
+            if pn and pn.kind == "finalizer" then
+                collected = pn.finalizer(collected)
+                p.finalized = p.finalized + 1
+                return collected
+            end
+            return nil
+        end
+    end
+    if collected then
+        p.matched = p.matched + 1
+    end
+    return collected
+end
+
+local function traced_apply(list,parsed,nofparsed,order)
+    if trace_lparse then
+        lshow(parsed)
+    end
+    logs.report("lpath", "collecting : %s",parsed.pattern)
+    logs.report("lpath", " root tags : %s",tagstostring(list))
+    logs.report("lpath", "     order : %s",order or "unset")
+    local collected = list
+    for i=1,nofparsed do
+        local pi = parsed[i]
+        local kind = pi.kind
+        if kind == "axis" then
+            collected = apply_axis[pi.axis](collected)
+            logs.report("lpath", "% 10i : ax : %s",(collected and #collected) or 0,pi.axis)
+        elseif kind == "nodes" then
+            collected = apply_nodes(collected,pi.nodetest,pi.nodes)
+            logs.report("lpath", "% 10i : ns : %s",(collected and #collected) or 0,nodesettostring(pi.nodes,pi.nodetest))
+        elseif kind == "expression" then
+            collected = apply_expression(collected,pi.evaluator,order)
+            logs.report("lpath", "% 10i : ex : %s -> %s",(collected and #collected) or 0,pi.expression,pi.converted)
+        elseif kind == "finalizer" then
+            collected = pi.finalizer(collected)
+            logs.report("lpath", "% 10i : fi : %s : %s(%s)",(type(collected) == "table" and #collected) or 0,parsed.protocol or xml.defaultprotocol,pi.name,pi.arguments or "")
+            return collected
+        end
+        if not collected or #collected == 0 then
+            local pn = i < nofparsed and parsed[nofparsed]
+            if pn and pn.kind == "finalizer" then
+                collected = pn.finalizer(collected)
+                logs.report("lpath", "% 10i : fi : %s : %s(%s)",(type(collected) == "table" and #collected) or 0,parsed.protocol or xml.defaultprotocol,pn.name,pn.arguments or "")
+                return collected
+            end
+            return nil
+        end
+    end
+    return collected
+end
+
+local function normal_apply(list,parsed,nofparsed,order)
+    local collected = list
+    for i=1,nofparsed do
+        local pi = parsed[i]
+        local kind = pi.kind
+        if kind == "axis" then
+            local axis = pi.axis
+            if axis ~= "self" then
+                collected = apply_axis[axis](collected)
+            end
+        elseif kind == "nodes" then
+            collected = apply_nodes(collected,pi.nodetest,pi.nodes)
+        elseif kind == "expression" then
+            collected = apply_expression(collected,pi.evaluator,order)
+        elseif kind == "finalizer" then
+            return pi.finalizer(collected)
+        end
+        if not collected or #collected == 0 then
+            local pf = i < nofparsed and parsed[nofparsed].finalizer
+            if pf then
+                return pf(collected) -- can be anything
+            end
+            return nil
+        end
+    end
+    return collected
+end
+
+local function parse_apply(list,pattern)
+    -- we avoid an extra call
+    local parsed = cache[pattern]
+    if parsed then
+        lpathcalls = lpathcalls + 1
+        lpathcached = lpathcached + 1
+    elseif type(pattern) == "table" then
+        lpathcalls = lpathcalls + 1
+        parsed = pattern
+    else
+        parsed = parse_pattern(pattern) or pattern
+    end
+    if not parsed then
+        return
+    end
+    local nofparsed = #parsed
+    if nofparsed == 0 then
+        return -- something is wrong
+    end
+    local one = list[1]
+    if not one then
+        return -- something is wrong
+    elseif not trace_lpath then
+        return normal_apply(list,parsed,nofparsed,one.mi)
+    elseif trace_lprofile then
+        return profiled_apply(list,parsed,nofparsed,one.mi)
+    else
+        return traced_apply(list,parsed,nofparsed,one.mi)
+    end
+end
+
+-- internal (parsed)
+
+expressions.child = function(e,pattern)
+    return parse_apply({ e },pattern) -- todo: cache
+end
+expressions.count = function(e,pattern)
+    local collected = parse_apply({ e },pattern) -- todo: cache
+    return (collected and #collected) or 0
+end
+
+-- external
+
+expressions.oneof = function(s,...) -- slow
+    local t = {...} for i=1,#t do if s == t[i] then return true end end return false
+end
+expressions.error = function(str)
+    xml.error_handler("unknown function in lpath expression",tostring(str or "?"))
+    return false
+end
+expressions.undefined = function(s)
+    return s == nil
+end
+
+expressions.quit = function(s)
+    if s or s == nil then
+        quit_expression = true
+    end
+    return true
+end
+
+expressions.print = function(...)
+    print(...)
+    return true
+end
+
+expressions.contains  = find
+expressions.find      = find
+expressions.upper     = upper
+expressions.lower     = lower
+expressions.number    = tonumber
+expressions.boolean   = toboolean
+
+-- user interface
+
+local function traverse(root,pattern,handle)
+    logs.report("xml","use 'xml.selection' instead for '%s'",pattern)
+    local collected = parse_apply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            local r = e.__p__
+            handle(r,r.dt,e.ni)
+        end
+    end
+end
+
+local function selection(root,pattern,handle)
+    local collected = parse_apply({ root },pattern)
+    if collected then
+        if handle then
+            for c=1,#collected do
+                handle(collected[c])
+            end
+        else
+            return collected
+        end
+    end
+end
+
+xml.parse_parser  = parser
+xml.parse_pattern = parse_pattern
+xml.parse_apply   = parse_apply
+xml.traverse      = traverse           -- old method, r, d, k
+xml.selection     = selection          -- new method, simple handle
+
+local lpath = parse_pattern
+
+xml.lpath = lpath
+
+function xml.cached_patterns()
+    return cache
+end
+
+-- generic function finalizer (independant namespace)
+
+local function dofunction(collected,fnc)
+    if collected then
+        local f = functions[fnc]
+        if f then
+            for c=1,#collected do
+                f(collected[c])
+            end
+        else
+            logs.report("xml","unknown function '%s'",fnc)
+        end
+    end
+end
+
+xml.finalizers.xml["function"] = dofunction
+xml.finalizers.tex["function"] = dofunction
+
+-- functions
+
+expressions.text = function(e,n)
+    local rdt = e.__p__.dt
+    return (rdt and rdt[n]) or ""
+end
+
+expressions.name = function(e,n) -- ns + tg
+    local found = false
+    n = tonumber(n) or 0
+    if n == 0 then
+        found = type(e) == "table" and e
+    elseif n < 0 then
+        local d, k = e.__p__.dt, e.ni
+        for i=k-1,1,-1 do
+            local di = d[i]
+            if type(di) == "table" then
+                if n == -1 then
+                    found = di
+                    break
+                else
+                    n = n + 1
+                end
+            end
+        end
+    else
+        local d, k = e.__p__.dt, e.ni
+        for i=k+1,#d,1 do
+            local di = d[i]
+            if type(di) == "table" then
+                if n == 1 then
+                    found = di
+                    break
+                else
+                    n = n - 1
+                end
+            end
+        end
+    end
+    if found then
+        local ns, tg = found.rn or found.ns or "", found.tg
+        if ns ~= "" then
+            return ns .. ":" .. tg
+        else
+            return tg
+        end
+    else
+        return ""
+    end
+end
+
+expressions.tag = function(e,n) -- only tg
+    if not e then
+        return ""
+    else
+        local found = false
+        n = tonumber(n) or 0
+        if n == 0 then
+            found = (type(e) == "table") and e -- seems to fail
+        elseif n < 0 then
+            local d, k = e.__p__.dt, e.ni
+            for i=k-1,1,-1 do
+                local di = d[i]
+                if type(di) == "table" then
+                    if n == -1 then
+                        found = di
+                        break
+                    else
+                        n = n + 1
+                    end
+                end
+            end
+        else
+            local d, k = e.__p__.dt, e.ni
+            for i=k+1,#d,1 do
+                local di = d[i]
+                if type(di) == "table" then
+                    if n == 1 then
+                        found = di
+                        break
+                    else
+                        n = n - 1
+                    end
+                end
+            end
+        end
+        return (found and found.tg) or ""
+    end
+end
+
+--[[ldx--
+<p>This is the main filter function. It returns whatever is asked for.</p>
+--ldx]]--
+
+function xml.filter(root,pattern) -- no longer funny attribute handling here
+    return parse_apply({ root },pattern)
+end
+
+--[[ldx--
+<p>Often using an iterators looks nicer in the code than passing handler
+functions. The <l n='lua'/> book describes how to use coroutines for that
+purpose (<url href='http://www.lua.org/pil/9.3.html'/>). This permits
+code like:</p>
+
+<typing>
+for r, d, k in xml.elements(xml.load('text.xml'),"title") do
+    print(d[k]) -- old method
+end
+for e in xml.collected(xml.load('text.xml'),"title") do
+    print(e) -- new one
+end
+</typing>
+--ldx]]--
+
+local wrap, yield = coroutine.wrap, coroutine.yield
+
+function xml.elements(root,pattern,reverse) -- r, d, k
+    local collected = parse_apply({ root },pattern)
+    if collected then
+        if reverse then
+            return wrap(function() for c=#collected,1,-1 do
+                local e = collected[c] local r = e.__p__ yield(r,r.dt,e.ni)
+            end end)
+        else
+            return wrap(function() for c=1,#collected    do
+                local e = collected[c] local r = e.__p__ yield(r,r.dt,e.ni)
+            end end)
+        end
+    end
+    return wrap(function() end)
+end
+
+function xml.collected(root,pattern,reverse) -- e
+    local collected = parse_apply({ root },pattern)
+    if collected then
+        if reverse then
+            return wrap(function() for c=#collected,1,-1 do yield(collected[c]) end end)
+        else
+            return wrap(function() for c=1,#collected    do yield(collected[c]) end end)
+        end
+    end
+    return wrap(function() end)
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['lxml-mis'] = {
+    version   = 1.001,
+    comment   = "this module is the basis for the lxml-* ones",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local concat = table.concat
+local type, next, tonumber, tostring, setmetatable, loadstring = type, next, tonumber, tostring, setmetatable, loadstring
+local format, gsub, match = string.format, string.gsub, string.match
+local lpegmatch = lpeg.match
+
+--[[ldx--
+<p>The following helper functions best belong to the <t>lxml-ini</t>
+module. Some are here because we need then in the <t>mk</t>
+document and other manuals, others came up when playing with
+this module. Since this module is also used in <l n='mtxrun'/> we've
+put them here instead of loading mode modules there then needed.</p>
+--ldx]]--
+
+local function xmlgsub(t,old,new) -- will be replaced
+    local dt = t.dt
+    if dt then
+        for k=1,#dt do
+            local v = dt[k]
+            if type(v) == "string" then
+                dt[k] = gsub(v,old,new)
+            else
+                xmlgsub(v,old,new)
+            end
+        end
+    end
+end
+
+--~ xml.gsub = xmlgsub
+
+function xml.strip_leading_spaces(dk,d,k) -- cosmetic, for manual
+    if d and k then
+        local dkm = d[k-1]
+        if dkm and type(dkm) == "string" then
+            local s = match(dkm,"\n(%s+)")
+            xmlgsub(dk,"\n"..rep(" ",#s),"\n")
+        end
+    end
+end
+
+--~ xml.escapes   = { ['&'] = '&amp;', ['<'] = '&lt;', ['>'] = '&gt;', ['"'] = '&quot;' }
+--~ xml.unescapes = { } for k,v in next, xml.escapes do xml.unescapes[v] = k end
+
+--~ function xml.escaped  (str) return (gsub(str,"(.)"   , xml.escapes  )) end
+--~ function xml.unescaped(str) return (gsub(str,"(&.-;)", xml.unescapes)) end
+--~ function xml.cleansed (str) return (gsub(str,"<.->"  , ''           )) end -- "%b<>"
+
+local P, S, R, C, V, Cc, Cs = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.V, lpeg.Cc, lpeg.Cs
+
+-- 100 * 2500 * "oeps< oeps> oeps&" : gsub:lpeg|lpeg|lpeg
+--
+-- 1021:0335:0287:0247
+
+-- 10 * 1000 * "oeps< oeps> oeps& asfjhalskfjh alskfjh alskfjh alskfjh ;al J;LSFDJ"
+--
+-- 1559:0257:0288:0190 (last one suggested by roberto)
+
+--    escaped = Cs((S("<&>") / xml.escapes + 1)^0)
+--    escaped = Cs((S("<")/"&lt;" + S(">")/"&gt;" + S("&")/"&amp;" + 1)^0)
+local normal  = (1 - S("<&>"))^0
+local special = P("<")/"&lt;" + P(">")/"&gt;" + P("&")/"&amp;"
+local escaped = Cs(normal * (special * normal)^0)
+
+-- 100 * 1000 * "oeps&lt; oeps&gt; oeps&amp;" : gsub:lpeg == 0153:0280:0151:0080 (last one by roberto)
+
+local normal    = (1 - S"&")^0
+local special   = P("&lt;")/"<" + P("&gt;")/">" + P("&amp;")/"&"
+local unescaped = Cs(normal * (special * normal)^0)
+
+-- 100 * 5000 * "oeps <oeps bla='oeps' foo='bar'> oeps </oeps> oeps " : gsub:lpeg == 623:501 msec (short tags, less difference)
+
+local cleansed = Cs(((P("<") * (1-P(">"))^0 * P(">"))/"" + 1)^0)
+
+xml.escaped_pattern   = escaped
+xml.unescaped_pattern = unescaped
+xml.cleansed_pattern  = cleansed
+
+function xml.escaped  (str) return lpegmatch(escaped,str)   end
+function xml.unescaped(str) return lpegmatch(unescaped,str) end
+function xml.cleansed (str) return lpegmatch(cleansed,str)  end
+
+-- this might move
+
+function xml.fillin(root,pattern,str,check)
+    local e = xml.first(root,pattern)
+    if e then
+        local n = #e.dt
+        if not check or n == 0 or (n == 1 and e.dt[1] == "") then
+            e.dt = { str }
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['lxml-aux'] = {
+    version   = 1.001,
+    comment   = "this module is the basis for the lxml-* ones",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- not all functions here make sense anymore vbut we keep them for
+-- compatibility reasons
+
+local trace_manipulations = false  trackers.register("lxml.manipulations", function(v) trace_manipulations = v end)
+
+local xmlparseapply, xmlconvert, xmlcopy, xmlname = xml.parse_apply, xml.convert, xml.copy, xml.name
+local xmlinheritedconvert = xml.inheritedconvert
+
+local type = type
+local insert, remove = table.insert, table.remove
+local gmatch, gsub = string.gmatch, string.gsub
+
+local function report(what,pattern,c,e)
+    logs.report("xml","%s element '%s' (root: '%s', position: %s, index: %s, pattern: %s)",what,xmlname(e),xmlname(e.__p__),c,e.ni,pattern)
+end
+
+local function withelements(e,handle,depth)
+    if e and handle then
+        local edt = e.dt
+        if edt then
+            depth = depth or 0
+            for i=1,#edt do
+                local e = edt[i]
+                if type(e) == "table" then
+                    handle(e,depth)
+                    withelements(e,handle,depth+1)
+                end
+            end
+        end
+    end
+end
+
+xml.withelements = withelements
+
+function xml.withelement(e,n,handle) -- slow
+    if e and n ~= 0 and handle then
+        local edt = e.dt
+        if edt then
+            if n > 0 then
+                for i=1,#edt do
+                    local ei = edt[i]
+                    if type(ei) == "table" then
+                        if n == 1 then
+                            handle(ei)
+                            return
+                        else
+                            n = n - 1
+                        end
+                    end
+                end
+            elseif n < 0 then
+                for i=#edt,1,-1 do
+                    local ei = edt[i]
+                    if type(ei) == "table" then
+                        if n == -1 then
+                            handle(ei)
+                            return
+                        else
+                            n = n + 1
+                        end
+                    end
+                end
+            end
+        end
+    end
+end
+
+xml.elements_only = xml.collected
+
+function xml.each_element(root,pattern,handle,reverse)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        if reverse then
+            for c=#collected,1,-1 do
+                handle(collected[c])
+            end
+        else
+            for c=1,#collected do
+                handle(collected[c])
+            end
+        end
+        return collected
+    end
+end
+
+xml.process_elements = xml.each_element
+
+function xml.process_attributes(root,pattern,handle)
+    local collected = xmlparseapply({ root },pattern)
+    if collected and handle then
+        for c=1,#collected do
+            handle(collected[c].at)
+        end
+    end
+    return collected
+end
+
+--[[ldx--
+<p>The following functions collect elements and texts.</p>
+--ldx]]--
+
+-- are these still needed -> lxml-cmp.lua
+
+function xml.collect_elements(root, pattern)
+    return xmlparseapply({ root },pattern)
+end
+
+function xml.collect_texts(root, pattern, flatten) -- todo: variant with handle
+    local collected = xmlparseapply({ root },pattern)
+    if collected and flatten then
+        local xmltostring = xml.tostring
+        for c=1,#collected do
+            collected[c] = xmltostring(collected[c].dt)
+        end
+    end
+    return collected or { }
+end
+
+function xml.collect_tags(root, pattern, nonamespace)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        local t = { }
+        for c=1,#collected do
+            local e = collected[c]
+            local ns, tg = e.ns, e.tg
+            if nonamespace then
+                t[#t+1] = tg
+            elseif ns == "" then
+                t[#t+1] = tg
+            else
+                t[#t+1] = ns .. ":" .. tg
+            end
+        end
+        return t
+    end
+end
+
+--[[ldx--
+<p>We've now arrived at the functions that manipulate the tree.</p>
+--ldx]]--
+
+local no_root = { no_root = true }
+
+function xml.redo_ni(d)
+    for k=1,#d do
+        local dk = d[k]
+        if type(dk) == "table" then
+            dk.ni = k
+        end
+    end
+end
+
+local function xmltoelement(whatever,root)
+    if not whatever then
+        return nil
+    end
+    local element
+    if type(whatever) == "string" then
+        element = xmlinheritedconvert(whatever,root)
+    else
+        element = whatever -- we assume a table
+    end
+    if element.error then
+        return whatever -- string
+    end
+    if element then
+    --~ if element.ri then
+    --~     element = element.dt[element.ri].dt
+    --~ else
+    --~     element = element.dt
+    --~ end
+    end
+    return element
+end
+
+xml.toelement = xmltoelement
+
+local function copiedelement(element,newparent)
+    if type(element) == "string" then
+        return element
+    else
+        element = xmlcopy(element).dt
+        if newparent and type(element) == "table" then
+            element.__p__ = newparent
+        end
+        return element
+    end
+end
+
+function xml.delete_element(root,pattern)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            local p = e.__p__
+            if p then
+                if trace_manipulations then
+                    report('deleting',pattern,c,e)
+                end
+                local d = p.dt
+                remove(d,e.ni)
+                xml.redo_ni(d) -- can be made faster and inlined
+            end
+        end
+    end
+end
+
+function xml.replace_element(root,pattern,whatever)
+    local element = root and xmltoelement(whatever,root)
+    local collected = element and xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            local p = e.__p__
+            if p then
+                if trace_manipulations then
+                    report('replacing',pattern,c,e)
+                end
+                local d = p.dt
+                d[e.ni] = copiedelement(element,p)
+                xml.redo_ni(d) -- probably not needed
+            end
+        end
+    end
+end
+
+local function inject_element(root,pattern,whatever,prepend)
+    local element = root and xmltoelement(whatever,root)
+    local collected = element and xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            local r = e.__p__
+            local d, k, rri = r.dt, e.ni, r.ri
+            local edt = (rri and d[rri].dt) or (d and d[k] and d[k].dt)
+            if edt then
+                local be, af
+                local cp = copiedelement(element,e)
+                if prepend then
+                    be, af = cp, edt
+                else
+                    be, af = edt, cp
+                end
+                for i=1,#af do
+                    be[#be+1] = af[i]
+                end
+                if rri then
+                    r.dt[rri].dt = be
+                else
+                    d[k].dt = be
+                end
+                xml.redo_ni(d)
+            end
+        end
+    end
+end
+
+local function insert_element(root,pattern,whatever,before) -- todo: element als functie
+    local element = root and xmltoelement(whatever,root)
+    local collected = element and xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            local r = e.__p__
+            local d, k = r.dt, e.ni
+            if not before then
+                k = k + 1
+            end
+            insert(d,k,copiedelement(element,r))
+            xml.redo_ni(d)
+        end
+    end
+end
+
+xml.insert_element        =                 insert_element
+xml.insert_element_after  =                 insert_element
+xml.insert_element_before = function(r,p,e) insert_element(r,p,e,true) end
+xml.inject_element        =                 inject_element
+xml.inject_element_after  =                 inject_element
+xml.inject_element_before = function(r,p,e) inject_element(r,p,e,true) end
+
+local function include(xmldata,pattern,attribute,recursive,loaddata)
+    -- parse="text" (default: xml), encoding="" (todo)
+    -- attribute = attribute or 'href'
+    pattern = pattern or 'include'
+    loaddata = loaddata or io.loaddata
+    local collected = xmlparseapply({ xmldata },pattern)
+    if collected then
+        for c=1,#collected do
+            local ek = collected[c]
+            local name = nil
+            local ekdt = ek.dt
+            local ekat = ek.at
+            local epdt = ek.__p__.dt
+            if not attribute or attribute == "" then
+                name = (type(ekdt) == "table" and ekdt[1]) or ekdt -- ckeck, probably always tab or str
+            end
+            if not name then
+                for a in gmatch(attribute or "href","([^|]+)") do
+                    name = ekat[a]
+                    if name then break end
+                end
+            end
+            local data = (name and name ~= "" and loaddata(name)) or ""
+            if data == "" then
+                epdt[ek.ni] = "" -- xml.empty(d,k)
+            elseif ekat["parse"] == "text" then
+                -- for the moment hard coded
+                epdt[ek.ni] = xml.escaped(data) -- d[k] = xml.escaped(data)
+            else
+--~                 local settings = xmldata.settings
+--~                 settings.parent_root = xmldata -- to be tested
+--~                 local xi = xmlconvert(data,settings)
+                local xi = xmlinheritedconvert(data,xmldata)
+                if not xi then
+                    epdt[ek.ni] = "" -- xml.empty(d,k)
+                else
+                    if recursive then
+                        include(xi,pattern,attribute,recursive,loaddata)
+                    end
+                    epdt[ek.ni] = xml.body(xi) -- xml.assign(d,k,xi)
+                end
+            end
+        end
+    end
+end
+
+xml.include = include
+
+--~ local function manipulate(xmldata,pattern,manipulator) -- untested and might go away
+--~     local collected = xmlparseapply({ xmldata },pattern)
+--~     if collected then
+--~         local xmltostring = xml.tostring
+--~         for c=1,#collected do
+--~             local e = collected[c]
+--~             local data = manipulator(xmltostring(e))
+--~             if data == "" then
+--~                 epdt[e.ni] = ""
+--~             else
+--~                 local xi = xmlinheritedconvert(data,xmldata)
+--~                 if not xi then
+--~                     epdt[e.ni] = ""
+--~                 else
+--~                     epdt[e.ni] = xml.body(xi) -- xml.assign(d,k,xi)
+--~                 end
+--~             end
+--~         end
+--~     end
+--~ end
+
+--~ xml.manipulate = manipulate
+
+function xml.strip_whitespace(root, pattern, nolines) -- strips all leading and trailing space !
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for i=1,#collected do
+            local e = collected[i]
+            local edt = e.dt
+            if edt then
+                local t = { }
+                for i=1,#edt do
+                    local str = edt[i]
+                    if type(str) == "string" then
+                        if str == "" then
+                            -- stripped
+                        else
+                            if nolines then
+                                str = gsub(str,"[ \n\r\t]+"," ")
+                            end
+                            if str == "" then
+                                -- stripped
+                            else
+                                t[#t+1] = str
+                            end
+                        end
+                    else
+        --~                         str.ni = i
+                        t[#t+1] = str
+                    end
+                end
+                e.dt = t
+            end
+        end
+    end
+end
+
+function xml.strip_whitespace(root, pattern, nolines, anywhere) -- strips all leading and trailing spacing
+    local collected = xmlparseapply({ root },pattern) -- beware, indices no longer are valid now
+    if collected then
+        for i=1,#collected do
+            local e = collected[i]
+            local edt = e.dt
+            if edt then
+                if anywhere then
+                    local t = { }
+                    for e=1,#edt do
+                        local str = edt[e]
+                        if type(str) ~= "string" then
+                            t[#t+1] = str
+                        elseif str ~= "" then
+                            -- todo: lpeg for each case
+                            if nolines then
+                                str = gsub(str,"%s+"," ")
+                            end
+                            str = gsub(str,"^%s*(.-)%s*$","%1")
+                            if str ~= "" then
+                                t[#t+1] = str
+                            end
+                        end
+                    end
+                    e.dt = t
+                else
+                    -- we can assume a regular sparse xml table with no successive strings
+                    -- otherwise we should use a while loop
+                    if #edt > 0 then
+                        -- strip front
+                        local str = edt[1]
+                        if type(str) ~= "string" then
+                            -- nothing
+                        elseif str == "" then
+                            remove(edt,1)
+                        else
+                            if nolines then
+                                str = gsub(str,"%s+"," ")
+                            end
+                            str = gsub(str,"^%s+","")
+                            if str == "" then
+                                remove(edt,1)
+                            else
+                                edt[1] = str
+                            end
+                        end
+                    end
+                    if #edt > 1 then
+                        -- strip end
+                        local str = edt[#edt]
+                        if type(str) ~= "string" then
+                            -- nothing
+                        elseif str == "" then
+                            remove(edt)
+                        else
+                            if nolines then
+                                str = gsub(str,"%s+"," ")
+                            end
+                            str = gsub(str,"%s+$","")
+                            if str == "" then
+                                remove(edt)
+                            else
+                                edt[#edt] = str
+                            end
+                        end
+                    end
+                end
+            end
+        end
+    end
+end
+
+local function rename_space(root, oldspace, newspace) -- fast variant
+    local ndt = #root.dt
+    for i=1,ndt or 0 do
+        local e = root[i]
+        if type(e) == "table" then
+            if e.ns == oldspace then
+                e.ns = newspace
+                if e.rn then
+                    e.rn = newspace
+                end
+            end
+            local edt = e.dt
+            if edt then
+                rename_space(edt, oldspace, newspace)
+            end
+        end
+    end
+end
+
+xml.rename_space = rename_space
+
+function xml.remap_tag(root, pattern, newtg)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            collected[c].tg = newtg
+        end
+    end
+end
+
+function xml.remap_namespace(root, pattern, newns)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            collected[c].ns = newns
+        end
+    end
+end
+
+function xml.check_namespace(root, pattern, newns)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            if (not e.rn or e.rn == "") and e.ns == "" then
+                e.rn = newns
+            end
+        end
+    end
+end
+
+function xml.remap_name(root, pattern, newtg, newns, newrn)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            e.tg, e.ns, e.rn = newtg, newns, newrn
+        end
+    end
+end
+
+--[[ldx--
+<p>Here are a few synonyms.</p>
+--ldx]]--
+
+xml.each     = xml.each_element
+xml.process  = xml.process_element
+xml.strip    = xml.strip_whitespace
+xml.collect  = xml.collect_elements
+xml.all      = xml.collect_elements
+
+xml.insert   = xml.insert_element_after
+xml.inject   = xml.inject_element_after
+xml.after    = xml.insert_element_after
+xml.before   = xml.insert_element_before
+xml.delete   = xml.delete_element
+xml.replace  = xml.replace_element
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['lxml-xml'] = {
+    version   = 1.001,
+    comment   = "this module is the basis for the lxml-* ones",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local finalizers   = xml.finalizers.xml
+local xmlfilter    = xml.filter -- we could inline this one for speed
+local xmltostring  = xml.tostring
+local xmlserialize = xml.serialize
+
+local function first(collected) -- wrong ?
+    return collected and collected[1]
+end
+
+local function last(collected)
+    return collected and collected[#collected]
+end
+
+local function all(collected)
+    return collected
+end
+
+local function reverse(collected)
+    if collected then
+        local reversed = { }
+        for c=#collected,1,-1 do
+            reversed[#reversed+1] = collected[c]
+        end
+        return reversed
+    end
+end
+
+local function attribute(collected,name)
+    if collected and #collected > 0 then
+        local at = collected[1].at
+        return at and at[name]
+    end
+end
+
+local function att(id,name)
+    local at = id.at
+    return at and at[name]
+end
+
+local function count(collected)
+    return (collected and #collected) or 0
+end
+
+local function position(collected,n)
+    if collected then
+        n = tonumber(n) or 0
+        if n < 0 then
+            return collected[#collected + n + 1]
+        elseif n > 0 then
+            return collected[n]
+        else
+            return collected[1].mi or 0
+        end
+    end
+end
+
+local function match(collected)
+    return (collected and collected[1].mi) or 0 -- match
+end
+
+local function index(collected)
+    if collected then
+        return collected[1].ni
+    end
+end
+
+local function attributes(collected,arguments)
+    if collected then
+        local at = collected[1].at
+        if arguments then
+            return at[arguments]
+        elseif next(at) then
+            return at -- all of them
+        end
+    end
+end
+
+local function chainattribute(collected,arguments) -- todo: optional levels
+    if collected then
+        local e = collected[1]
+        while e do
+            local at = e.at
+            if at then
+                local a = at[arguments]
+                if a then
+                    return a
+                end
+            else
+                break -- error
+            end
+            e = e.__p__
+        end
+    end
+    return ""
+end
+
+local function raw(collected) -- hybrid
+    if collected then
+        local e = collected[1] or collected
+        return (e and xmlserialize(e)) or "" -- only first as we cannot concat function
+    else
+        return ""
+    end
+end
+
+local function text(collected) -- hybrid
+    if collected then
+        local e = collected[1] or collected
+        return (e and xmltostring(e.dt)) or ""
+    else
+        return ""
+    end
+end
+
+local function texts(collected)
+    if collected then
+        local t = { }
+        for c=1,#collected do
+            local e = collection[c]
+            if e and e.dt then
+                t[#t+1] = e.dt
+            end
+        end
+        return t
+    end
+end
+
+local function tag(collected,n)
+    if collected then
+        local c
+        if n == 0 or not n then
+            c = collected[1]
+        elseif n > 1 then
+            c = collected[n]
+        else
+            c = collected[#collected-n+1]
+        end
+        return c and c.tg
+    end
+end
+
+local function name(collected,n)
+    if collected then
+        local c
+        if n == 0 or not n then
+            c = collected[1]
+        elseif n > 1 then
+            c = collected[n]
+        else
+            c = collected[#collected-n+1]
+        end
+        if c then
+            if c.ns == "" then
+                return c.tg
+            else
+                return c.ns .. ":" .. c.tg
+            end
+        end
+    end
+end
+
+local function tags(collected,nonamespace)
+    if collected then
+        local t = { }
+        for c=1,#collected do
+            local e = collected[c]
+            local ns, tg = e.ns, e.tg
+            if nonamespace or ns == "" then
+                t[#t+1] = tg
+            else
+                t[#t+1] = ns .. ":" .. tg
+            end
+        end
+        return t
+    end
+end
+
+local function empty(collected)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            if e then
+                local edt = e.dt
+                if edt then
+                    local n = #edt
+                    if n == 1 then
+                        local edk = edt[1]
+                        local typ = type(edk)
+                        if typ == "table" then
+                            return false
+                        elseif edk ~= "" then -- maybe an extra tester for spacing only
+                            return false
+                        end
+                    elseif n > 1 then
+                        return false
+                    end
+                end
+            end
+        end
+    end
+    return true
+end
+
+finalizers.first          = first
+finalizers.last           = last
+finalizers.all            = all
+finalizers.reverse        = reverse
+finalizers.elements       = all
+finalizers.default        = all
+finalizers.attribute      = attribute
+finalizers.att            = att
+finalizers.count          = count
+finalizers.position       = position
+finalizers.match          = match
+finalizers.index          = index
+finalizers.attributes     = attributes
+finalizers.chainattribute = chainattribute
+finalizers.text           = text
+finalizers.texts          = texts
+finalizers.tag            = tag
+finalizers.name           = name
+finalizers.tags           = tags
+finalizers.empty          = empty
+
+-- shortcuts -- we could support xmlfilter(id,pattern,first)
+
+function xml.first(id,pattern)
+    return first(xmlfilter(id,pattern))
+end
+
+function xml.last(id,pattern)
+    return last(xmlfilter(id,pattern))
+end
+
+function xml.count(id,pattern)
+    return count(xmlfilter(id,pattern))
+end
+
+function xml.attribute(id,pattern,a,default)
+    return attribute(xmlfilter(id,pattern),a,default)
+end
+
+function xml.raw(id,pattern)
+    if pattern then
+        return raw(xmlfilter(id,pattern))
+    else
+        return raw(id)
+    end
+end
+
+function xml.text(id,pattern)
+    if pattern then
+     -- return text(xmlfilter(id,pattern))
+        local collected = xmlfilter(id,pattern)
+        return (collected and xmltostring(collected[1].dt)) or ""
+    elseif id then
+     -- return text(id)
+        return xmltostring(id.dt) or ""
+    else
+        return ""
+    end
+end
+
+xml.content = text
+
+function xml.position(id,pattern,n) -- element
+    return position(xmlfilter(id,pattern),n)
+end
+
+function xml.match(id,pattern) -- number
+    return match(xmlfilter(id,pattern))
+end
+
+function xml.empty(id,pattern)
+    return empty(xmlfilter(id,pattern))
+end
+
+xml.all    = xml.filter
+xml.index  = xml.position
+xml.found  = xml.filter
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['luat-env'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- A former version provided functionality for non embeded core
+-- scripts i.e. runtime library loading. Given the amount of
+-- Lua code we use now, this no longer makes sense. Much of this
+-- evolved before bytecode arrays were available and so a lot of
+-- code has disappeared already.
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+local format, sub, match, gsub, find = string.format, string.sub, string.match, string.gsub, string.find
+local unquote, quote = string.unquote, string.quote
+
+-- precautions
+
+os.setlocale(nil,nil) -- useless feature and even dangerous in luatex
+
+function os.setlocale()
+    -- no way you can mess with it
+end
+
+-- dirty tricks
+
+if arg and (arg[0] == 'luatex' or arg[0] == 'luatex.exe') and arg[1] == "--luaonly" then
+    arg[-1]=arg[0] arg[0]=arg[2] for k=3,#arg do arg[k-2]=arg[k] end arg[#arg]=nil arg[#arg]=nil
+end
+
+if profiler and os.env["MTX_PROFILE_RUN"] == "YES" then
+    profiler.start("luatex-profile.log")
+end
+
+-- environment
+
+environment             = environment or { }
+environment.arguments   = { }
+environment.files       = { }
+environment.sortedflags = nil
+
+if not environment.jobname or environment.jobname == "" then if tex then environment.jobname = tex.jobname end end
+if not environment.version or environment.version == "" then             environment.version = "unknown"   end
+if not environment.jobname                              then             environment.jobname = "unknown"   end
+
+function environment.initialize_arguments(arg)
+    local arguments, files = { }, { }
+    environment.arguments, environment.files, environment.sortedflags = arguments, files, nil
+    for index=1,#arg do
+        local argument = arg[index]
+        if index > 0 then
+            local flag, value = match(argument,"^%-+(.-)=(.-)$")
+            if flag then
+                arguments[flag] = unquote(value or "")
+            else
+                flag = match(argument,"^%-+(.+)")
+                if flag then
+                    arguments[flag] = true
+                else
+                    files[#files+1] = argument
+                end
+            end
+        end
+    end
+    environment.ownname = environment.ownname or arg[0] or 'unknown.lua'
+end
+
+function environment.setargument(name,value)
+    environment.arguments[name] = value
+end
+
+-- todo: defaults, better checks e.g on type (boolean versus string)
+--
+-- tricky: too many hits when we support partials unless we add
+-- a registration of arguments so from now on we have 'partial'
+
+function environment.argument(name,partial)
+    local arguments, sortedflags = environment.arguments, environment.sortedflags
+    if arguments[name] then
+        return arguments[name]
+    elseif partial then
+        if not sortedflags then
+            sortedflags = table.sortedkeys(arguments)
+            for k=1,#sortedflags do
+                sortedflags[k] = "^" .. sortedflags[k]
+            end
+            environment.sortedflags = sortedflags
+        end
+        -- example of potential clash: ^mode ^modefile
+        for k=1,#sortedflags do
+            local v = sortedflags[k]
+            if find(name,v) then
+                return arguments[sub(v,2,#v)]
+            end
+        end
+    end
+    return nil
+end
+
+environment.argument("x",true)
+
+function environment.split_arguments(separator) -- rather special, cut-off before separator
+    local done, before, after = false, { }, { }
+    local original_arguments = environment.original_arguments
+    for k=1,#original_arguments do
+        local v = original_arguments[k]
+        if not done and v == separator then
+            done = true
+        elseif done then
+            after[#after+1] = v
+        else
+            before[#before+1] = v
+        end
+    end
+    return before, after
+end
+
+function environment.reconstruct_commandline(arg,noquote)
+    arg = arg or environment.original_arguments
+    if noquote and #arg == 1 then
+        local a = arg[1]
+        a = resolvers.resolve(a)
+        a = unquote(a)
+        return a
+    elseif #arg > 0 then
+        local result = { }
+        for i=1,#arg do
+            local a = arg[i]
+            a = resolvers.resolve(a)
+            a = unquote(a)
+            a = gsub(a,'"','\\"') -- tricky
+            if find(a," ") then
+                result[#result+1] = quote(a)
+            else
+                result[#result+1] = a
+            end
+        end
+        return table.join(result," ")
+    else
+        return ""
+    end
+end
+
+if arg then
+
+    -- new, reconstruct quoted snippets (maybe better just remove the " then and add them later)
+    local newarg, instring = { }, false
+
+    for index=1,#arg do
+        local argument = arg[index]
+        if find(argument,"^\"") then
+            newarg[#newarg+1] = gsub(argument,"^\"","")
+            if not find(argument,"\"$") then
+                instring = true
+            end
+        elseif find(argument,"\"$") then
+            newarg[#newarg] = newarg[#newarg] .. " " .. gsub(argument,"\"$","")
+            instring = false
+        elseif instring then
+            newarg[#newarg] = newarg[#newarg] .. " " .. argument
+        else
+            newarg[#newarg+1] = argument
+        end
+    end
+    for i=1,-5,-1 do
+        newarg[i] = arg[i]
+    end
+
+    environment.initialize_arguments(newarg)
+    environment.original_arguments = newarg
+    environment.raw_arguments = arg
+
+    arg = { } -- prevent duplicate handling
+
+end
+
+-- weird place ... depends on a not yet loaded module
+
+function environment.texfile(filename)
+    return resolvers.find_file(filename,'tex')
+end
+
+function environment.luafile(filename)
+    local resolved = resolvers.find_file(filename,'tex') or ""
+    if resolved ~= "" then
+        return resolved
+    end
+    resolved = resolvers.find_file(filename,'texmfscripts') or ""
+    if resolved ~= "" then
+        return resolved
+    end
+    return resolvers.find_file(filename,'luatexlibs') or ""
+end
+
+environment.loadedluacode = loadfile -- can be overloaded
+
+--~ function environment.loadedluacode(name)
+--~     if os.spawn("texluac -s -o texluac.luc " .. name) == 0 then
+--~         local chunk = loadstring(io.loaddata("texluac.luc"))
+--~         os.remove("texluac.luc")
+--~         return chunk
+--~     else
+--~         environment.loadedluacode = loadfile -- can be overloaded
+--~         return loadfile(name)
+--~     end
+--~ end
+
+function environment.luafilechunk(filename) -- used for loading lua bytecode in the format
+    filename = file.replacesuffix(filename, "lua")
+    local fullname = environment.luafile(filename)
+    if fullname and fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading file %s", fullname)
+        end
+        return environment.loadedluacode(fullname)
+    else
+        if trace_locating then
+            logs.report("fileio","unknown file %s", filename)
+        end
+        return nil
+    end
+end
+
+-- the next ones can use the previous ones / combine
+
+function environment.loadluafile(filename, version)
+    local lucname, luaname, chunk
+    local basename = file.removesuffix(filename)
+    if basename == filename then
+        lucname, luaname = basename .. ".luc",  basename .. ".lua"
+    else
+        lucname, luaname = nil, basename -- forced suffix
+    end
+    -- when not overloaded by explicit suffix we look for a luc file first
+    local fullname = (lucname and environment.luafile(lucname)) or ""
+    if fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading %s", fullname)
+        end
+        chunk = loadfile(fullname) -- this way we don't need a file exists check
+    end
+    if chunk then
+        assert(chunk)()
+        if version then
+            -- we check of the version number of this chunk matches
+            local v = version -- can be nil
+            if modules and modules[filename] then
+                v = modules[filename].version -- new method
+            elseif versions and versions[filename] then
+                v = versions[filename]        -- old method
+            end
+            if v == version then
+                return true
+            else
+                if trace_locating then
+                    logs.report("fileio","version mismatch for %s: lua=%s, luc=%s", filename, v, version)
+                end
+                environment.loadluafile(filename)
+            end
+        else
+            return true
+        end
+    end
+    fullname = (luaname and environment.luafile(luaname)) or ""
+    if fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading %s", fullname)
+        end
+        chunk = loadfile(fullname) -- this way we don't need a file exists check
+        if not chunk then
+            if trace_locating then
+                logs.report("fileio","unknown file %s", filename)
+            end
+        else
+            assert(chunk)()
+            return true
+        end
+    end
+    return false
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-inf'] = {
+    version   = 1.001,
+    comment   = "companion to trac-inf.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format = string.format
+
+local statusinfo, n, registered = { }, 0, { }
+
+statistics = statistics or { }
+
+statistics.enable    = true
+statistics.threshold = 0.05
+
+-- timing functions
+
+local clock = os.gettimeofday or os.clock
+
+local notimer
+
+function statistics.hastimer(instance)
+    return instance and instance.starttime
+end
+
+function statistics.resettiming(instance)
+    if not instance then
+        notimer = { timing = 0, loadtime = 0 }
+    else
+        instance.timing, instance.loadtime = 0, 0
+    end
+end
+
+function statistics.starttiming(instance)
+    if not instance then
+        notimer = { }
+        instance = notimer
+    end
+    local it = instance.timing
+    if not it then
+        it = 0
+    end
+    if it == 0 then
+        instance.starttime = clock()
+        if not instance.loadtime then
+            instance.loadtime = 0
+        end
+    else
+--~         logs.report("system","nested timing (%s)",tostring(instance))
+    end
+    instance.timing = it + 1
+end
+
+function statistics.stoptiming(instance, report)
+    if not instance then
+        instance = notimer
+    end
+    if instance then
+        local it = instance.timing
+        if it > 1 then
+            instance.timing = it - 1
+        else
+            local starttime = instance.starttime
+            if starttime then
+                local stoptime = clock()
+                local loadtime = stoptime - starttime
+                instance.stoptime = stoptime
+                instance.loadtime = instance.loadtime + loadtime
+                if report then
+                    statistics.report("load time %0.3f",loadtime)
+                end
+                instance.timing = 0
+                return loadtime
+            end
+        end
+    end
+    return 0
+end
+
+function statistics.elapsedtime(instance)
+    if not instance then
+        instance = notimer
+    end
+    return format("%0.3f",(instance and instance.loadtime) or 0)
+end
+
+function statistics.elapsedindeed(instance)
+    if not instance then
+        instance = notimer
+    end
+    local t = (instance and instance.loadtime) or 0
+    return t > statistics.threshold
+end
+
+function statistics.elapsedseconds(instance,rest) -- returns nil if 0 seconds
+    if statistics.elapsedindeed(instance) then
+        return format("%s seconds %s", statistics.elapsedtime(instance),rest or "")
+    end
+end
+
+-- general function
+
+function statistics.register(tag,fnc)
+    if statistics.enable and type(fnc) == "function" then
+        local rt = registered[tag] or (#statusinfo + 1)
+        statusinfo[rt] = { tag, fnc }
+        registered[tag] = rt
+        if #tag > n then n = #tag end
+    end
+end
+
+function statistics.show(reporter)
+    if statistics.enable then
+        if not reporter then reporter = function(tag,data,n) texio.write_nl(tag .. " " .. data) end end
+        -- this code will move
+        local register = statistics.register
+        register("luatex banner", function()
+            return string.lower(status.banner)
+        end)
+        register("control sequences", function()
+            return format("%s of %s", status.cs_count, status.hash_size+status.hash_extra)
+        end)
+        register("callbacks", function()
+            local total, indirect = status.callbacks or 0, status.indirect_callbacks or 0
+            return format("direct: %s, indirect: %s, total: %s", total-indirect, indirect, total)
+        end)
+        register("current memory usage", statistics.memused)
+        register("runtime",statistics.runtime)
+--         --
+        for i=1,#statusinfo do
+            local s = statusinfo[i]
+            local r = s[2]()
+            if r then
+                reporter(s[1],r,n)
+            end
+        end
+        texio.write_nl("") -- final newline
+        statistics.enable = false
+    end
+end
+
+function statistics.show_job_stat(tag,data,n)
+    texio.write_nl(format("%-15s: %s - %s","mkiv lua stats",tag:rpadd(n," "),data))
+end
+
+function statistics.memused() -- no math.round yet -)
+    local round = math.round or math.floor
+    return format("%s MB (ctx: %s MB)",round(collectgarbage("count")/1000), round(status.luastate_bytes/1000000))
+end
+
+if statistics.runtime then
+    -- already loaded and set
+elseif luatex and luatex.starttime then
+    statistics.starttime = luatex.starttime
+    statistics.loadtime = 0
+    statistics.timing = 0
+else
+    statistics.starttiming(statistics)
+end
+
+function statistics.runtime()
+    statistics.stoptiming(statistics)
+    return statistics.formatruntime(statistics.elapsedtime(statistics))
+end
+
+function statistics.formatruntime(runtime)
+    return format("%s seconds", statistics.elapsedtime(statistics))
+end
+
+function statistics.timed(action,report)
+    local timer = { }
+    report = report or logs.simple
+    statistics.starttiming(timer)
+    action()
+    statistics.stoptiming(timer)
+    report("total runtime: %s",statistics.elapsedtime(timer))
+end
+
+-- where, not really the best spot for this:
+
+commands = commands or { }
+
+local timer
+
+function commands.resettimer()
+    statistics.resettiming(timer)
+    statistics.starttiming(timer)
+end
+
+function commands.elapsedtime()
+    statistics.stoptiming(timer)
+    tex.sprint(statistics.elapsedtime(timer))
+end
+
+commands.resettimer()
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-log'] = {
+    version   = 1.001,
+    comment   = "companion to trac-log.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- this is old code that needs an overhaul
+
+--~ io.stdout:setvbuf("no")
+--~ io.stderr:setvbuf("no")
+
+local write_nl, write = texio.write_nl or print, texio.write or io.write
+local format, gmatch = string.format, string.gmatch
+local texcount = tex and tex.count
+
+if texlua then
+    write_nl = print
+    write    = io.write
+end
+
+--[[ldx--
+<p>This is a prelude to a more extensive logging module. For the sake
+of parsing log files, in addition to the standard logging we will
+provide an <l n='xml'/> structured file. Actually, any logging that
+is hooked into callbacks will be \XML\ by default.</p>
+--ldx]]--
+
+logs     = logs     or { }
+logs.xml = logs.xml or { }
+logs.tex = logs.tex or { }
+
+--[[ldx--
+<p>This looks pretty ugly but we need to speed things up a bit.</p>
+--ldx]]--
+
+logs.moreinfo = [[
+more information about ConTeXt and the tools that come with it can be found at:
+
+maillist : ntg-context@ntg.nl / http://www.ntg.nl/mailman/listinfo/ntg-context
+webpage  : http://www.pragma-ade.nl / http://tex.aanhet.net
+wiki     : http://contextgarden.net
+]]
+
+logs.levels = {
+    ['error']   = 1,
+    ['warning'] = 2,
+    ['info']    = 3,
+    ['debug']   = 4,
+}
+
+logs.functions = {
+    'report', 'start', 'stop', 'push', 'pop', 'line', 'direct',
+    'start_run', 'stop_run',
+    'start_page_number', 'stop_page_number',
+    'report_output_pages', 'report_output_log',
+    'report_tex_stat', 'report_job_stat',
+    'show_open', 'show_close', 'show_load',
+}
+
+logs.tracers = {
+}
+
+logs.level = 0
+logs.mode  = string.lower((os.getenv("MTX.LOG.MODE") or os.getenv("MTX_LOG_MODE") or "tex"))
+
+function logs.set_level(level)
+    logs.level = logs.levels[level] or level
+end
+
+function logs.set_method(method)
+    for _, v in next, logs.functions do
+        logs[v] = logs[method][v] or function() end
+    end
+end
+
+-- tex logging
+
+function logs.tex.report(category,fmt,...) -- new
+    if fmt then
+        write_nl(category .. " | " .. format(fmt,...))
+    else
+        write_nl(category .. " |")
+    end
+end
+
+function logs.tex.line(fmt,...) -- new
+    if fmt then
+        write_nl(format(fmt,...))
+    else
+        write_nl("")
+    end
+end
+
+--~ function logs.tex.start_page_number()
+--~     local real, user, sub = texcount.realpageno, texcount.userpageno, texcount.subpageno
+--~     if real > 0 then
+--~         if user > 0 then
+--~             if sub > 0 then
+--~                 write(format("[%s.%s.%s",real,user,sub))
+--~             else
+--~                 write(format("[%s.%s",real,user))
+--~             end
+--~         else
+--~             write(format("[%s",real))
+--~         end
+--~     else
+--~         write("[-")
+--~     end
+--~ end
+
+--~ function logs.tex.stop_page_number()
+--~     write("]")
+--~ end
+
+local real, user, sub
+
+function logs.tex.start_page_number()
+    real, user, sub = texcount.realpageno, texcount.userpageno, texcount.subpageno
+end
+
+function logs.tex.stop_page_number()
+    if real > 0 then
+        if user > 0 then
+            if sub > 0 then
+                logs.report("pages", "flushing realpage %s, userpage %s, subpage %s",real,user,sub)
+            else
+                logs.report("pages", "flushing realpage %s, userpage %s",real,user)
+            end
+        else
+            logs.report("pages", "flushing realpage %s",real)
+        end
+    else
+        logs.report("pages", "flushing page")
+    end
+    io.flush()
+end
+
+logs.tex.report_job_stat = statistics.show_job_stat
+
+-- xml logging
+
+function logs.xml.report(category,fmt,...) -- new
+    if fmt then
+        write_nl(format("<r category='%s'>%s</r>",category,format(fmt,...)))
+    else
+        write_nl(format("<r category='%s'/>",category))
+    end
+end
+function logs.xml.line(fmt,...) -- new
+    if fmt then
+        write_nl(format("<r>%s</r>",format(fmt,...)))
+    else
+        write_nl("<r/>")
+    end
+end
+
+function logs.xml.start() if logs.level > 0 then tw("<%s>" ) end end
+function logs.xml.stop () if logs.level > 0 then tw("</%s>") end end
+function logs.xml.push () if logs.level > 0 then tw("<!-- ") end end
+function logs.xml.pop  () if logs.level > 0 then tw(" -->" ) end end
+
+function logs.xml.start_run()
+    write_nl("<?xml version='1.0' standalone='yes'?>")
+    write_nl("<job>") --  xmlns='www.pragma-ade.com/luatex/schemas/context-job.rng'
+    write_nl("")
+end
+
+function logs.xml.stop_run()
+    write_nl("</job>")
+end
+
+function logs.xml.start_page_number()
+    write_nl(format("<p real='%s' page='%s' sub='%s'", texcount.realpageno, texcount.userpageno, texcount.subpageno))
+end
+
+function logs.xml.stop_page_number()
+    write("/>")
+    write_nl("")
+end
+
+function logs.xml.report_output_pages(p,b)
+    write_nl(format("<v k='pages' v='%s'/>", p))
+    write_nl(format("<v k='bytes' v='%s'/>", b))
+    write_nl("")
+end
+
+function logs.xml.report_output_log()
+end
+
+function logs.xml.report_tex_stat(k,v)
+    texiowrite_nl("log","<v k='"..k.."'>"..tostring(v).."</v>")
+end
+
+local level = 0
+
+function logs.xml.show_open(name)
+    level = level + 1
+    texiowrite_nl(format("<f l='%s' n='%s'>",level,name))
+end
+
+function logs.xml.show_close(name)
+    texiowrite("</f> ")
+    level = level - 1
+end
+
+function logs.xml.show_load(name)
+    texiowrite_nl(format("<f l='%s' n='%s'/>",level+1,name))
+end
+
+--
+
+local name, banner = 'report', 'context'
+
+local function report(category,fmt,...)
+    if fmt then
+        write_nl(format("%s | %s: %s",name,category,format(fmt,...)))
+    elseif category then
+        write_nl(format("%s | %s",name,category))
+    else
+        write_nl(format("%s |",name))
+    end
+end
+
+local function simple(fmt,...)
+    if fmt then
+        write_nl(format("%s | %s",name,format(fmt,...)))
+    else
+        write_nl(format("%s |",name))
+    end
+end
+
+function logs.setprogram(_name_,_banner_,_verbose_)
+    name, banner = _name_, _banner_
+    if _verbose_ then
+        trackers.enable("resolvers.locating")
+    end
+    logs.set_method("tex")
+    logs.report = report -- also used in libraries
+    logs.simple = simple -- only used in scripts !
+    if utils then
+        utils.report = simple
+    end
+    logs.verbose = _verbose_
+end
+
+function logs.setverbose(what)
+    if what then
+        trackers.enable("resolvers.locating")
+    else
+        trackers.disable("resolvers.locating")
+    end
+    logs.verbose = what or false
+end
+
+function logs.extendbanner(_banner_,_verbose_)
+    banner = banner .. " | ".. _banner_
+    if _verbose_ ~= nil then
+        logs.setverbose(what)
+    end
+end
+
+logs.verbose = false
+logs.report  = logs.tex.report
+logs.simple  = logs.tex.report
+
+function logs.reportlines(str) -- todo: <lines></lines>
+    for line in gmatch(str,"(.-)[\n\r]") do
+        logs.report(line)
+    end
+end
+
+function logs.reportline() -- for scripts too
+    logs.report()
+end
+
+logs.simpleline = logs.reportline
+
+function logs.reportbanner() -- for scripts too
+    logs.report(banner)
+end
+
+function logs.help(message,option)
+    logs.reportbanner()
+    logs.reportline()
+    logs.reportlines(message)
+    local moreinfo = logs.moreinfo or ""
+    if moreinfo ~= "" and option ~= "nomoreinfo" then
+        logs.reportline()
+        logs.reportlines(moreinfo)
+    end
+end
+
+logs.set_level('error')
+logs.set_method('tex')
+
+function logs.system(whereto,process,jobname,category,...)
+    for i=1,10 do
+        local f = io.open(whereto,"a")
+        if f then
+            f:write(format("%s %s => %s => %s => %s\r",os.date("%d/%m/%y %H:%m:%S"),process,jobname,category,format(...)))
+            f:close()
+            break
+        else
+            sleep(0.1)
+        end
+    end
+end
+
+--~ local syslogname = "oeps.xxx"
+--~
+--~ for i=1,10 do
+--~     logs.system(syslogname,"context","test","fonts","font %s recached due to newer version (%s)","blabla","123")
+--~ end
+
+function logs.fatal(where,...)
+    logs.report(where,"fatal error: %s, aborting now",format(...))
+    os.exit()
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-inp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files",
+}
+
+-- After a few years using the code the large luat-inp.lua file
+-- has been split up a bit. In the process some functionality was
+-- dropped:
+--
+-- * support for reading lsr files
+-- * selective scanning (subtrees)
+-- * some public auxiliary functions were made private
+--
+-- TODO: os.getenv -> os.env[]
+-- TODO: instances.[hashes,cnffiles,configurations,522]
+-- TODO: check escaping in find etc, too much, too slow
+
+-- This lib is multi-purpose and can be loaded again later on so that
+-- additional functionality becomes available. We will split thislogs.report("fileio",
+-- module in components once we're done with prototyping. This is the
+-- first code I wrote for LuaTeX, so it needs some cleanup. Before changing
+-- something in this module one can best check with Taco or Hans first; there
+-- is some nasty trickery going on that relates to traditional kpse support.
+
+-- To be considered: hash key lowercase, first entry in table filename
+-- (any case), rest paths (so no need for optimization). Or maybe a
+-- separate table that matches lowercase names to mixed case when
+-- present. In that case the lower() cases can go away. I will do that
+-- only when we run into problems with names ... well ... Iwona-Regular.
+
+-- Beware, loading and saving is overloaded in luat-tmp!
+
+local format, gsub, find, lower, upper, match, gmatch = string.format, string.gsub, string.find, string.lower, string.upper, string.match, string.gmatch
+local concat, insert, sortedkeys = table.concat, table.insert, table.sortedkeys
+local next, type = next, type
+local lpegmatch = lpeg.match
+
+local trace_locating, trace_detail, trace_expansions = false, false, false
+
+trackers.register("resolvers.locating",   function(v) trace_locating   = v end)
+trackers.register("resolvers.details",    function(v) trace_detail     = v end)
+trackers.register("resolvers.expansions", function(v) trace_expansions = v end) -- todo
+
+if not resolvers then
+    resolvers = {
+        suffixes     = { },
+        formats      = { },
+        dangerous    = { },
+        suffixmap    = { },
+        alternatives = { },
+        locators     = { },  -- locate databases
+        hashers      = { },  -- load databases
+        generators   = { },  -- generate databases
+    }
+end
+
+local resolvers = resolvers
+
+resolvers.locators  .notfound = { nil }
+resolvers.hashers   .notfound = { nil }
+resolvers.generators.notfound = { nil }
+
+resolvers.cacheversion = '1.0.1'
+resolvers.cnfname      = 'texmf.cnf'
+resolvers.luaname      = 'texmfcnf.lua'
+resolvers.homedir      = os.env[os.type == "windows" and 'USERPROFILE'] or os.env['HOME'] or '~'
+resolvers.cnfdefault   = '{$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,.local,}/web2c}'
+
+local dummy_path_expr = "^!*unset/*$"
+
+local formats      = resolvers.formats
+local suffixes     = resolvers.suffixes
+local dangerous    = resolvers.dangerous
+local suffixmap    = resolvers.suffixmap
+local alternatives = resolvers.alternatives
+
+formats['afm'] = 'AFMFONTS'       suffixes['afm'] = { 'afm' }
+formats['enc'] = 'ENCFONTS'       suffixes['enc'] = { 'enc' }
+formats['fmt'] = 'TEXFORMATS'     suffixes['fmt'] = { 'fmt' }
+formats['map'] = 'TEXFONTMAPS'    suffixes['map'] = { 'map' }
+formats['mp']  = 'MPINPUTS'       suffixes['mp']  = { 'mp' }
+formats['ocp'] = 'OCPINPUTS'      suffixes['ocp'] = { 'ocp' }
+formats['ofm'] = 'OFMFONTS'       suffixes['ofm'] = { 'ofm', 'tfm' }
+formats['otf'] = 'OPENTYPEFONTS'  suffixes['otf'] = { 'otf' } -- 'ttf'
+formats['opl'] = 'OPLFONTS'       suffixes['opl'] = { 'opl' }
+formats['otp'] = 'OTPINPUTS'      suffixes['otp'] = { 'otp' }
+formats['ovf'] = 'OVFFONTS'       suffixes['ovf'] = { 'ovf', 'vf' }
+formats['ovp'] = 'OVPFONTS'       suffixes['ovp'] = { 'ovp' }
+formats['tex'] = 'TEXINPUTS'      suffixes['tex'] = { 'tex' }
+formats['tfm'] = 'TFMFONTS'       suffixes['tfm'] = { 'tfm' }
+formats['ttf'] = 'TTFONTS'        suffixes['ttf'] = { 'ttf', 'ttc', 'dfont' }
+formats['pfb'] = 'T1FONTS'        suffixes['pfb'] = { 'pfb', 'pfa' }
+formats['vf']  = 'VFFONTS'        suffixes['vf']  = { 'vf' }
+
+formats['fea'] = 'FONTFEATURES'   suffixes['fea'] = { 'fea' }
+formats['cid'] = 'FONTCIDMAPS'    suffixes['cid'] = { 'cid', 'cidmap' }
+
+formats ['texmfscripts'] = 'TEXMFSCRIPTS' -- new
+suffixes['texmfscripts'] = { 'rb', 'pl', 'py' } -- 'lua'
+
+formats ['lua'] = 'LUAINPUTS' -- new
+suffixes['lua'] = { 'lua', 'luc', 'tma', 'tmc' }
+
+-- backward compatible ones
+
+alternatives['map files']            = 'map'
+alternatives['enc files']            = 'enc'
+alternatives['cid maps']             = 'cid' -- great, why no cid files
+alternatives['font feature files']   = 'fea' -- and fea files here
+alternatives['opentype fonts']       = 'otf'
+alternatives['truetype fonts']       = 'ttf'
+alternatives['truetype collections'] = 'ttc'
+alternatives['truetype dictionary']  = 'dfont'
+alternatives['type1 fonts']          = 'pfb'
+
+-- obscure ones
+
+formats ['misc fonts'] = ''
+suffixes['misc fonts'] = { }
+
+formats     ['sfd']                      = 'SFDFONTS'
+suffixes    ['sfd']                      = { 'sfd' }
+alternatives['subfont definition files'] = 'sfd'
+
+-- lib paths
+
+formats ['lib'] = 'CLUAINPUTS' -- new (needs checking)
+suffixes['lib'] = (os.libsuffix and { os.libsuffix }) or { 'dll', 'so' }
+
+-- In practice we will work within one tds tree, but i want to keep
+-- the option open to build tools that look at multiple trees, which is
+-- why we keep the tree specific data in a table. We used to pass the
+-- instance but for practical pusposes we now avoid this and use a
+-- instance variable.
+
+-- here we catch a few new thingies (todo: add these paths to context.tmf)
+--
+-- FONTFEATURES  = .;$TEXMF/fonts/fea//
+-- FONTCIDMAPS   = .;$TEXMF/fonts/cid//
+
+-- we always have one instance active
+
+resolvers.instance = resolvers.instance or nil -- the current one (slow access)
+local instance = resolvers.instance or nil -- the current one (fast access)
+
+function resolvers.newinstance()
+
+    -- store once, freeze and faster (once reset we can best use
+    -- instance.environment) maybe better have a register suffix
+    -- function
+
+    for k, v in next, suffixes do
+        for i=1,#v do
+            local vi = v[i]
+            if vi then
+                suffixmap[vi] = k
+            end
+        end
+    end
+
+    -- because vf searching is somewhat dangerous, we want to prevent
+    -- too liberal searching esp because we do a lookup on the current
+    -- path anyway; only tex (or any) is safe
+
+    for k, v in next, formats do
+        dangerous[k] = true
+    end
+    dangerous.tex = nil
+
+    -- the instance
+
+    local newinstance = {
+        rootpath        = '',
+        treepath        = '',
+        progname        = 'context',
+        engine          = 'luatex',
+        format          = '',
+        environment     = { },
+        variables       = { },
+        expansions      = { },
+        files           = { },
+        remap           = { },
+        configuration   = { },
+        setup           = { },
+        order           = { },
+        found           = { },
+        foundintrees    = { },
+        kpsevars        = { },
+        hashes          = { },
+        cnffiles        = { },
+        luafiles        = { },
+        lists           = { },
+        remember        = true,
+        diskcache       = true,
+        renewcache      = false,
+        scandisk        = true,
+        cachepath       = nil,
+        loaderror       = false,
+        sortdata        = false,
+        savelists       = true,
+        cleanuppaths    = true,
+        allresults      = false,
+        pattern         = nil, -- lists
+        data            = { }, -- only for loading
+        force_suffixes  = true,
+        fakepaths       = { },
+    }
+
+    local ne = newinstance.environment
+
+    for k,v in next, os.env do
+        ne[k] = resolvers.bare_variable(v)
+    end
+
+    return newinstance
+
+end
+
+function resolvers.setinstance(someinstance)
+    instance = someinstance
+    resolvers.instance = someinstance
+    return someinstance
+end
+
+function resolvers.reset()
+    return resolvers.setinstance(resolvers.newinstance())
+end
+
+local function reset_hashes()
+    instance.lists = { }
+    instance.found = { }
+end
+
+local function check_configuration() -- not yet ok, no time for debugging now
+    local ie, iv = instance.environment, instance.variables
+    local function fix(varname,default)
+        local proname = varname .. "." .. instance.progname or "crap"
+        local p, v = ie[proname], ie[varname] or iv[varname]
+        if not ((p and p ~= "") or (v and v ~= "")) then
+            iv[varname] = default -- or environment?
+        end
+    end
+    local name = os.name
+    if name == "windows" then
+        fix("OSFONTDIR", "c:/windows/fonts//")
+    elseif name == "macosx" then
+        fix("OSFONTDIR", "$HOME/Library/Fonts//;/Library/Fonts//;/System/Library/Fonts//")
+    else
+        -- bad luck
+    end
+    fix("LUAINPUTS"   , ".;$TEXINPUTS;$TEXMFSCRIPTS") -- no progname, hm
+    -- this will go away some day
+    fix("FONTFEATURES", ".;$TEXMF/fonts/{data,fea}//;$OPENTYPEFONTS;$TTFONTS;$T1FONTS;$AFMFONTS")
+    fix("FONTCIDMAPS" , ".;$TEXMF/fonts/{data,cid}//;$OPENTYPEFONTS;$TTFONTS;$T1FONTS;$AFMFONTS")
+    --
+    fix("LUATEXLIBS"  , ".;$TEXMF/luatex/lua//")
+end
+
+function resolvers.bare_variable(str) -- assumes str is a string
+    return (gsub(str,"\s*([\"\']?)(.+)%1\s*", "%2"))
+end
+
+function resolvers.settrace(n) -- no longer number but: 'locating' or 'detail'
+    if n then
+        trackers.disable("resolvers.*")
+        trackers.enable("resolvers."..n)
+    end
+end
+
+resolvers.settrace(os.getenv("MTX_INPUT_TRACE"))
+
+function resolvers.osenv(key)
+    local ie = instance.environment
+    local value = ie[key]
+    if value == nil then
+     -- local e = os.getenv(key)
+        local e = os.env[key]
+        if e == nil then
+         -- value = "" -- false
+        else
+            value = resolvers.bare_variable(e)
+        end
+        ie[key] = value
+    end
+    return value or ""
+end
+
+function resolvers.env(key)
+    return instance.environment[key] or resolvers.osenv(key)
+end
+
+--
+
+local function expand_vars(lst) -- simple vars
+    local variables, env = instance.variables, resolvers.env
+    local function resolve(a)
+        return variables[a] or env(a)
+    end
+    for k=1,#lst do
+        lst[k] = gsub(lst[k],"%$([%a%d%_%-]+)",resolve)
+    end
+end
+
+local function expanded_var(var) -- simple vars
+    local function resolve(a)
+        return instance.variables[a] or resolvers.env(a)
+    end
+    return (gsub(var,"%$([%a%d%_%-]+)",resolve))
+end
+
+local function entry(entries,name)
+    if name and (name ~= "") then
+        name = gsub(name,'%$','')
+        local result = entries[name..'.'..instance.progname] or entries[name]
+        if result then
+            return result
+        else
+            result = resolvers.env(name)
+            if result then
+                instance.variables[name] = result
+                resolvers.expand_variables()
+                return instance.expansions[name] or ""
+            end
+        end
+    end
+    return ""
+end
+
+local function is_entry(entries,name)
+    if name and name ~= "" then
+        name = gsub(name,'%$','')
+        return (entries[name..'.'..instance.progname] or entries[name]) ~= nil
+    else
+        return false
+    end
+end
+
+-- {a,b,c,d}
+-- a,b,c/{p,q,r},d
+-- a,b,c/{p,q,r}/d/{x,y,z}//
+-- a,b,c/{p,q/{x,y,z},r},d/{p,q,r}
+-- a,b,c/{p,q/{x,y,z},r},d/{p,q,r}
+-- a{b,c}{d,e}f
+-- {a,b,c,d}
+-- {a,b,c/{p,q,r},d}
+-- {a,b,c/{p,q,r}/d/{x,y,z}//}
+-- {a,b,c/{p,q/{x,y,z}},d/{p,q,r}}
+-- {a,b,c/{p,q/{x,y,z},w}v,d/{p,q,r}}
+-- {$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,.local,}/web2c}
+
+-- this one is better and faster, but it took me a while to realize
+-- that this kind of replacement is cleaner than messy parsing and
+-- fuzzy concatenating we can probably gain a bit with selectively
+-- applying lpeg, but experiments with lpeg parsing this proved not to
+-- work that well; the parsing is ok, but dealing with the resulting
+-- table is a pain because we need to work inside-out recursively
+
+local function do_first(a,b)
+    local t = { }
+    for s in gmatch(b,"[^,]+") do t[#t+1] = a .. s end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_second(a,b)
+    local t = { }
+    for s in gmatch(a,"[^,]+") do t[#t+1] = s .. b end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_both(a,b)
+    local t = { }
+    for sa in gmatch(a,"[^,]+") do
+        for sb in gmatch(b,"[^,]+") do
+            t[#t+1] = sa .. sb
+        end
+    end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_three(a,b,c)
+    return a .. b.. c
+end
+
+local function splitpathexpr(str, t, validate)
+    -- no need for further optimization as it is only called a
+    -- few times, we can use lpeg for the sub
+    if trace_expansions then
+        logs.report("fileio","expanding variable '%s'",str)
+    end
+    t = t or { }
+    str = gsub(str,",}",",@}")
+    str = gsub(str,"{,","{@,")
+ -- str = "@" .. str .. "@"
+    local ok, done
+    while true do
+        done = false
+        while true do
+            str, ok = gsub(str,"([^{},]+){([^{}]+)}",do_first)
+            if ok > 0 then done = true else break end
+        end
+        while true do
+            str, ok = gsub(str,"{([^{}]+)}([^{},]+)",do_second)
+            if ok > 0 then done = true else break end
+        end
+        while true do
+            str, ok = gsub(str,"{([^{}]+)}{([^{}]+)}",do_both)
+            if ok > 0 then done = true else break end
+        end
+        str, ok = gsub(str,"({[^{}]*){([^{}]+)}([^{}]*})",do_three)
+        if ok > 0 then done = true end
+        if not done then break end
+    end
+    str = gsub(str,"[{}]", "")
+    str = gsub(str,"@","")
+    if validate then
+        for s in gmatch(str,"[^,]+") do
+            s = validate(s)
+            if s then t[#t+1] = s end
+        end
+    else
+        for s in gmatch(str,"[^,]+") do
+            t[#t+1] = s
+        end
+    end
+    if trace_expansions then
+        for k=1,#t do
+            logs.report("fileio","% 4i: %s",k,t[k])
+        end
+    end
+    return t
+end
+
+local function expanded_path_from_list(pathlist) -- maybe not a list, just a path
+    -- a previous version fed back into pathlist
+    local newlist, ok = { }, false
+    for k=1,#pathlist do
+        if find(pathlist[k],"[{}]") then
+            ok = true
+            break
+        end
+    end
+    if ok then
+        local function validate(s)
+            s = file.collapse_path(s)
+            return s ~= "" and not find(s,dummy_path_expr) and s
+        end
+        for k=1,#pathlist do
+            splitpathexpr(pathlist[k],newlist,validate)
+        end
+    else
+        for k=1,#pathlist do
+            for p in gmatch(pathlist[k],"([^,]+)") do
+                p = file.collapse_path(p)
+                if p ~= "" then newlist[#newlist+1] = p end
+            end
+        end
+    end
+    return newlist
+end
+
+-- we follow a rather traditional approach:
+--
+-- (1) texmf.cnf given in TEXMFCNF
+-- (2) texmf.cnf searched in default variable
+--
+-- also we now follow the stupid route: if not set then just assume *one*
+-- cnf file under texmf (i.e. distribution)
+
+local args = environment and environment.original_arguments or arg -- this needs a cleanup
+
+resolvers.ownbin = resolvers.ownbin or args[-2] or arg[-2] or args[-1] or arg[-1] or arg[0] or "luatex"
+resolvers.ownbin = gsub(resolvers.ownbin,"\\","/")
+
+function resolvers.getownpath()
+    local ownpath = resolvers.ownpath or os.selfdir
+    if not ownpath or ownpath == "" or ownpath == "unset" then
+        ownpath = args[-1] or arg[-1]
+        ownpath = ownpath and file.dirname(gsub(ownpath,"\\","/"))
+        if not ownpath or ownpath == "" then
+            ownpath = args[-0] or arg[-0]
+            ownpath = ownpath and file.dirname(gsub(ownpath,"\\","/"))
+        end
+        local binary = resolvers.ownbin
+        if not ownpath or ownpath == "" then
+            ownpath = ownpath and file.dirname(binary)
+        end
+        if not ownpath or ownpath == "" then
+            if os.binsuffix ~= "" then
+                binary = file.replacesuffix(binary,os.binsuffix)
+            end
+            for p in gmatch(os.getenv("PATH"),"[^"..io.pathseparator.."]+") do
+                local b = file.join(p,binary)
+                if lfs.isfile(b) then
+                    -- we assume that after changing to the path the currentdir function
+                    -- resolves to the real location and use this side effect here; this
+                    -- trick is needed because on the mac installations use symlinks in the
+                    -- path instead of real locations
+                    local olddir = lfs.currentdir()
+                    if lfs.chdir(p) then
+                        local pp = lfs.currentdir()
+                        if trace_locating and p ~= pp then
+                            logs.report("fileio","following symlink '%s' to '%s'",p,pp)
+                        end
+                        ownpath = pp
+                        lfs.chdir(olddir)
+                    else
+                        if trace_locating then
+                            logs.report("fileio","unable to check path '%s'",p)
+                        end
+                        ownpath =  p
+                    end
+                    break
+                end
+            end
+        end
+        if not ownpath or ownpath == "" then
+            ownpath = "."
+            logs.report("fileio","forcing fallback ownpath .")
+        elseif trace_locating then
+            logs.report("fileio","using ownpath '%s'",ownpath)
+        end
+    end
+    resolvers.ownpath = ownpath
+    function resolvers.getownpath()
+        return resolvers.ownpath
+    end
+    return ownpath
+end
+
+local own_places = { "SELFAUTOLOC", "SELFAUTODIR", "SELFAUTOPARENT", "TEXMFCNF" }
+
+local function identify_own()
+    local ownpath = resolvers.getownpath() or dir.current()
+    local ie = instance.environment
+    if ownpath then
+        if resolvers.env('SELFAUTOLOC')    == "" then os.env['SELFAUTOLOC']    = file.collapse_path(ownpath) end
+        if resolvers.env('SELFAUTODIR')    == "" then os.env['SELFAUTODIR']    = file.collapse_path(ownpath .. "/..") end
+        if resolvers.env('SELFAUTOPARENT') == "" then os.env['SELFAUTOPARENT'] = file.collapse_path(ownpath .. "/../..") end
+    else
+        logs.report("fileio","error: unable to locate ownpath")
+        os.exit()
+    end
+    if resolvers.env('TEXMFCNF') == "" then os.env['TEXMFCNF'] = resolvers.cnfdefault end
+    if resolvers.env('TEXOS')    == "" then os.env['TEXOS']    = resolvers.env('SELFAUTODIR') end
+    if resolvers.env('TEXROOT')  == "" then os.env['TEXROOT']  = resolvers.env('SELFAUTOPARENT') end
+    if trace_locating then
+        for i=1,#own_places do
+            local v = own_places[i]
+            logs.report("fileio","variable '%s' set to '%s'",v,resolvers.env(v) or "unknown")
+        end
+    end
+    identify_own = function() end
+end
+
+function resolvers.identify_cnf()
+    if #instance.cnffiles == 0 then
+        -- fallback
+        identify_own()
+        -- the real search
+        resolvers.expand_variables()
+        local t = resolvers.split_path(resolvers.env('TEXMFCNF'))
+        t = expanded_path_from_list(t)
+        expand_vars(t) -- redundant
+        local function locate(filename,list)
+            for i=1,#t do
+                local ti = t[i]
+                local texmfcnf = file.collapse_path(file.join(ti,filename))
+                if lfs.isfile(texmfcnf) then
+                    list[#list+1] = texmfcnf
+                end
+            end
+        end
+        locate(resolvers.luaname,instance.luafiles)
+        locate(resolvers.cnfname,instance.cnffiles)
+    end
+end
+
+local function load_cnf_file(fname)
+    fname = resolvers.clean_path(fname)
+    local lname = file.replacesuffix(fname,'lua')
+    if lfs.isfile(lname) then
+        local dname = file.dirname(fname) -- fname ?
+        if not instance.configuration[dname] then
+            resolvers.load_data(dname,'configuration',lname and file.basename(lname))
+            instance.order[#instance.order+1] = instance.configuration[dname]
+        end
+    else
+        f = io.open(fname)
+        if f then
+            if trace_locating then
+                logs.report("fileio","loading configuration file %s", fname)
+            end
+            local line, data, n, k, v
+            local dname = file.dirname(fname)
+            if not instance.configuration[dname] then
+                instance.configuration[dname] = { }
+                instance.order[#instance.order+1] = instance.configuration[dname]
+            end
+            local data = instance.configuration[dname]
+            while true do
+                local line, n = f:read(), 0
+                if line then
+                    while true do -- join lines
+                        line, n = gsub(line,"\\%s*$", "")
+                        if n > 0 then
+                            line = line .. f:read()
+                        else
+                            break
+                        end
+                    end
+                    if not find(line,"^[%%#]") then
+                        local l = gsub(line,"%s*%%.*$","")
+                        local k, v = match(l,"%s*(.-)%s*=%s*(.-)%s*$")
+                        if k and v and not data[k] then
+                            v = gsub(v,"[%%#].*",'')
+                            data[k] = gsub(v,"~","$HOME")
+                            instance.kpsevars[k] = true
+                        end
+                    end
+                else
+                    break
+                end
+            end
+            f:close()
+        elseif trace_locating then
+            logs.report("fileio","skipping configuration file '%s'", fname)
+        end
+    end
+end
+
+local function collapse_cnf_data() -- potential optimization: pass start index (setup and configuration are shared)
+    local order = instance.order
+    for i=1,#order do
+        local c = order[i]
+        for k,v in next, c do
+            if not instance.variables[k] then
+                if instance.environment[k] then
+                    instance.variables[k] = instance.environment[k]
+                else
+                    instance.kpsevars[k] = true
+                    instance.variables[k] = resolvers.bare_variable(v)
+                end
+            end
+        end
+    end
+end
+
+function resolvers.load_cnf()
+    local function loadoldconfigdata()
+        local cnffiles = instance.cnffiles
+        for i=1,#cnffiles do
+            load_cnf_file(cnffiles[i])
+        end
+    end
+    -- instance.cnffiles contain complete names now !
+    -- we still use a funny mix of cnf and new but soon
+    -- we will switch to lua exclusively as we only use
+    -- the file to collect the tree roots
+    if #instance.cnffiles == 0 then
+        if trace_locating then
+            logs.report("fileio","no cnf files found (TEXMFCNF may not be set/known)")
+        end
+    else
+        local cnffiles = instance.cnffiles
+        instance.rootpath = cnffiles[1]
+        for k=1,#cnffiles do
+            instance.cnffiles[k] = file.collapse_path(cnffiles[k])
+        end
+        for i=1,3 do
+            instance.rootpath = file.dirname(instance.rootpath)
+        end
+        instance.rootpath = file.collapse_path(instance.rootpath)
+        if instance.diskcache and not instance.renewcache then
+            resolvers.loadoldconfig(instance.cnffiles)
+            if instance.loaderror then
+                loadoldconfigdata()
+                resolvers.saveoldconfig()
+            end
+        else
+            loadoldconfigdata()
+            if instance.renewcache then
+                resolvers.saveoldconfig()
+            end
+        end
+        collapse_cnf_data()
+    end
+    check_configuration()
+end
+
+function resolvers.load_lua()
+    if #instance.luafiles == 0 then
+        -- yet harmless
+    else
+        instance.rootpath = instance.luafiles[1]
+        local luafiles = instance.luafiles
+        for k=1,#luafiles do
+            instance.luafiles[k] = file.collapse_path(luafiles[k])
+        end
+        for i=1,3 do
+            instance.rootpath = file.dirname(instance.rootpath)
+        end
+        instance.rootpath = file.collapse_path(instance.rootpath)
+        resolvers.loadnewconfig()
+        collapse_cnf_data()
+    end
+    check_configuration()
+end
+
+-- database loading
+
+function resolvers.load_hash()
+    resolvers.locatelists()
+    if instance.diskcache and not instance.renewcache then
+        resolvers.loadfiles()
+        if instance.loaderror then
+            resolvers.loadlists()
+            resolvers.savefiles()
+        end
+    else
+        resolvers.loadlists()
+        if instance.renewcache then
+            resolvers.savefiles()
+        end
+    end
+end
+
+function resolvers.append_hash(type,tag,name)
+    if trace_locating then
+        logs.report("fileio","hash '%s' appended",tag)
+    end
+    insert(instance.hashes, { ['type']=type, ['tag']=tag, ['name']=name } )
+end
+
+function resolvers.prepend_hash(type,tag,name)
+    if trace_locating then
+        logs.report("fileio","hash '%s' prepended",tag)
+    end
+    insert(instance.hashes, 1, { ['type']=type, ['tag']=tag, ['name']=name } )
+end
+
+function resolvers.extend_texmf_var(specification) -- crap, we could better prepend the hash
+--  local t = resolvers.expanded_path_list('TEXMF') -- full expansion
+    local t = resolvers.split_path(resolvers.env('TEXMF'))
+    insert(t,1,specification)
+    local newspec = concat(t,";")
+    if instance.environment["TEXMF"] then
+        instance.environment["TEXMF"] = newspec
+    elseif instance.variables["TEXMF"] then
+        instance.variables["TEXMF"] = newspec
+    else
+        -- weird
+    end
+    resolvers.expand_variables()
+    reset_hashes()
+end
+
+-- locators
+
+function resolvers.locatelists()
+    local texmfpaths = resolvers.clean_path_list('TEXMF')
+    for i=1,#texmfpaths do
+        local path = texmfpaths[i]
+        if trace_locating then
+            logs.report("fileio","locating list of '%s'",path)
+        end
+        resolvers.locatedatabase(file.collapse_path(path))
+    end
+end
+
+function resolvers.locatedatabase(specification)
+    return resolvers.methodhandler('locators', specification)
+end
+
+function resolvers.locators.tex(specification)
+    if specification and specification ~= '' and lfs.isdir(specification) then
+        if trace_locating then
+            logs.report("fileio","tex locator '%s' found",specification)
+        end
+        resolvers.append_hash('file',specification,filename)
+    elseif trace_locating then
+        logs.report("fileio","tex locator '%s' not found",specification)
+    end
+end
+
+-- hashers
+
+function resolvers.hashdatabase(tag,name)
+    return resolvers.methodhandler('hashers',tag,name)
+end
+
+function resolvers.loadfiles()
+    instance.loaderror = false
+    instance.files = { }
+    if not instance.renewcache then
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            resolvers.hashdatabase(hash.tag,hash.name)
+            if instance.loaderror then break end
+        end
+    end
+end
+
+function resolvers.hashers.tex(tag,name)
+    resolvers.load_data(tag,'files')
+end
+
+-- generators:
+
+function resolvers.loadlists()
+    local hashes = instance.hashes
+    for i=1,#hashes do
+        resolvers.generatedatabase(hashes[i].tag)
+    end
+end
+
+function resolvers.generatedatabase(specification)
+    return resolvers.methodhandler('generators', specification)
+end
+
+-- starting with . or .. etc or funny char
+
+local weird = lpeg.P(".")^1 + lpeg.anywhere(lpeg.S("~`!#$%^&*()={}[]:;\"\'||<>,?\n\r\t"))
+
+--~ local l_forbidden = lpeg.S("~`!#$%^&*()={}[]:;\"\'||\\/<>,?\n\r\t")
+--~ local l_confusing = lpeg.P(" ")
+--~ local l_character = lpeg.patterns.utf8
+--~ local l_dangerous = lpeg.P(".")
+
+--~ local l_normal = (l_character - l_forbidden - l_confusing - l_dangerous) * (l_character - l_forbidden - l_confusing^2)^0 * lpeg.P(-1)
+--~ ----- l_normal = l_normal * lpeg.Cc(true) + lpeg.Cc(false)
+
+--~ local function test(str)
+--~     print(str,lpeg.match(l_normal,str))
+--~ end
+--~ test("ヒラギノ明朝 Pro W3")
+--~ test("..ヒラギノ明朝 Pro W3")
+--~ test(":ヒラギノ明朝 Pro W3;")
+--~ test("ヒラギノ明朝 /Pro W3;")
+--~ test("ヒラギノ明朝 Pro  W3")
+
+function resolvers.generators.tex(specification)
+    local tag = specification
+    if trace_locating then
+        logs.report("fileio","scanning path '%s'",specification)
+    end
+    instance.files[tag] = { }
+    local files = instance.files[tag]
+    local n, m, r = 0, 0, 0
+    local spec = specification .. '/'
+    local attributes = lfs.attributes
+    local directory = lfs.dir
+    local function action(path)
+        local full
+        if path then
+            full = spec .. path .. '/'
+        else
+            full = spec
+        end
+        for name in directory(full) do
+            if not lpegmatch(weird,name) then
+         -- if lpegmatch(l_normal,name) then
+                local mode = attributes(full..name,'mode')
+                if mode == 'file' then
+                    if path then
+                        n = n + 1
+                        local f = files[name]
+                        if f then
+                            if type(f) == 'string' then
+                                files[name] = { f, path }
+                            else
+                                f[#f+1] = path
+                            end
+                        else -- probably unique anyway
+                            files[name] = path
+                            local lower = lower(name)
+                            if name ~= lower then
+                                files["remap:"..lower] = name
+                                r = r + 1
+                            end
+                        end
+                    end
+                elseif mode == 'directory' then
+                    m = m + 1
+                    if path then
+                        action(path..'/'..name)
+                    else
+                        action(name)
+                    end
+                end
+            end
+        end
+    end
+    action()
+    if trace_locating then
+        logs.report("fileio","%s files found on %s directories with %s uppercase remappings",n,m,r)
+    end
+end
+
+-- savers, todo
+
+function resolvers.savefiles()
+    resolvers.save_data('files')
+end
+
+-- A config (optionally) has the paths split in tables. Internally
+-- we join them and split them after the expansion has taken place. This
+-- is more convenient.
+
+--~ local checkedsplit = string.checkedsplit
+
+local cache = { }
+
+local splitter = lpeg.Ct(lpeg.splitat(lpeg.S(os.type == "windows" and ";" or ":;")))
+
+local function split_kpse_path(str) -- beware, this can be either a path or a {specification}
+    local found = cache[str]
+    if not found then
+        if str == "" then
+            found = { }
+        else
+            str = gsub(str,"\\","/")
+--~             local split = (find(str,";") and checkedsplit(str,";")) or checkedsplit(str,io.pathseparator)
+local split = lpegmatch(splitter,str)
+            found = { }
+            for i=1,#split do
+                local s = split[i]
+                if not find(s,"^{*unset}*") then
+                    found[#found+1] = s
+                end
+            end
+            if trace_expansions then
+                logs.report("fileio","splitting path specification '%s'",str)
+                for k=1,#found do
+                    logs.report("fileio","% 4i: %s",k,found[k])
+                end
+            end
+            cache[str] = found
+        end
+    end
+    return found
+end
+
+resolvers.split_kpse_path = split_kpse_path
+
+function resolvers.splitconfig()
+    for i=1,#instance do
+        local c = instance[i]
+        for k,v in next, c do
+            if type(v) == 'string' then
+                local t = split_kpse_path(v)
+                if #t > 1 then
+                    c[k] = t
+                end
+            end
+        end
+    end
+end
+
+function resolvers.joinconfig()
+    local order = instance.order
+    for i=1,#order do
+        local c = order[i]
+        for k,v in next, c do -- indexed?
+            if type(v) == 'table' then
+                c[k] = file.join_path(v)
+            end
+        end
+    end
+end
+
+function resolvers.split_path(str)
+    if type(str) == 'table' then
+        return str
+    else
+        return split_kpse_path(str)
+    end
+end
+
+function resolvers.join_path(str)
+    if type(str) == 'table' then
+        return file.join_path(str)
+    else
+        return str
+    end
+end
+
+function resolvers.splitexpansions()
+    local ie = instance.expansions
+    for k,v in next, ie do
+        local t, h, p = { }, { }, split_kpse_path(v)
+        for kk=1,#p do
+            local vv = p[kk]
+            if vv ~= "" and not h[vv] then
+                t[#t+1] = vv
+                h[vv] = true
+            end
+        end
+        if #t > 1 then
+            ie[k] = t
+        else
+            ie[k] = t[1]
+        end
+    end
+end
+
+-- end of split/join code
+
+function resolvers.saveoldconfig()
+    resolvers.splitconfig()
+    resolvers.save_data('configuration')
+    resolvers.joinconfig()
+end
+
+resolvers.configbanner = [[
+-- This is a Luatex configuration file created by 'luatools.lua' or
+-- 'luatex.exe' directly. For comment, suggestions and questions you can
+-- contact the ConTeXt Development Team. This configuration file is
+-- not copyrighted. [HH & TH]
+]]
+
+function resolvers.serialize(files)
+    -- This version is somewhat optimized for the kind of
+    -- tables that we deal with, so it's much faster than
+    -- the generic serializer. This makes sense because
+    -- luatools and mtxtools are called frequently. Okay,
+    -- we pay a small price for properly tabbed tables.
+    local t = { }
+    local function dump(k,v,m) -- could be moved inline
+        if type(v) == 'string' then
+            return m .. "['" .. k .. "']='" .. v .. "',"
+        elseif #v == 1 then
+            return m .. "['" .. k .. "']='" .. v[1] .. "',"
+        else
+            return m .. "['" .. k .. "']={'" .. concat(v,"','").. "'},"
+        end
+    end
+    t[#t+1] = "return {"
+    if instance.sortdata then
+	local sortedfiles = sortedkeys(files)
+	for i=1,#sortedfiles do
+	    local k = sortedfiles[i]
+            local fk  = files[k]
+            if type(fk) == 'table' then
+                t[#t+1] = "\t['" .. k .. "']={"
+		local sortedfk = sortedkeys(fk)
+        	for j=1,#sortedfk do
+                    local kk = sortedfk[j]
+                    t[#t+1] = dump(kk,fk[kk],"\t\t")
+                end
+                t[#t+1] = "\t},"
+            else
+                t[#t+1] = dump(k,fk,"\t")
+            end
+        end
+    else
+        for k, v in next, files do
+            if type(v) == 'table' then
+                t[#t+1] = "\t['" .. k .. "']={"
+                for kk,vv in next, v do
+                    t[#t+1] = dump(kk,vv,"\t\t")
+                end
+                t[#t+1] = "\t},"
+            else
+                t[#t+1] = dump(k,v,"\t")
+            end
+        end
+    end
+    t[#t+1] = "}"
+    return concat(t,"\n")
+end
+
+local data_state = { }
+
+function resolvers.data_state()
+    return data_state or { }
+end
+
+function resolvers.save_data(dataname, makename) -- untested without cache overload
+    for cachename, files in next, instance[dataname] do
+        local name = (makename or file.join)(cachename,dataname)
+        local luaname, lucname = name .. ".lua", name .. ".luc"
+        if trace_locating then
+            logs.report("fileio","preparing '%s' for '%s'",dataname,cachename)
+        end
+        for k, v in next, files do
+            if type(v) == "table" and #v == 1 then
+                files[k] = v[1]
+            end
+        end
+        local data = {
+            type    = dataname,
+            root    = cachename,
+            version = resolvers.cacheversion,
+            date    = os.date("%Y-%m-%d"),
+            time    = os.date("%H:%M:%S"),
+            content = files,
+            uuid    = os.uuid(),
+        }
+        local ok = io.savedata(luaname,resolvers.serialize(data))
+        if ok then
+            if trace_locating then
+                logs.report("fileio","'%s' saved in '%s'",dataname,luaname)
+            end
+            if utils.lua.compile(luaname,lucname,false,true) then -- no cleanup but strip
+                if trace_locating then
+                    logs.report("fileio","'%s' compiled to '%s'",dataname,lucname)
+                end
+            else
+                if trace_locating then
+                    logs.report("fileio","compiling failed for '%s', deleting file '%s'",dataname,lucname)
+                end
+                os.remove(lucname)
+            end
+        elseif trace_locating then
+            logs.report("fileio","unable to save '%s' in '%s' (access error)",dataname,luaname)
+        end
+    end
+end
+
+function resolvers.load_data(pathname,dataname,filename,makename) -- untested without cache overload
+    filename = ((not filename or (filename == "")) and dataname) or filename
+    filename = (makename and makename(dataname,filename)) or file.join(pathname,filename)
+    local blob = loadfile(filename .. ".luc") or loadfile(filename .. ".lua")
+    if blob then
+        local data = blob()
+        if data and data.content and data.type == dataname and data.version == resolvers.cacheversion then
+            data_state[#data_state+1] = data.uuid
+            if trace_locating then
+                logs.report("fileio","loading '%s' for '%s' from '%s'",dataname,pathname,filename)
+            end
+            instance[dataname][pathname] = data.content
+        else
+            if trace_locating then
+                logs.report("fileio","skipping '%s' for '%s' from '%s'",dataname,pathname,filename)
+            end
+            instance[dataname][pathname] = { }
+            instance.loaderror = true
+        end
+    elseif trace_locating then
+        logs.report("fileio","skipping '%s' for '%s' from '%s'",dataname,pathname,filename)
+    end
+end
+
+-- some day i'll use the nested approach, but not yet (actually we even drop
+-- engine/progname support since we have only luatex now)
+--
+-- first texmfcnf.lua files are located, next the cached texmf.cnf files
+--
+-- return {
+--     TEXMFBOGUS = 'effe checken of dit werkt',
+-- }
+
+function resolvers.resetconfig()
+    identify_own()
+    instance.configuration, instance.setup, instance.order, instance.loaderror = { }, { }, { }, false
+end
+
+function resolvers.loadnewconfig()
+    local luafiles = instance.luafiles
+    for i=1,#luafiles do
+        local cnf = luafiles[i]
+        local pathname = file.dirname(cnf)
+        local filename = file.join(pathname,resolvers.luaname)
+        local blob = loadfile(filename)
+        if blob then
+            local data = blob()
+            if data then
+                if trace_locating then
+                    logs.report("fileio","loading configuration file '%s'",filename)
+                end
+                if true then
+                    -- flatten to variable.progname
+                    local t = { }
+                    for k, v in next, data do -- v = progname
+                        if type(v) == "string" then
+                            t[k] = v
+                        else
+                            for kk, vv in next, v do -- vv = variable
+                                if type(vv) == "string" then
+                                    t[vv.."."..v] = kk
+                                end
+                            end
+                        end
+                    end
+                    instance['setup'][pathname] = t
+                else
+                    instance['setup'][pathname] = data
+                end
+            else
+                if trace_locating then
+                    logs.report("fileio","skipping configuration file '%s'",filename)
+                end
+                instance['setup'][pathname] = { }
+                instance.loaderror = true
+            end
+        elseif trace_locating then
+            logs.report("fileio","skipping configuration file '%s'",filename)
+        end
+        instance.order[#instance.order+1] = instance.setup[pathname]
+        if instance.loaderror then break end
+    end
+end
+
+function resolvers.loadoldconfig()
+    if not instance.renewcache then
+        local cnffiles = instance.cnffiles
+        for i=1,#cnffiles do
+            local cnf = cnffiles[i]
+            local dname = file.dirname(cnf)
+            resolvers.load_data(dname,'configuration')
+            instance.order[#instance.order+1] = instance.configuration[dname]
+            if instance.loaderror then break end
+        end
+    end
+    resolvers.joinconfig()
+end
+
+function resolvers.expand_variables()
+    local expansions, environment, variables = { }, instance.environment, instance.variables
+    local env = resolvers.env
+    instance.expansions = expansions
+    if instance.engine   ~= "" then environment['engine']   = instance.engine   end
+    if instance.progname ~= "" then environment['progname'] = instance.progname end
+    for k,v in next, environment do
+        local a, b = match(k,"^(%a+)%_(.*)%s*$")
+        if a and b then
+            expansions[a..'.'..b] = v
+        else
+            expansions[k] = v
+        end
+    end
+    for k,v in next, environment do -- move environment to expansions
+        if not expansions[k] then expansions[k] = v end
+    end
+    for k,v in next, variables do -- move variables to expansions
+        if not expansions[k] then expansions[k] = v end
+    end
+    local busy = false
+    local function resolve(a)
+        busy = true
+        return expansions[a] or env(a)
+    end
+    while true do
+        busy = false
+        for k,v in next, expansions do
+            local s, n = gsub(v,"%$([%a%d%_%-]+)",resolve)
+            local s, m = gsub(s,"%$%{([%a%d%_%-]+)%}",resolve)
+            if n > 0 or m > 0 then
+                expansions[k]= s
+            end
+        end
+        if not busy then break end
+    end
+    for k,v in next, expansions do
+        expansions[k] = gsub(v,"\\", '/')
+    end
+end
+
+function resolvers.variable(name)
+    return entry(instance.variables,name)
+end
+
+function resolvers.expansion(name)
+    return entry(instance.expansions,name)
+end
+
+function resolvers.is_variable(name)
+    return is_entry(instance.variables,name)
+end
+
+function resolvers.is_expansion(name)
+    return is_entry(instance.expansions,name)
+end
+
+function resolvers.unexpanded_path_list(str)
+    local pth = resolvers.variable(str)
+    local lst = resolvers.split_path(pth)
+    return expanded_path_from_list(lst)
+end
+
+function resolvers.unexpanded_path(str)
+    return file.join_path(resolvers.unexpanded_path_list(str))
+end
+
+do -- no longer needed
+
+    local done = { }
+
+    function resolvers.reset_extra_path()
+        local ep = instance.extra_paths
+        if not ep then
+            ep, done = { }, { }
+            instance.extra_paths = ep
+        elseif #ep > 0 then
+            instance.lists, done = { }, { }
+        end
+    end
+
+    function resolvers.register_extra_path(paths,subpaths)
+        local ep = instance.extra_paths or { }
+        local n = #ep
+        if paths and paths ~= "" then
+            if subpaths and subpaths ~= "" then
+                for p in gmatch(paths,"[^,]+") do
+                    -- we gmatch each step again, not that fast, but used seldom
+                    for s in gmatch(subpaths,"[^,]+") do
+                        local ps = p .. "/" .. s
+                        if not done[ps] then
+                            ep[#ep+1] = resolvers.clean_path(ps)
+                            done[ps] = true
+                        end
+                    end
+                end
+            else
+                for p in gmatch(paths,"[^,]+") do
+                    if not done[p] then
+                        ep[#ep+1] = resolvers.clean_path(p)
+                        done[p] = true
+                    end
+                end
+            end
+        elseif subpaths and subpaths ~= "" then
+            for i=1,n do
+                -- we gmatch each step again, not that fast, but used seldom
+                for s in gmatch(subpaths,"[^,]+") do
+                    local ps = ep[i] .. "/" .. s
+                    if not done[ps] then
+                        ep[#ep+1] = resolvers.clean_path(ps)
+                        done[ps] = true
+                    end
+                end
+            end
+        end
+        if #ep > 0 then
+            instance.extra_paths = ep -- register paths
+        end
+        if #ep > n then
+            instance.lists = { } -- erase the cache
+        end
+    end
+
+end
+
+local function made_list(instance,list)
+    local ep = instance.extra_paths
+    if not ep or #ep == 0 then
+        return list
+    else
+        local done, new = { }, { }
+        -- honour . .. ../.. but only when at the start
+        for k=1,#list do
+            local v = list[k]
+            if not done[v] then
+                if find(v,"^[%.%/]$") then
+                    done[v] = true
+                    new[#new+1] = v
+                else
+                    break
+                end
+            end
+        end
+        -- first the extra paths
+        for k=1,#ep do
+            local v = ep[k]
+            if not done[v] then
+                done[v] = true
+                new[#new+1] = v
+            end
+        end
+        -- next the formal paths
+        for k=1,#list do
+            local v = list[k]
+            if not done[v] then
+                done[v] = true
+                new[#new+1] = v
+            end
+        end
+        return new
+    end
+end
+
+function resolvers.clean_path_list(str)
+    local t = resolvers.expanded_path_list(str)
+    if t then
+        for i=1,#t do
+            t[i] = file.collapse_path(resolvers.clean_path(t[i]))
+        end
+    end
+    return t
+end
+
+function resolvers.expand_path(str)
+    return file.join_path(resolvers.expanded_path_list(str))
+end
+
+function resolvers.expanded_path_list(str)
+    if not str then
+        return ep or { } -- ep ?
+    elseif instance.savelists then
+        -- engine+progname hash
+        str = gsub(str,"%$","")
+        if not instance.lists[str] then -- cached
+            local lst = made_list(instance,resolvers.split_path(resolvers.expansion(str)))
+            instance.lists[str] = expanded_path_from_list(lst)
+        end
+        return instance.lists[str]
+    else
+        local lst = resolvers.split_path(resolvers.expansion(str))
+        return made_list(instance,expanded_path_from_list(lst))
+    end
+end
+
+function resolvers.expanded_path_list_from_var(str) -- brrr
+    local tmp = resolvers.var_of_format_or_suffix(gsub(str,"%$",""))
+    if tmp ~= "" then
+        return resolvers.expanded_path_list(tmp)
+    else
+        return resolvers.expanded_path_list(str)
+    end
+end
+
+function resolvers.expand_path_from_var(str)
+    return file.join_path(resolvers.expanded_path_list_from_var(str))
+end
+
+function resolvers.format_of_var(str)
+    return formats[str] or formats[alternatives[str]] or ''
+end
+function resolvers.format_of_suffix(str)
+    return suffixmap[file.extname(str)] or 'tex'
+end
+
+function resolvers.variable_of_format(str)
+    return formats[str] or formats[alternatives[str]] or ''
+end
+
+function resolvers.var_of_format_or_suffix(str)
+    local v = formats[str]
+    if v then
+        return v
+    end
+    v = formats[alternatives[str]]
+    if v then
+        return v
+    end
+    v = suffixmap[file.extname(str)]
+    if v then
+        return formats[isf]
+    end
+    return ''
+end
+
+function resolvers.expand_braces(str) -- output variable and brace expansion of STRING
+    local ori = resolvers.variable(str)
+    local pth = expanded_path_from_list(resolvers.split_path(ori))
+    return file.join_path(pth)
+end
+
+resolvers.isreadable = { }
+
+function resolvers.isreadable.file(name)
+    local readable = lfs.isfile(name) -- brrr
+    if trace_detail then
+        if readable then
+            logs.report("fileio","file '%s' is readable",name)
+        else
+            logs.report("fileio","file '%s' is not readable", name)
+        end
+    end
+    return readable
+end
+
+resolvers.isreadable.tex = resolvers.isreadable.file
+
+-- name
+-- name/name
+
+local function collect_files(names)
+    local filelist = { }
+    for k=1,#names do
+        local fname = names[k]
+        if trace_detail then
+            logs.report("fileio","checking name '%s'",fname)
+        end
+        local bname = file.basename(fname)
+        local dname = file.dirname(fname)
+        if dname == "" or find(dname,"^%.") then
+            dname = false
+        else
+            dname = "/" .. dname .. "$"
+        end
+        local hashes = instance.hashes
+        for h=1,#hashes do
+            local hash = hashes[h]
+            local blobpath = hash.tag
+            local files = blobpath and instance.files[blobpath]
+            if files then
+                if trace_detail then
+                    logs.report("fileio","deep checking '%s' (%s)",blobpath,bname)
+                end
+                local blobfile = files[bname]
+                if not blobfile then
+                    local rname = "remap:"..bname
+                    blobfile = files[rname]
+                    if blobfile then
+                        bname = files[rname]
+                        blobfile = files[bname]
+                    end
+                end
+                if blobfile then
+                    if type(blobfile) == 'string' then
+                        if not dname or find(blobfile,dname) then
+                            filelist[#filelist+1] = {
+                                hash.type,
+                                file.join(blobpath,blobfile,bname), -- search
+                                resolvers.concatinators[hash.type](blobpath,blobfile,bname) -- result
+                            }
+                        end
+                    else
+                        for kk=1,#blobfile do
+                            local vv = blobfile[kk]
+                            if not dname or find(vv,dname) then
+                                filelist[#filelist+1] = {
+                                    hash.type,
+                                    file.join(blobpath,vv,bname), -- search
+                                    resolvers.concatinators[hash.type](blobpath,vv,bname) -- result
+                                }
+                            end
+                        end
+                    end
+                end
+            elseif trace_locating then
+                logs.report("fileio","no match in '%s' (%s)",blobpath,bname)
+            end
+        end
+    end
+    if #filelist > 0 then
+        return filelist
+    else
+        return nil
+    end
+end
+
+function resolvers.suffix_of_format(str)
+    if suffixes[str] then
+        return suffixes[str][1]
+    else
+        return ""
+    end
+end
+
+function resolvers.suffixes_of_format(str)
+    if suffixes[str] then
+        return suffixes[str]
+    else
+        return {}
+    end
+end
+
+function resolvers.register_in_trees(name)
+    if not find(name,"^%.") then
+        instance.foundintrees[name] = (instance.foundintrees[name] or 0) + 1 -- maybe only one
+    end
+end
+
+-- split the next one up for readability (bu this module needs a cleanup anyway)
+
+local function can_be_dir(name) -- can become local
+    local fakepaths = instance.fakepaths
+    if not fakepaths[name] then
+        if lfs.isdir(name) then
+            fakepaths[name] = 1 -- directory
+        else
+            fakepaths[name] = 2 -- no directory
+        end
+    end
+    return (fakepaths[name] == 1)
+end
+
+local function collect_instance_files(filename,collected) -- todo : plugin (scanners, checkers etc)
+    local result = collected or { }
+    local stamp  = nil
+    filename = file.collapse_path(filename)
+    -- speed up / beware: format problem
+    if instance.remember then
+        stamp = filename .. "--" .. instance.engine .. "--" .. instance.progname .. "--" .. instance.format
+        if instance.found[stamp] then
+            if trace_locating then
+                logs.report("fileio","remembering file '%s'",filename)
+            end
+            return instance.found[stamp]
+        end
+    end
+    if not dangerous[instance.format or "?"] then
+        if resolvers.isreadable.file(filename) then
+            if trace_detail then
+                logs.report("fileio","file '%s' found directly",filename)
+            end
+            instance.found[stamp] = { filename }
+            return { filename }
+        end
+    end
+    if find(filename,'%*') then
+        if trace_locating then
+            logs.report("fileio","checking wildcard '%s'", filename)
+        end
+        result = resolvers.find_wildcard_files(filename)
+    elseif file.is_qualified_path(filename) then
+        if resolvers.isreadable.file(filename) then
+            if trace_locating then
+                logs.report("fileio","qualified name '%s'", filename)
+            end
+            result = { filename }
+        else
+            local forcedname, ok, suffix = "", false, file.extname(filename)
+            if suffix == "" then -- why
+                if instance.format == "" then
+                    forcedname = filename .. ".tex"
+                    if resolvers.isreadable.file(forcedname) then
+                        if trace_locating then
+                            logs.report("fileio","no suffix, forcing standard filetype 'tex'")
+                        end
+                        result, ok = { forcedname }, true
+                    end
+                else
+                    local suffixes = resolvers.suffixes_of_format(instance.format)
+                    for _, s in next, suffixes do
+                        forcedname = filename .. "." .. s
+                        if resolvers.isreadable.file(forcedname) then
+                            if trace_locating then
+                                logs.report("fileio","no suffix, forcing format filetype '%s'", s)
+                            end
+                            result, ok = { forcedname }, true
+                            break
+                        end
+                    end
+                end
+            end
+            if not ok and suffix ~= "" then
+                -- try to find in tree (no suffix manipulation), here we search for the
+                -- matching last part of the name
+                local basename = file.basename(filename)
+                local pattern = gsub(filename .. "$","([%.%-])","%%%1")
+                local savedformat = instance.format
+                local format = savedformat or ""
+                if format == "" then
+                    instance.format = resolvers.format_of_suffix(suffix)
+                end
+                if not format then
+                    instance.format = "othertextfiles" -- kind of everything, maybe texinput is better
+                end
+                --
+                if basename ~= filename then
+                    local resolved = collect_instance_files(basename)
+                    if #result == 0 then
+                        local lowered = lower(basename)
+                        if filename ~= lowered then
+                            resolved = collect_instance_files(lowered)
+                        end
+                    end
+                    resolvers.format = savedformat
+                    --
+                    for r=1,#resolved do
+                        local rr = resolved[r]
+                        if find(rr,pattern) then
+                            result[#result+1], ok = rr, true
+                        end
+                    end
+                end
+                -- a real wildcard:
+                --
+                -- if not ok then
+                --     local filelist = collect_files({basename})
+                --     for f=1,#filelist do
+                --         local ff = filelist[f][3] or ""
+                --         if find(ff,pattern) then
+                --             result[#result+1], ok = ff, true
+                --         end
+                --     end
+                -- end
+            end
+            if not ok and trace_locating then
+                logs.report("fileio","qualified name '%s'", filename)
+            end
+        end
+    else
+        -- search spec
+        local filetype, extra, done, wantedfiles, ext = '', nil, false, { }, file.extname(filename)
+        if ext == "" then
+            if not instance.force_suffixes then
+                wantedfiles[#wantedfiles+1] = filename
+            end
+        else
+            wantedfiles[#wantedfiles+1] = filename
+        end
+        if instance.format == "" then
+            if ext == "" then
+                local forcedname = filename .. '.tex'
+                wantedfiles[#wantedfiles+1] = forcedname
+                filetype = resolvers.format_of_suffix(forcedname)
+                if trace_locating then
+                    logs.report("fileio","forcing filetype '%s'",filetype)
+                end
+            else
+                filetype = resolvers.format_of_suffix(filename)
+                if trace_locating then
+                    logs.report("fileio","using suffix based filetype '%s'",filetype)
+                end
+            end
+        else
+            if ext == "" then
+                local suffixes = resolvers.suffixes_of_format(instance.format)
+                for _, s in next, suffixes do
+                    wantedfiles[#wantedfiles+1] = filename .. "." .. s
+                end
+            end
+            filetype = instance.format
+            if trace_locating then
+                logs.report("fileio","using given filetype '%s'",filetype)
+            end
+        end
+        local typespec = resolvers.variable_of_format(filetype)
+        local pathlist = resolvers.expanded_path_list(typespec)
+        if not pathlist or #pathlist == 0 then
+            -- no pathlist, access check only / todo == wildcard
+            if trace_detail then
+                logs.report("fileio","checking filename '%s', filetype '%s', wanted files '%s'",filename, filetype or '?',concat(wantedfiles," | "))
+            end
+            for k=1,#wantedfiles do
+                local fname = wantedfiles[k]
+                if fname and resolvers.isreadable.file(fname) then
+                    filename, done = fname, true
+                    result[#result+1] = file.join('.',fname)
+                    break
+                end
+            end
+            -- this is actually 'other text files' or 'any' or 'whatever'
+            local filelist = collect_files(wantedfiles)
+            local fl = filelist and filelist[1]
+            if fl then
+                filename = fl[3]
+                result[#result+1] = filename
+                done = true
+            end
+        else
+            -- list search
+            local filelist = collect_files(wantedfiles)
+            local dirlist = { }
+            if filelist then
+                for i=1,#filelist do
+                    dirlist[i] = file.dirname(filelist[i][2]) .. "/"
+                end
+            end
+            if trace_detail then
+                logs.report("fileio","checking filename '%s'",filename)
+            end
+            -- a bit messy ... esp the doscan setting here
+            local doscan
+            for k=1,#pathlist do
+                local path = pathlist[k]
+                if find(path,"^!!") then doscan  = false else doscan  = true  end
+                local pathname = gsub(path,"^!+", '')
+                done = false
+                -- using file list
+                if filelist then
+                    local expression
+                    -- compare list entries with permitted pattern -- /xx /xx//
+                    if not find(pathname,"/$") then
+                        expression = pathname .. "/"
+                    else
+                        expression = pathname
+                    end
+                    expression = gsub(expression,"([%-%.])","%%%1") -- this also influences
+                    expression = gsub(expression,"//+$", '/.*')     -- later usage of pathname
+                    expression = gsub(expression,"//", '/.-/')      -- not ok for /// but harmless
+                    expression = "^" .. expression .. "$"
+                    if trace_detail then
+                        logs.report("fileio","using pattern '%s' for path '%s'",expression,pathname)
+                    end
+                    for k=1,#filelist do
+                        local fl = filelist[k]
+                        local f = fl[2]
+                        local d = dirlist[k]
+                        if find(d,expression) then
+                            --- todo, test for readable
+                            result[#result+1] = fl[3]
+                            resolvers.register_in_trees(f) -- for tracing used files
+                            done = true
+                            if instance.allresults then
+                                if trace_detail then
+                                    logs.report("fileio","match in hash for file '%s' on path '%s', continue scanning",f,d)
+                                end
+                            else
+                                if trace_detail then
+                                    logs.report("fileio","match in hash for file '%s' on path '%s', quit scanning",f,d)
+                                end
+                                break
+                            end
+                        elseif trace_detail then
+                            logs.report("fileio","no match in hash for file '%s' on path '%s'",f,d)
+                        end
+                    end
+                end
+                if not done and doscan then
+                    -- check if on disk / unchecked / does not work at all / also zips
+                    if resolvers.splitmethod(pathname).scheme == 'file' then -- ?
+                        local pname = gsub(pathname,"%.%*$",'')
+                        if not find(pname,"%*") then
+                            local ppname = gsub(pname,"/+$","")
+                            if can_be_dir(ppname) then
+                                for k=1,#wantedfiles do
+                                    local w = wantedfiles[k]
+                                    local fname = file.join(ppname,w)
+                                    if resolvers.isreadable.file(fname) then
+                                        if trace_detail then
+                                            logs.report("fileio","found '%s' by scanning",fname)
+                                        end
+                                        result[#result+1] = fname
+                                        done = true
+                                        if not instance.allresults then break end
+                                    end
+                                end
+                            else
+                                -- no access needed for non existing path, speedup (esp in large tree with lots of fake)
+                            end
+                        end
+                    end
+                end
+                if not done and doscan then
+                    -- todo: slow path scanning
+                end
+                if done and not instance.allresults then break end
+            end
+        end
+    end
+    for k=1,#result do
+        result[k] = file.collapse_path(result[k])
+    end
+    if instance.remember then
+        instance.found[stamp] = result
+    end
+    return result
+end
+
+if not resolvers.concatinators  then resolvers.concatinators = { } end
+
+resolvers.concatinators.tex  = file.join
+resolvers.concatinators.file = resolvers.concatinators.tex
+
+function resolvers.find_files(filename,filetype,mustexist)
+    if type(mustexist) == boolean then
+        -- all set
+    elseif type(filetype) == 'boolean' then
+        filetype, mustexist = nil, false
+    elseif type(filetype) ~= 'string' then
+        filetype, mustexist = nil, false
+    end
+    instance.format = filetype or ''
+    local result = collect_instance_files(filename)
+    if #result == 0 then
+        local lowered = lower(filename)
+        if filename ~= lowered then
+            return collect_instance_files(lowered)
+        end
+    end
+    instance.format = ''
+    return result
+end
+
+function resolvers.find_file(filename,filetype,mustexist)
+    return (resolvers.find_files(filename,filetype,mustexist)[1] or "")
+end
+
+function resolvers.find_given_files(filename)
+    local bname, result = file.basename(filename), { }
+    local hashes = instance.hashes
+    for k=1,#hashes do
+        local hash = hashes[k]
+        local files = instance.files[hash.tag] or { }
+        local blist = files[bname]
+        if not blist then
+            local rname = "remap:"..bname
+            blist = files[rname]
+            if blist then
+                bname = files[rname]
+                blist = files[bname]
+            end
+        end
+        if blist then
+            if type(blist) == 'string' then
+                result[#result+1] = resolvers.concatinators[hash.type](hash.tag,blist,bname) or ""
+                if not instance.allresults then break end
+            else
+                for kk=1,#blist do
+                    local vv = blist[kk]
+                    result[#result+1] = resolvers.concatinators[hash.type](hash.tag,vv,bname) or ""
+                    if not instance.allresults then break end
+                end
+            end
+        end
+    end
+    return result
+end
+
+function resolvers.find_given_file(filename)
+    return (resolvers.find_given_files(filename)[1] or "")
+end
+
+local function doit(path,blist,bname,tag,kind,result,allresults)
+    local done = false
+    if blist and kind then
+        if type(blist) == 'string' then
+            -- make function and share code
+            if find(lower(blist),path) then
+                result[#result+1] = resolvers.concatinators[kind](tag,blist,bname) or ""
+                done = true
+            end
+        else
+            for kk=1,#blist do
+                local vv = blist[kk]
+                if find(lower(vv),path) then
+                    result[#result+1] = resolvers.concatinators[kind](tag,vv,bname) or ""
+                    done = true
+                    if not allresults then break end
+                end
+            end
+        end
+    end
+    return done
+end
+
+function resolvers.find_wildcard_files(filename) -- todo: remap:
+    local result = { }
+    local bname, dname = file.basename(filename), file.dirname(filename)
+    local path = gsub(dname,"^*/","")
+    path = gsub(path,"*",".*")
+    path = gsub(path,"-","%%-")
+    if dname == "" then
+        path = ".*"
+    end
+    local name = bname
+    name = gsub(name,"*",".*")
+    name = gsub(name,"-","%%-")
+    path = lower(path)
+    name = lower(name)
+    local files, allresults, done = instance.files, instance.allresults, false
+    if find(name,"%*") then
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            local tag, kind = hash.tag, hash.type
+            for kk, hh in next, files[hash.tag] do
+                if not find(kk,"^remap:") then
+                    if find(lower(kk),name) then
+                        if doit(path,hh,kk,tag,kind,result,allresults) then done = true end
+                        if done and not allresults then break end
+                    end
+                end
+            end
+        end
+    else
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            local tag, kind = hash.tag, hash.type
+            if doit(path,files[tag][bname],bname,tag,kind,result,allresults) then done = true end
+            if done and not allresults then break end
+        end
+    end
+    -- we can consider also searching the paths not in the database, but then
+    -- we end up with a messy search (all // in all path specs)
+    return result
+end
+
+function resolvers.find_wildcard_file(filename)
+    return (resolvers.find_wildcard_files(filename)[1] or "")
+end
+
+-- main user functions
+
+function resolvers.automount()
+    -- implemented later
+end
+
+function resolvers.load(option)
+    statistics.starttiming(instance)
+    resolvers.resetconfig()
+    resolvers.identify_cnf()
+    resolvers.load_lua() -- will become the new method
+    resolvers.expand_variables()
+    resolvers.load_cnf() -- will be skipped when we have a lua file
+    resolvers.expand_variables()
+    if option ~= "nofiles" then
+        resolvers.load_hash()
+        resolvers.automount()
+    end
+    statistics.stoptiming(instance)
+end
+
+function resolvers.for_files(command, files, filetype, mustexist)
+    if files and #files > 0 then
+        local function report(str)
+            if trace_locating then
+                logs.report("fileio",str) -- has already verbose
+            else
+                print(str)
+            end
+        end
+        if trace_locating then
+            report('') -- ?
+        end
+        for f=1,#files do
+            local file = files[f]
+            local result = command(file,filetype,mustexist)
+            if type(result) == 'string' then
+                report(result)
+            else
+                for i=1,#result do
+                    report(result[i]) -- could be unpack
+                end
+            end
+        end
+    end
+end
+
+-- strtab
+
+resolvers.var_value  = resolvers.variable   -- output the value of variable $STRING.
+resolvers.expand_var = resolvers.expansion  -- output variable expansion of STRING.
+
+function resolvers.show_path(str)     -- output search path for file type NAME
+    return file.join_path(resolvers.expanded_path_list(resolvers.format_of_var(str)))
+end
+
+-- resolvers.find_file(filename)
+-- resolvers.find_file(filename, filetype, mustexist)
+-- resolvers.find_file(filename, mustexist)
+-- resolvers.find_file(filename, filetype)
+
+function resolvers.register_file(files, name, path)
+    if files[name] then
+        if type(files[name]) == 'string' then
+            files[name] = { files[name], path }
+        else
+            files[name] = path
+        end
+    else
+        files[name] = path
+    end
+end
+
+function resolvers.splitmethod(filename)
+    if not filename then
+        return { } -- safeguard
+    elseif type(filename) == "table" then
+        return filename -- already split
+    elseif not find(filename,"://") then
+        return { scheme="file", path = filename, original=filename } -- quick hack
+    else
+        return url.hashed(filename)
+    end
+end
+
+function table.sequenced(t,sep) -- temp here
+    local s = { }
+    for k, v in next, t do -- indexed?
+        s[#s+1] = k .. "=" .. tostring(v)
+    end
+    return concat(s, sep or " | ")
+end
+
+function resolvers.methodhandler(what, filename, filetype) -- ...
+    filename = file.collapse_path(filename)
+    local specification = (type(filename) == "string" and resolvers.splitmethod(filename)) or filename -- no or { }, let it bomb
+    local scheme = specification.scheme
+    if resolvers[what][scheme] then
+        if trace_locating then
+            logs.report("fileio","handler '%s' -> '%s' -> '%s'",specification.original,what,table.sequenced(specification))
+        end
+        return resolvers[what][scheme](filename,filetype) -- todo: specification
+    else
+        return resolvers[what].tex(filename,filetype) -- todo: specification
+    end
+end
+
+function resolvers.clean_path(str)
+    if str then
+        str = gsub(str,"\\","/")
+        str = gsub(str,"^!+","")
+        str = gsub(str,"^~",resolvers.homedir)
+        return str
+    else
+        return nil
+    end
+end
+
+function resolvers.do_with_path(name,func)
+    local pathlist = resolvers.expanded_path_list(name)
+    for i=1,#pathlist do
+        func("^"..resolvers.clean_path(pathlist[i]))
+    end
+end
+
+function resolvers.do_with_var(name,func)
+    func(expanded_var(name))
+end
+
+function resolvers.with_files(pattern,handle)
+    local hashes = instance.hashes
+    for i=1,#hashes do
+        local hash = hashes[i]
+        local blobpath = hash.tag
+        local blobtype = hash.type
+        if blobpath then
+            local files = instance.files[blobpath]
+            if files then
+                for k,v in next, files do
+                    if find(k,"^remap:") then
+                        k = files[k]
+                        v = files[k] -- chained
+                    end
+                    if find(k,pattern) then
+                        if type(v) == "string" then
+                            handle(blobtype,blobpath,v,k)
+                        else
+                            for _,vv in next, v do -- indexed
+                                handle(blobtype,blobpath,vv,k)
+                            end
+                        end
+                    end
+                end
+            end
+        end
+    end
+end
+
+function resolvers.locate_format(name)
+    local barename, fmtname = gsub(name,"%.%a+$",""), ""
+    if resolvers.usecache then
+        local path = file.join(caches.setpath("formats")) -- maybe platform
+        fmtname = file.join(path,barename..".fmt") or ""
+    end
+    if fmtname == "" then
+        fmtname = resolvers.find_files(barename..".fmt")[1] or ""
+    end
+    fmtname = resolvers.clean_path(fmtname)
+    if fmtname ~= "" then
+        local barename = file.removesuffix(fmtname)
+        local luaname, lucname, luiname = barename .. ".lua", barename .. ".luc", barename .. ".lui"
+        if lfs.isfile(luiname) then
+            return barename, luiname
+        elseif lfs.isfile(lucname) then
+            return barename, lucname
+        elseif lfs.isfile(luaname) then
+            return barename, luaname
+        end
+    end
+    return nil, nil
+end
+
+function resolvers.boolean_variable(str,default)
+    local b = resolvers.expansion(str)
+    if b == "" then
+        return default
+    else
+        b = toboolean(b)
+        return (b == nil and default) or b
+    end
+end
+
+texconfig.kpse_init = false
+
+kpse = { original = kpse } setmetatable(kpse, { __index = function(k,v) return resolvers[v] end } )
+
+-- for a while
+
+input = resolvers
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-tmp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+--[[ldx--
+<p>This module deals with caching data. It sets up the paths and
+implements loaders and savers for tables. Best is to set the
+following variable. When not set, the usual paths will be
+checked. Personally I prefer the (users) temporary path.</p>
+
+</code>
+TEXMFCACHE=$TMP;$TEMP;$TMPDIR;$TEMPDIR;$HOME;$TEXMFVAR;$VARTEXMF;.
+</code>
+
+<p>Currently we do no locking when we write files. This is no real
+problem because most caching involves fonts and the chance of them
+being written at the same time is small. We also need to extend
+luatools with a recache feature.</p>
+--ldx]]--
+
+local format, lower, gsub = string.format, string.lower, string.gsub
+
+local trace_cache = false  trackers.register("resolvers.cache", function(v) trace_cache = v end) -- not used yet
+
+caches = caches or { }
+
+caches.path     = caches.path or nil
+caches.base     = caches.base or "luatex-cache"
+caches.more     = caches.more or "context"
+caches.direct   = false -- true is faster but may need huge amounts of memory
+caches.tree     = false
+caches.paths    = caches.paths or nil
+caches.force    = false
+caches.defaults = { "TEXMFCACHE", "TMPDIR", "TEMPDIR", "TMP", "TEMP", "HOME", "HOMEPATH" }
+
+function caches.temp()
+    local cachepath = nil
+    local function check(list,isenv)
+        if not cachepath then
+            for k=1,#list do
+                local v = list[k]
+                cachepath = (isenv and (os.env[v] or "")) or v or ""
+                if cachepath == "" then
+                    -- next
+                else
+                    cachepath = resolvers.clean_path(cachepath)
+                    if lfs.isdir(cachepath) and file.iswritable(cachepath) then -- lfs.attributes(cachepath,"mode") == "directory"
+                        break
+                    elseif caches.force or io.ask(format("\nShould I create the cache path %s?",cachepath), "no", { "yes", "no" }) == "yes" then
+                        dir.mkdirs(cachepath)
+                        if lfs.isdir(cachepath) and file.iswritable(cachepath) then
+                            break
+                        end
+                    end
+                end
+                cachepath = nil
+            end
+        end
+    end
+    check(resolvers.clean_path_list("TEXMFCACHE") or { })
+    check(caches.defaults,true)
+    if not cachepath then
+        print("\nfatal error: there is no valid (writable) cache path defined\n")
+        os.exit()
+    elseif not lfs.isdir(cachepath) then -- lfs.attributes(cachepath,"mode") ~= "directory"
+        print(format("\nfatal error: cache path %s is not a directory\n",cachepath))
+        os.exit()
+    end
+    cachepath = file.collapse_path(cachepath)
+    function caches.temp()
+        return cachepath
+    end
+    return cachepath
+end
+
+function caches.configpath()
+    return table.concat(resolvers.instance.cnffiles,";")
+end
+
+function caches.hashed(tree)
+    return md5.hex(gsub(lower(tree),"[\\\/]+","/"))
+end
+
+function caches.treehash()
+    local tree = caches.configpath()
+    if not tree or tree == "" then
+        return false
+    else
+        return caches.hashed(tree)
+    end
+end
+
+function caches.setpath(...)
+    if not caches.path then
+        if not caches.path then
+            caches.path = caches.temp()
+        end
+        caches.path = resolvers.clean_path(caches.path) -- to be sure
+        caches.tree = caches.tree or caches.treehash()
+        if caches.tree then
+            caches.path = dir.mkdirs(caches.path,caches.base,caches.more,caches.tree)
+        else
+            caches.path = dir.mkdirs(caches.path,caches.base,caches.more)
+        end
+    end
+    if not caches.path then
+        caches.path = '.'
+    end
+    caches.path = resolvers.clean_path(caches.path)
+    local dirs = { ... }
+    if #dirs > 0 then
+        local pth = dir.mkdirs(caches.path,...)
+        return pth
+    end
+    caches.path = dir.expand_name(caches.path)
+    return caches.path
+end
+
+function caches.definepath(category,subcategory)
+    return function()
+        return caches.setpath(category,subcategory)
+    end
+end
+
+function caches.setluanames(path,name)
+    return path .. "/" .. name .. ".tma", path .. "/" .. name .. ".tmc"
+end
+
+function caches.loaddata(path,name)
+    local tmaname, tmcname = caches.setluanames(path,name)
+    local loader = loadfile(tmcname) or loadfile(tmaname)
+    if loader then
+        loader = loader()
+        collectgarbage("step")
+        return loader
+    else
+        return false
+    end
+end
+
+--~ function caches.loaddata(path,name)
+--~     local tmaname, tmcname = caches.setluanames(path,name)
+--~     return dofile(tmcname) or dofile(tmaname)
+--~ end
+
+function caches.iswritable(filepath,filename)
+    local tmaname, tmcname = caches.setluanames(filepath,filename)
+    return file.iswritable(tmaname)
+end
+
+function caches.savedata(filepath,filename,data,raw)
+    local tmaname, tmcname = caches.setluanames(filepath,filename)
+    local reduce, simplify = true, true
+    if raw then
+        reduce, simplify = false, false
+    end
+    data.cache_uuid = os.uuid()
+    if caches.direct then
+        file.savedata(tmaname, table.serialize(data,'return',false,true,false)) -- no hex
+    else
+        table.tofile(tmaname, data,'return',false,true,false) -- maybe not the last true
+    end
+    local cleanup = resolvers.boolean_variable("PURGECACHE", false)
+    local strip = resolvers.boolean_variable("LUACSTRIP", true)
+    utils.lua.compile(tmaname, tmcname, cleanup, strip)
+end
+
+-- here we use the cache for format loading (texconfig.[formatname|jobname])
+
+--~ if tex and texconfig and texconfig.formatname and texconfig.formatname == "" then
+if tex and texconfig and (not texconfig.formatname or texconfig.formatname == "") and input and resolvers.instance then
+    if not texconfig.luaname then texconfig.luaname = "cont-en.lua" end -- or luc
+    texconfig.formatname = caches.setpath("formats") .. "/" .. gsub(texconfig.luaname,"%.lu.$",".fmt")
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-res'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+--~ print(resolvers.resolve("abc env:tmp file:cont-en.tex path:cont-en.tex full:cont-en.tex rel:zapf/one/p-chars.tex"))
+
+local upper, lower, gsub = string.upper, string.lower, string.gsub
+
+local prefixes = { }
+
+prefixes.environment = function(str)
+    return resolvers.clean_path(os.getenv(str) or os.getenv(upper(str)) or os.getenv(lower(str)) or "")
+end
+
+prefixes.relative = function(str,n)
+    if io.exists(str) then
+        -- nothing
+    elseif io.exists("./" .. str) then
+        str = "./" .. str
+    else
+        local p = "../"
+        for i=1,n or 2 do
+            if io.exists(p .. str) then
+                str = p .. str
+                break
+            else
+                p = p .. "../"
+            end
+        end
+    end
+    return resolvers.clean_path(str)
+end
+
+prefixes.auto = function(str)
+    local fullname = prefixes.relative(str)
+    if not lfs.isfile(fullname) then
+        fullname = prefixes.locate(str)
+    end
+    return fullname
+end
+
+prefixes.locate = function(str)
+    local fullname = resolvers.find_given_file(str) or ""
+    return resolvers.clean_path((fullname ~= "" and fullname) or str)
+end
+
+prefixes.filename = function(str)
+    local fullname = resolvers.find_given_file(str) or ""
+    return resolvers.clean_path(file.basename((fullname ~= "" and fullname) or str))
+end
+
+prefixes.pathname = function(str)
+    local fullname = resolvers.find_given_file(str) or ""
+    return resolvers.clean_path(file.dirname((fullname ~= "" and fullname) or str))
+end
+
+prefixes.env  = prefixes.environment
+prefixes.rel  = prefixes.relative
+prefixes.loc  = prefixes.locate
+prefixes.kpse = prefixes.locate
+prefixes.full = prefixes.locate
+prefixes.file = prefixes.filename
+prefixes.path = prefixes.pathname
+
+function resolvers.allprefixes(separator)
+    local all = table.sortedkeys(prefixes)
+    if separator then
+        for i=1,#all do
+            all[i] = all[i] .. ":"
+        end
+    end
+    return all
+end
+
+local function _resolve_(method,target)
+    if prefixes[method] then
+        return prefixes[method](target)
+    else
+        return method .. ":" .. target
+    end
+end
+
+local function resolve(str)
+    if type(str) == "table" then
+        for k=1,#str do
+            local v = str[k]
+            str[k] = resolve(v) or v
+        end
+    elseif str and str ~= "" then
+        str = gsub(str,"([a-z]+):([^ \"\']*)",_resolve_)
+    end
+    return str
+end
+
+resolvers.resolve = resolve
+
+if os.uname then
+
+    for k, v in next, os.uname() do
+        if not prefixes[k] then
+            prefixes[k] = function() return v end
+        end
+    end
+
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-inp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+resolvers.finders = resolvers.finders or { }
+resolvers.openers = resolvers.openers or { }
+resolvers.loaders = resolvers.loaders or { }
+
+resolvers.finders.notfound  = { nil }
+resolvers.openers.notfound  = { nil }
+resolvers.loaders.notfound  = { false, nil, 0 }
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-out'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+outputs = outputs or { }
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-con'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, lower, gsub = string.format, string.lower, string.gsub
+
+local trace_cache      = false  trackers.register("resolvers.cache",      function(v) trace_cache      = v end)
+local trace_containers = false  trackers.register("resolvers.containers", function(v) trace_containers = v end)
+local trace_storage    = false  trackers.register("resolvers.storage",    function(v) trace_storage    = v end)
+
+--[[ldx--
+<p>Once we found ourselves defining similar cache constructs
+several times, containers were introduced. Containers are used
+to collect tables in memory and reuse them when possible based
+on (unique) hashes (to be provided by the calling function).</p>
+
+<p>Caching to disk is disabled by default. Version numbers are
+stored in the saved table which makes it possible to change the
+table structures without bothering about the disk cache.</p>
+
+<p>Examples of usage can be found in the font related code.</p>
+--ldx]]--
+
+containers = containers or { }
+
+containers.usecache = true
+
+local function report(container,tag,name)
+    if trace_cache or trace_containers then
+        logs.report(format("%s cache",container.subcategory),"%s: %s",tag,name or 'invalid')
+    end
+end
+
+local allocated = { }
+
+-- tracing
+
+function containers.define(category, subcategory, version, enabled)
+    return function()
+        if category and subcategory then
+            local c = allocated[category]
+            if not c then
+                c  = { }
+                allocated[category] = c
+            end
+            local s = c[subcategory]
+            if not s then
+                s = {
+                    category = category,
+                    subcategory = subcategory,
+                    storage = { },
+                    enabled = enabled,
+                    version = version or 1.000,
+                    trace = false,
+                    path = caches and caches.setpath and caches.setpath(category,subcategory),
+                }
+                c[subcategory] = s
+            end
+            return s
+        else
+            return nil
+        end
+    end
+end
+
+function containers.is_usable(container, name)
+    return container.enabled and caches and caches.iswritable(container.path, name)
+end
+
+function containers.is_valid(container, name)
+    if name and name ~= "" then
+        local storage = container.storage[name]
+        return storage and storage.cache_version == container.version
+    else
+        return false
+    end
+end
+
+function containers.read(container,name)
+    if container.enabled and caches and not container.storage[name] and containers.usecache then
+        container.storage[name] = caches.loaddata(container.path,name)
+        if containers.is_valid(container,name) then
+            report(container,"loaded",name)
+        else
+            container.storage[name] = nil
+        end
+    end
+    if container.storage[name] then
+        report(container,"reusing",name)
+    end
+    return container.storage[name]
+end
+
+function containers.write(container, name, data)
+    if data then
+        data.cache_version = container.version
+        if container.enabled and caches then
+            local unique, shared = data.unique, data.shared
+            data.unique, data.shared = nil, nil
+            caches.savedata(container.path, name, data)
+            report(container,"saved",name)
+            data.unique, data.shared = unique, shared
+        end
+        report(container,"stored",name)
+        container.storage[name] = data
+    end
+    return data
+end
+
+function containers.content(container,name)
+    return container.storage[name]
+end
+
+function containers.cleanname(name)
+    return (gsub(lower(name),"[^%w%d]+","-"))
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-use'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, lower, gsub, find = string.format, string.lower, string.gsub, string.find
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+-- since we want to use the cache instead of the tree, we will now
+-- reimplement the saver.
+
+local save_data = resolvers.save_data
+local load_data = resolvers.load_data
+
+resolvers.cachepath = nil  -- public, for tracing
+resolvers.usecache  = true -- public, for tracing
+
+function resolvers.save_data(dataname)
+    save_data(dataname, function(cachename,dataname)
+        resolvers.usecache = not toboolean(resolvers.expansion("CACHEINTDS") or "false",true)
+        if resolvers.usecache then
+            resolvers.cachepath = resolvers.cachepath or caches.definepath("trees")
+            return file.join(resolvers.cachepath(),caches.hashed(cachename))
+        else
+            return file.join(cachename,dataname)
+        end
+    end)
+end
+
+function resolvers.load_data(pathname,dataname,filename)
+    load_data(pathname,dataname,filename,function(dataname,filename)
+        resolvers.usecache = not toboolean(resolvers.expansion("CACHEINTDS") or "false",true)
+        if resolvers.usecache then
+            resolvers.cachepath = resolvers.cachepath or caches.definepath("trees")
+            return file.join(resolvers.cachepath(),caches.hashed(pathname))
+        else
+            if not filename or (filename == "") then
+                filename = dataname
+            end
+            return file.join(pathname,filename)
+        end
+    end)
+end
+
+-- we will make a better format, maybe something xml or just text or lua
+
+resolvers.automounted = resolvers.automounted or { }
+
+function resolvers.automount(usecache)
+    local mountpaths = resolvers.clean_path_list(resolvers.expansion('TEXMFMOUNT'))
+    if (not mountpaths or #mountpaths == 0) and usecache then
+        mountpaths = { caches.setpath("mount") }
+    end
+    if mountpaths and #mountpaths > 0 then
+        statistics.starttiming(resolvers.instance)
+        for k=1,#mountpaths do
+            local root = mountpaths[k]
+            local f = io.open(root.."/url.tmi")
+            if f then
+                for line in f:lines() do
+                    if line then
+                        if find(line,"^[%%#%-]") then -- or %W
+                            -- skip
+                        elseif find(line,"^zip://") then
+                            if trace_locating then
+                                logs.report("fileio","mounting %s",line)
+                            end
+                            table.insert(resolvers.automounted,line)
+                            resolvers.usezipfile(line)
+                        end
+                    end
+                end
+                f:close()
+            end
+        end
+        statistics.stoptiming(resolvers.instance)
+    end
+end
+
+-- status info
+
+statistics.register("used config path", function() return caches.configpath()  end)
+statistics.register("used cache path",  function() return caches.temp() or "?" end)
+
+-- experiment (code will move)
+
+function statistics.save_fmt_status(texname,formatbanner,sourcefile) -- texname == formatname
+    local enginebanner = status.list().banner
+    if formatbanner and enginebanner and sourcefile then
+        local luvname = file.replacesuffix(texname,"luv")
+        local luvdata = {
+            enginebanner = enginebanner,
+            formatbanner = formatbanner,
+            sourcehash   = md5.hex(io.loaddata(resolvers.find_file(sourcefile)) or "unknown"),
+            sourcefile   = sourcefile,
+        }
+        io.savedata(luvname,table.serialize(luvdata,true))
+    end
+end
+
+function statistics.check_fmt_status(texname)
+    local enginebanner = status.list().banner
+    if enginebanner and texname then
+        local luvname = file.replacesuffix(texname,"luv")
+        if lfs.isfile(luvname) then
+            local luv = dofile(luvname)
+            if luv and luv.sourcefile then
+                local sourcehash = md5.hex(io.loaddata(resolvers.find_file(luv.sourcefile)) or "unknown")
+                local luvbanner = luv.enginebanner or "?"
+                if luvbanner ~= enginebanner then
+                    return string.format("engine mismatch (luv:%s <> bin:%s)",luvbanner,enginebanner)
+                end
+                local luvhash = luv.sourcehash or "?"
+                if luvhash ~= sourcehash then
+                    return string.format("source mismatch (luv:%s <> bin:%s)",luvhash,sourcehash)
+                end
+            else
+                return "invalid status file"
+            end
+        else
+            return "missing status file"
+        end
+    end
+    return true
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-zip'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, find, match = string.format, string.find, string.match
+local unpack = unpack or table.unpack
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+-- zip:///oeps.zip?name=bla/bla.tex
+-- zip:///oeps.zip?tree=tex/texmf-local
+-- zip:///texmf.zip?tree=/tex/texmf
+-- zip:///texmf.zip?tree=/tex/texmf-local
+-- zip:///texmf-mine.zip?tree=/tex/texmf-projects
+
+zip                 = zip or { }
+zip.archives        = zip.archives or { }
+zip.registeredfiles = zip.registeredfiles or { }
+
+local finders, openers, loaders = resolvers.finders, resolvers.openers, resolvers.loaders
+local locators, hashers, concatinators = resolvers.locators, resolvers.hashers, resolvers.concatinators
+
+local archives = zip.archives
+
+local function validzip(str) -- todo: use url splitter
+    if not find(str,"^zip://") then
+        return "zip:///" .. str
+    else
+        return str
+    end
+end
+
+function zip.openarchive(name)
+    if not name or name == "" then
+        return nil
+    else
+        local arch = archives[name]
+        if not arch then
+           local full = resolvers.find_file(name) or ""
+           arch = (full ~= "" and zip.open(full)) or false
+           archives[name] = arch
+        end
+       return arch
+    end
+end
+
+function zip.closearchive(name)
+    if not name or (name == "" and archives[name]) then
+        zip.close(archives[name])
+        archives[name] = nil
+    end
+end
+
+function locators.zip(specification) -- where is this used? startup zips (untested)
+    specification = resolvers.splitmethod(specification)
+    local zipfile = specification.path
+    local zfile = zip.openarchive(name) -- tricky, could be in to be initialized tree
+    if trace_locating then
+        if zfile then
+            logs.report("fileio","zip locator, archive '%s' found",specification.original)
+        else
+            logs.report("fileio","zip locator, archive '%s' not found",specification.original)
+        end
+    end
+end
+
+function hashers.zip(tag,name)
+    if trace_locating then
+        logs.report("fileio","loading zip file '%s' as '%s'",name,tag)
+    end
+    resolvers.usezipfile(format("%s?tree=%s",tag,name))
+end
+
+function concatinators.zip(tag,path,name)
+    if not path or path == "" then
+        return format('%s?name=%s',tag,name)
+    else
+        return format('%s?name=%s/%s',tag,path,name)
+    end
+end
+
+function resolvers.isreadable.zip(name)
+    return true
+end
+
+function finders.zip(specification,filetype)
+    specification = resolvers.splitmethod(specification)
+    if specification.path then
+        local q = url.query(specification.query)
+        if q.name then
+            local zfile = zip.openarchive(specification.path)
+            if zfile then
+                if trace_locating then
+                    logs.report("fileio","zip finder, archive '%s' found",specification.path)
+                end
+                local dfile = zfile:open(q.name)
+                if dfile then
+                    dfile = zfile:close()
+                    if trace_locating then
+                        logs.report("fileio","zip finder, file '%s' found",q.name)
+                    end
+                    return specification.original
+                elseif trace_locating then
+                    logs.report("fileio","zip finder, file '%s' not found",q.name)
+                end
+            elseif trace_locating then
+                logs.report("fileio","zip finder, unknown archive '%s'",specification.path)
+            end
+        end
+    end
+    if trace_locating then
+        logs.report("fileio","zip finder, '%s' not found",filename)
+    end
+    return unpack(finders.notfound)
+end
+
+function openers.zip(specification)
+    local zipspecification = resolvers.splitmethod(specification)
+    if zipspecification.path then
+        local q = url.query(zipspecification.query)
+        if q.name then
+            local zfile = zip.openarchive(zipspecification.path)
+            if zfile then
+                if trace_locating then
+                    logs.report("fileio","zip opener, archive '%s' opened",zipspecification.path)
+                end
+                local dfile = zfile:open(q.name)
+                if dfile then
+                    logs.show_open(specification)
+                    if trace_locating then
+                        logs.report("fileio","zip opener, file '%s' found",q.name)
+                    end
+                    return openers.text_opener(specification,dfile,'zip')
+                elseif trace_locating then
+                    logs.report("fileio","zip opener, file '%s' not found",q.name)
+                end
+            elseif trace_locating then
+                logs.report("fileio","zip opener, unknown archive '%s'",zipspecification.path)
+            end
+        end
+    end
+    if trace_locating then
+        logs.report("fileio","zip opener, '%s' not found",filename)
+    end
+    return unpack(openers.notfound)
+end
+
+function loaders.zip(specification)
+    specification = resolvers.splitmethod(specification)
+    if specification.path then
+        local q = url.query(specification.query)
+        if q.name then
+            local zfile = zip.openarchive(specification.path)
+            if zfile then
+                if trace_locating then
+                    logs.report("fileio","zip loader, archive '%s' opened",specification.path)
+                end
+                local dfile = zfile:open(q.name)
+                if dfile then
+                    logs.show_load(filename)
+                    if trace_locating then
+                        logs.report("fileio","zip loader, file '%s' loaded",filename)
+                    end
+                    local s = dfile:read("*all")
+                    dfile:close()
+                    return true, s, #s
+                elseif trace_locating then
+                    logs.report("fileio","zip loader, file '%s' not found",q.name)
+                end
+            elseif trace_locating then
+                logs.report("fileio","zip loader, unknown archive '%s'",specification.path)
+            end
+        end
+    end
+    if trace_locating then
+        logs.report("fileio","zip loader, '%s' not found",filename)
+    end
+    return unpack(openers.notfound)
+end
+
+-- zip:///somefile.zip
+-- zip:///somefile.zip?tree=texmf-local -> mount
+
+function resolvers.usezipfile(zipname)
+    zipname = validzip(zipname)
+    local specification = resolvers.splitmethod(zipname)
+    local zipfile = specification.path
+    if zipfile and not zip.registeredfiles[zipname] then
+        local tree = url.query(specification.query).tree or ""
+        local z = zip.openarchive(zipfile)
+        if z then
+            local instance = resolvers.instance
+            if trace_locating then
+                logs.report("fileio","zip registering, registering archive '%s'",zipname)
+            end
+            statistics.starttiming(instance)
+            resolvers.prepend_hash('zip',zipname,zipfile)
+            resolvers.extend_texmf_var(zipname) -- resets hashes too
+            zip.registeredfiles[zipname] = z
+            instance.files[zipname] = resolvers.register_zip_file(z,tree or "")
+            statistics.stoptiming(instance)
+        elseif trace_locating then
+            logs.report("fileio","zip registering, unknown archive '%s'",zipname)
+        end
+    elseif trace_locating then
+        logs.report("fileio","zip registering, '%s' not found",zipname)
+    end
+end
+
+function resolvers.register_zip_file(z,tree)
+    local files, filter = { }, ""
+    if tree == "" then
+        filter = "^(.+)/(.-)$"
+    else
+        filter = format("^%s/(.+)/(.-)$",tree)
+    end
+    if trace_locating then
+        logs.report("fileio","zip registering, using filter '%s'",filter)
+    end
+    local register, n = resolvers.register_file, 0
+    for i in z:files() do
+        local path, name = match(i.filename,filter)
+        if path then
+            if name and name ~= '' then
+                register(files, name, path)
+                n = n + 1
+            else
+                -- directory
+            end
+        else
+            register(files, i.filename, '')
+            n = n + 1
+        end
+    end
+    logs.report("fileio","zip registering, %s files registered",n)
+    return files
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-crl'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local gsub = string.gsub
+
+curl = curl or { }
+
+curl.cached    = { }
+curl.cachepath = caches.definepath("curl")
+
+local finders, openers, loaders = resolvers.finders, resolvers.openers, resolvers.loaders
+
+function curl.fetch(protocol, name)
+    local cachename = curl.cachepath() .. "/" .. gsub(name,"[^%a%d%.]+","-")
+--  cachename = gsub(cachename,"[\\/]", io.fileseparator)
+    cachename = gsub(cachename,"[\\]", "/") -- cleanup
+    if not curl.cached[name] then
+        if not io.exists(cachename) then
+            curl.cached[name] = cachename
+            local command = "curl --silent --create-dirs --output " .. cachename .. " " .. name -- no protocol .. "://"
+            os.spawn(command)
+        end
+        if io.exists(cachename) then
+            curl.cached[name] = cachename
+        else
+            curl.cached[name] = ""
+        end
+    end
+    return curl.cached[name]
+end
+
+function finders.curl(protocol,filename)
+    local foundname = curl.fetch(protocol, filename)
+    return finders.generic(protocol,foundname,filetype)
+end
+
+function openers.curl(protocol,filename)
+    return openers.generic(protocol,filename)
+end
+
+function loaders.curl(protocol,filename)
+    return loaders.generic(protocol,filename)
+end
+
+-- todo: metamethod
+
+function curl.install(protocol)
+    finders[protocol] = function (filename,filetype) return finders.curl(protocol,filename) end
+    openers[protocol] = function (filename)          return openers.curl(protocol,filename) end
+    loaders[protocol] = function (filename)          return loaders.curl(protocol,filename) end
+end
+
+curl.install('http')
+curl.install('https')
+curl.install('ftp')
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-lua'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- some loading stuff ... we might move this one to slot 2 depending
+-- on the developments (the loaders must not trigger kpse); we could
+-- of course use a more extensive lib path spec
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+local gsub, insert = string.gsub, table.insert
+local unpack = unpack or table.unpack
+
+local  libformats = { 'luatexlibs', 'tex', 'texmfscripts', 'othertextfiles' } -- 'luainputs'
+local clibformats = { 'lib' }
+
+local _path_, libpaths, _cpath_, clibpaths
+
+function package.libpaths()
+    if not _path_ or package.path ~= _path_ then
+        _path_ = package.path
+        libpaths = file.split_path(_path_,";")
+    end
+    return libpaths
+end
+
+function package.clibpaths()
+    if not _cpath_ or package.cpath ~= _cpath_ then
+        _cpath_ = package.cpath
+        clibpaths = file.split_path(_cpath_,";")
+    end
+    return clibpaths
+end
+
+local function thepath(...)
+    local t = { ... } t[#t+1] = "?.lua"
+    local path = file.join(unpack(t))
+    if trace_locating then
+        logs.report("fileio","! appending '%s' to 'package.path'",path)
+    end
+    return path
+end
+
+local p_libpaths, a_libpaths = { }, { }
+
+function package.append_libpath(...)
+    insert(a_libpath,thepath(...))
+end
+
+function package.prepend_libpath(...)
+    insert(p_libpaths,1,thepath(...))
+end
+
+-- beware, we need to return a loadfile result !
+
+local function loaded(libpaths,name,simple)
+    for i=1,#libpaths do -- package.path, might become option
+        local libpath = libpaths[i]
+        local resolved = gsub(libpath,"%?",simple)
+        if trace_locating then -- more detail
+            logs.report("fileio","! checking for '%s' on 'package.path': '%s' => '%s'",simple,libpath,resolved)
+        end
+        if resolvers.isreadable.file(resolved) then
+            if trace_locating then
+                logs.report("fileio","! lib '%s' located via 'package.path': '%s'",name,resolved)
+            end
+            return loadfile(resolved)
+        end
+    end
+end
+
+
+package.loaders[2] = function(name) -- was [#package.loaders+1]
+    if trace_locating then -- mode detail
+        logs.report("fileio","! locating '%s'",name)
+    end
+    for i=1,#libformats do
+        local format = libformats[i]
+        local resolved = resolvers.find_file(name,format) or ""
+        if trace_locating then -- mode detail
+            logs.report("fileio","! checking for '%s' using 'libformat path': '%s'",name,format)
+        end
+        if resolved ~= "" then
+            if trace_locating then
+                logs.report("fileio","! lib '%s' located via environment: '%s'",name,resolved)
+            end
+            return loadfile(resolved)
+        end
+    end
+    -- libpaths
+    local libpaths, clibpaths = package.libpaths(), package.clibpaths()
+    local simple = gsub(name,"%.lua$","")
+    local simple = gsub(simple,"%.","/")
+    local resolved = loaded(p_libpaths,name,simple) or loaded(libpaths,name,simple) or loaded(a_libpaths,name,simple)
+    if resolved then
+        return resolved
+    end
+    --
+    local libname = file.addsuffix(simple,os.libsuffix)
+    for i=1,#clibformats do
+        -- better have a dedicated loop
+        local format = clibformats[i]
+        local paths = resolvers.expanded_path_list_from_var(format)
+        for p=1,#paths do
+            local path = paths[p]
+            local resolved = file.join(path,libname)
+            if trace_locating then -- mode detail
+                logs.report("fileio","! checking for '%s' using 'clibformat path': '%s'",libname,path)
+            end
+            if resolvers.isreadable.file(resolved) then
+                if trace_locating then
+                    logs.report("fileio","! lib '%s' located via 'clibformat': '%s'",libname,resolved)
+                end
+                return package.loadlib(resolved,name)
+            end
+        end
+    end
+    for i=1,#clibpaths do -- package.path, might become option
+        local libpath = clibpaths[i]
+        local resolved = gsub(libpath,"?",simple)
+        if trace_locating then -- more detail
+            logs.report("fileio","! checking for '%s' on 'package.cpath': '%s'",simple,libpath)
+        end
+        if resolvers.isreadable.file(resolved) then
+            if trace_locating then
+                logs.report("fileio","! lib '%s' located via 'package.cpath': '%s'",name,resolved)
+            end
+            return package.loadlib(resolved,name)
+        end
+    end
+    -- just in case the distribution is messed up
+    if trace_loading then -- more detail
+        logs.report("fileio","! checking for '%s' using 'luatexlibs': '%s'",name)
+    end
+    local resolved = resolvers.find_file(file.basename(name),'luatexlibs') or ""
+    if resolved ~= "" then
+        if trace_locating then
+            logs.report("fileio","! lib '%s' located by basename via environment: '%s'",name,resolved)
+        end
+        return loadfile(resolved)
+    end
+    if trace_locating then
+        logs.report("fileio",'? unable to locate lib: %s',name)
+    end
+--  return "unable to locate " .. name
+end
+
+resolvers.loadlualib = require
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['luat-kps'] = {
+    version   = 1.001,
+    comment   = "companion to luatools.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+--[[ldx--
+<p>This file is used when we want the input handlers to behave like
+<type>kpsewhich</type>. What to do with the following:</p>
+
+<typing>
+{$SELFAUTOLOC,$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,}/web2c}
+$SELFAUTOLOC    : /usr/tex/bin/platform
+$SELFAUTODIR    : /usr/tex/bin
+$SELFAUTOPARENT : /usr/tex
+</typing>
+
+<p>How about just forgetting about them?</p>
+--ldx]]--
+
+local suffixes = resolvers.suffixes
+local formats  = resolvers.formats
+
+suffixes['gf']                       = { '<resolution>gf' }
+suffixes['pk']                       = { '<resolution>pk' }
+suffixes['base']                     = { 'base' }
+suffixes['bib']                      = { 'bib' }
+suffixes['bst']                      = { 'bst' }
+suffixes['cnf']                      = { 'cnf' }
+suffixes['mem']                      = { 'mem' }
+suffixes['mf']                       = { 'mf' }
+suffixes['mfpool']                   = { 'pool' }
+suffixes['mft']                      = { 'mft' }
+suffixes['mppool']                   = { 'pool' }
+suffixes['graphic/figure']           = { 'eps', 'epsi' }
+suffixes['texpool']                  = { 'pool' }
+suffixes['PostScript header']        = { 'pro' }
+suffixes['ist']                      = { 'ist' }
+suffixes['web']                      = { 'web', 'ch' }
+suffixes['cweb']                     = { 'w', 'web', 'ch' }
+suffixes['cmap files']               = { 'cmap' }
+suffixes['lig files']                = { 'lig' }
+suffixes['bitmap font']              = { }
+suffixes['MetaPost support']         = { }
+suffixes['TeX system documentation'] = { }
+suffixes['TeX system sources']       = { }
+suffixes['dvips config']             = { }
+suffixes['type42 fonts']             = { }
+suffixes['web2c files']              = { }
+suffixes['other text files']         = { }
+suffixes['other binary files']       = { }
+suffixes['opentype fonts']           = { 'otf' }
+
+suffixes['fmt']                      = { 'fmt' }
+suffixes['texmfscripts']             = { 'rb','lua','py','pl' }
+
+suffixes['pdftex config']            = { }
+suffixes['Troff fonts']              = { }
+
+suffixes['ls-R']                     = { }
+
+--[[ldx--
+<p>If you wondered abou tsome of the previous mappings, how about
+the next bunch:</p>
+--ldx]]--
+
+formats['bib']                      = ''
+formats['bst']                      = ''
+formats['mft']                      = ''
+formats['ist']                      = ''
+formats['web']                      = ''
+formats['cweb']                     = ''
+formats['MetaPost support']         = ''
+formats['TeX system documentation'] = ''
+formats['TeX system sources']       = ''
+formats['Troff fonts']              = ''
+formats['dvips config']             = ''
+formats['graphic/figure']           = ''
+formats['ls-R']                     = ''
+formats['other text files']         = ''
+formats['other binary files']       = ''
+
+formats['gf']                       = ''
+formats['pk']                       = ''
+formats['base']                     = 'MFBASES'
+formats['cnf']                      = ''
+formats['mem']                      = 'MPMEMS'
+formats['mf']                       = 'MFINPUTS'
+formats['mfpool']                   = 'MFPOOL'
+formats['mppool']                   = 'MPPOOL'
+formats['texpool']                  = 'TEXPOOL'
+formats['PostScript header']        = 'TEXPSHEADERS'
+formats['cmap files']               = 'CMAPFONTS'
+formats['type42 fonts']             = 'T42FONTS'
+formats['web2c files']              = 'WEB2C'
+formats['pdftex config']            = 'PDFTEXCONFIG'
+formats['texmfscripts']             = 'TEXMFSCRIPTS'
+formats['bitmap font']              = ''
+formats['lig files']                = 'LIGFONTS'
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-aux'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local find = string.find
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+function resolvers.update_script(oldname,newname) -- oldname -> own.name, not per se a suffix
+    local scriptpath = "scripts/context/lua"
+    newname = file.addsuffix(newname,"lua")
+    local oldscript = resolvers.clean_path(oldname)
+    if trace_locating then
+        logs.report("fileio","to be replaced old script %s", oldscript)
+    end
+    local newscripts = resolvers.find_files(newname) or { }
+    if #newscripts == 0 then
+        if trace_locating then
+            logs.report("fileio","unable to locate new script")
+        end
+    else
+        for i=1,#newscripts do
+            local newscript = resolvers.clean_path(newscripts[i])
+            if trace_locating then
+                logs.report("fileio","checking new script %s", newscript)
+            end
+            if oldscript == newscript then
+                if trace_locating then
+                    logs.report("fileio","old and new script are the same")
+                end
+            elseif not find(newscript,scriptpath) then
+                if trace_locating then
+                    logs.report("fileio","new script should come from %s",scriptpath)
+                end
+            elseif not (find(oldscript,file.removesuffix(newname).."$") or find(oldscript,newname.."$")) then
+                if trace_locating then
+                    logs.report("fileio","invalid new script name")
+                end
+            else
+                local newdata = io.loaddata(newscript)
+                if newdata then
+                    if trace_locating then
+                        logs.report("fileio","old script content replaced by new content")
+                    end
+                    io.savedata(oldscript,newdata)
+                    break
+                elseif trace_locating then
+                    logs.report("fileio","unable to load new script")
+                end
+            end
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-tmf'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local find, gsub, match = string.find, string.gsub, string.match
+local getenv, setenv = os.getenv, os.setenv
+
+-- loads *.tmf files in minimal tree roots (to be optimized and documented)
+
+function resolvers.check_environment(tree)
+    logs.simpleline()
+    setenv('TMP', getenv('TMP') or getenv('TEMP') or getenv('TMPDIR') or getenv('HOME'))
+    setenv('TEXOS', getenv('TEXOS') or ("texmf-" .. os.platform))
+    setenv('TEXPATH', gsub(tree or "tex","\/+$",''))
+    setenv('TEXMFOS', getenv('TEXPATH') .. "/" .. getenv('TEXOS'))
+    logs.simpleline()
+    logs.simple("preset : TEXPATH => %s", getenv('TEXPATH'))
+    logs.simple("preset : TEXOS   => %s", getenv('TEXOS'))
+    logs.simple("preset : TEXMFOS => %s", getenv('TEXMFOS'))
+    logs.simple("preset : TMP     => %s", getenv('TMP'))
+    logs.simple('')
+end
+
+function resolvers.load_environment(name) -- todo: key=value as well as lua
+    local f = io.open(name)
+    if f then
+        for line in f:lines() do
+            if find(line,"^[%%%#]") then
+                -- skip comment
+            else
+                local key, how, value = match(line,"^(.-)%s*([<=>%?]+)%s*(.*)%s*$")
+                if how then
+                    value = gsub(value,"%%(.-)%%", function(v) return getenv(v) or "" end)
+                        if how == "=" or how == "<<" then
+                            setenv(key,value)
+                    elseif how == "?" or how == "??" then
+                            setenv(key,getenv(key) or value)
+                    elseif how == "<" or how == "+=" then
+                        if getenv(key) then
+                            setenv(key,getenv(key) .. io.fileseparator .. value)
+                        else
+                            setenv(key,value)
+                        end
+                    elseif how == ">" or how == "=+" then
+                        if getenv(key) then
+                            setenv(key,value .. io.pathseparator .. getenv(key))
+                        else
+                            setenv(key,value)
+                        end
+                    end
+                end
+            end
+        end
+        f:close()
+    end
+end
+
+function resolvers.load_tree(tree)
+    if tree and tree ~= "" then
+        local setuptex = 'setuptex.tmf'
+        if lfs.attributes(tree, "mode") == "directory" then -- check if not nil
+            setuptex = tree .. "/" .. setuptex
+        else
+            setuptex = tree
+        end
+        if io.exists(setuptex) then
+            resolvers.check_environment(tree)
+            resolvers.load_environment(setuptex)
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['luat-sta'] = {
+    version   = 1.001,
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- this code is used in the updater
+
+local gmatch, match = string.gmatch, string.match
+local type = type
+
+states          = states          or { }
+states.data     = states.data     or { }
+states.hash     = states.hash     or { }
+states.tag      = states.tag      or ""
+states.filename = states.filename or ""
+
+function states.save(filename,tag)
+    tag = tag or states.tag
+    filename = file.addsuffix(filename or states.filename,'lus')
+    io.savedata(filename,
+        "-- generator : luat-sta.lua\n" ..
+        "-- state tag : " .. tag .. "\n\n" ..
+        table.serialize(states.data[tag or states.tag] or {},true)
+    )
+end
+
+function states.load(filename,tag)
+    states.filename = filename
+    states.tag = tag or "whatever"
+    states.filename = file.addsuffix(states.filename,'lus')
+    states.data[states.tag], states.hash[states.tag] = (io.exists(filename) and dofile(filename)) or { }, { }
+end
+
+function states.set_by_tag(tag,key,value,default,persistent)
+    local d, h = states.data[tag], states.hash[tag]
+    if d then
+        if type(d) == "table" then
+            local dkey, hkey = key, key
+            local pre, post = match(key,"(.+)%.([^%.]+)$")
+            if pre and post then
+                for k in gmatch(pre,"[^%.]+") do
+                    local dk = d[k]
+                    if not dk then
+                        dk = { }
+                        d[k] = dk
+                    elseif type(dk) == "string" then
+                        -- invalid table, unable to upgrade structure
+                        -- hope for the best or delete the state file
+                        break
+                    end
+                    d = dk
+                end
+                dkey, hkey = post, key
+            end
+            if type(value) == nil then
+                value = value or default
+            elseif persistent then
+                value = value or d[dkey] or default
+            else
+                value = value or default
+            end
+            d[dkey], h[hkey] = value, value
+        elseif type(d) == "string" then
+            -- weird
+            states.data[tag], states.hash[tag] = value, value
+        end
+    end
+end
+
+function states.get_by_tag(tag,key,default)
+    local h = states.hash[tag]
+    if h and h[key] then
+        return h[key]
+    else
+        local d = states.data[tag]
+        if d then
+            for k in gmatch(key,"[^%.]+") do
+                local dk = d[k]
+                if dk then
+                    d = dk
+                else
+                    return default
+                end
+            end
+            return d or default
+        end
+    end
+end
+
+function states.set(key,value,default,persistent)
+    states.set_by_tag(states.tag,key,value,default,persistent)
+end
+
+function states.get(key,default)
+    return states.get_by_tag(states.tag,key,default)
+end
+
+--~ states.data.update = {
+--~ 	["version"] = {
+--~ 		["major"] = 0,
+--~ 		["minor"] = 1,
+--~ 	},
+--~ 	["rsync"] = {
+--~ 		["server"]     = "contextgarden.net",
+--~ 		["module"]     = "minimals",
+--~ 		["repository"] = "current",
+--~ 		["flags"]      = "-rpztlv --stats",
+--~ 	},
+--~ 	["tasks"] = {
+--~ 		["update"] = true,
+--~ 		["make"]   = true,
+--~         ["delete"] = false,
+--~ 	},
+--~ 	["platform"] = {
+--~ 		["host"]  = true,
+--~ 		["other"] = {
+--~ 			["mswin"]     = false,
+--~ 			["linux"]     = false,
+--~ 			["linux-64"]  = false,
+--~ 			["osx-intel"] = false,
+--~ 			["osx-ppc"]   = false,
+--~ 			["sun"]       = false,
+--~ 		},
+--~ 	},
+--~ 	["context"] = {
+--~ 		["available"] = {"current", "beta", "alpha", "experimental"},
+--~ 		["selected"]  = "current",
+--~ 	},
+--~ 	["formats"] = {
+--~ 		["cont-en"] = true,
+--~ 		["cont-nl"] = true,
+--~ 		["cont-de"] = false,
+--~ 		["cont-cz"] = false,
+--~ 		["cont-fr"] = false,
+--~ 		["cont-ro"] = false,
+--~ 	},
+--~ 	["engine"] = {
+--~ 		["pdftex"] = {
+--~ 			["install"] = true,
+--~ 			["formats"] = {
+--~ 				["pdftex"] = true,
+--~ 			},
+--~ 		},
+--~ 		["luatex"] = {
+--~ 			["install"] = true,
+--~ 			["formats"] = {
+--~ 			},
+--~ 		},
+--~ 		["xetex"] = {
+--~ 			["install"] = true,
+--~ 			["formats"] = {
+--~ 				["xetex"] = false,
+--~ 			},
+--~ 		},
+--~ 		["metapost"] = {
+--~ 			["install"] = true,
+--~ 			["formats"] = {
+--~ 				["mpost"] = true,
+--~ 				["metafun"] = true,
+--~ 			},
+--~ 		},
+--~ 	},
+--~ 	["fonts"] = {
+--~ 	},
+--~ 	["doc"] = {
+--~ 	},
+--~ 	["modules"] = {
+--~ 		["f-urwgaramond"] = false,
+--~ 		["f-urwgothic"] = false,
+--~ 		["t-bnf"] = false,
+--~ 		["t-chromato"] = false,
+--~ 		["t-cmscbf"] = false,
+--~ 		["t-cmttbf"] = false,
+--~ 		["t-construction-plan"] = false,
+--~ 		["t-degrade"] = false,
+--~ 		["t-french"] = false,
+--~ 		["t-lettrine"] = false,
+--~ 		["t-lilypond"] = false,
+--~ 		["t-mathsets"] = false,
+--~ 		["t-tikz"] = false,
+--~ 		["t-typearea"] = false,
+--~ 		["t-vim"] = false,
+--~ 	},
+--~ }
+
+--~ states.save("teststate", "update")
+--~ states.load("teststate", "update")
+
+--~ print(states.get_by_tag("update","rsync.server","unknown"))
+--~ states.set_by_tag("update","rsync.server","oeps")
+--~ print(states.get_by_tag("update","rsync.server","unknown"))
+--~ states.save("teststate", "update")
+--~ states.load("teststate", "update")
+--~ print(states.get_by_tag("update","rsync.server","unknown"))
+
+
+end -- of closure
+-- end library merge
+
+own = { } -- not local
+
+own.libs = { -- todo: check which ones are really needed
+    'l-string.lua',
+    'l-lpeg.lua',
+    'l-table.lua',
+    'l-io.lua',
+    'l-number.lua',
+    'l-set.lua',
+    'l-os.lua',
+    'l-file.lua',
+    'l-md5.lua',
+    'l-url.lua',
+    'l-dir.lua',
+    'l-boolean.lua',
+    'l-math.lua',
+--  'l-unicode.lua',
+--  'l-tex.lua',
+    'l-utils.lua',
+    'l-aux.lua',
+--  'l-xml.lua',
+    'trac-tra.lua',
+    'lxml-tab.lua',
+    'lxml-lpt.lua',
+--  'lxml-ent.lua',
+    'lxml-mis.lua',
+    'lxml-aux.lua',
+    'lxml-xml.lua',
+    'luat-env.lua',
+    'trac-inf.lua',
+    'trac-log.lua',
+    'data-res.lua',
+    'data-tmp.lua',
+    'data-pre.lua',
+    'data-inp.lua',
+    'data-out.lua',
+    'data-con.lua',
+    'data-use.lua',
+--  'data-tex.lua',
+--  'data-bin.lua',
+    'data-zip.lua',
+    'data-crl.lua',
+    'data-lua.lua',
+    'data-kps.lua', -- so that we can replace kpsewhich
+    'data-aux.lua', -- updater
+    'data-tmf.lua', -- tree files
+    -- needed ?
+    'luat-sta.lua', -- states
+}
+
+-- We need this hack till luatex is fixed.
+--
+-- for k,v in pairs(arg) do print(k,v) end
+
+if arg and (arg[0] == 'luatex' or arg[0] == 'luatex.exe') and arg[1] == "--luaonly" then
+    arg[-1]=arg[0] arg[0]=arg[2] for k=3,#arg do arg[k-2]=arg[k] end arg[#arg]=nil arg[#arg]=nil
+end
+
+-- End of hack.
+
+own.name = (environment and environment.ownname) or arg[0]  or 'luatools.lua'
+
+
+own.path = string.match(own.name,"^(.+)[\\/].-$") or "."
+own.list = { '.' }
+if own.path ~= '.' then
+    table.insert(own.list,own.path)
+end
+table.insert(own.list,own.path.."/../../../tex/context/base")
+table.insert(own.list,own.path.."/mtx")
+table.insert(own.list,own.path.."/../sources")
+
+local function locate_libs()
+    for _, lib in pairs(own.libs) do
+        for _, pth in pairs(own.list) do
+            local filename = string.gsub(pth .. "/" .. lib,"\\","/")
+            local codeblob = loadfile(filename)
+            if codeblob then
+                codeblob()
+                own.list = { pth } -- speed up te search
+                break
+            end
+        end
+    end
+end
+
+if not resolvers then
+    locate_libs()
+end
+
+if not resolvers then
+    print("")
+    print("Mtxrun is unable to start up due to lack of libraries. You may")
+    print("try to run 'lua mtxrun.lua --selfmerge' in the path where this")
+    print("script is located (normally under ..../scripts/context/lua) which")
+    print("will make this script library independent.")
+    os.exit()
+end
+
+logs.setprogram('MTXrun',"TDS Runner Tool 1.24",environment.arguments["verbose"] or false)
+
+local instance = resolvers.reset()
+
+local trackspec = environment.argument("trackers") or environment.argument("track")
+
+if trackspec then
+    trackers.enable(trackspec)
+end
+
+runners  = runners  or { } -- global
+messages = messages or { }
+
+messages.help = [[
+--script              run an mtx script (lua prefered method) (--noquotes), no script gives list
+--execute             run a script or program (texmfstart method) (--noquotes)
+--resolve             resolve prefixed arguments
+--ctxlua              run internally (using preloaded libs)
+--internal            run script using built in libraries (same as --ctxlua)
+--locate              locate given filename
+
+--autotree            use texmf tree cf. env 'texmfstart_tree' or 'texmfstarttree'
+--tree=pathtotree     use given texmf tree (default file: 'setuptex.tmf')
+--environment=name    use given (tmf) environment file
+--path=runpath        go to given path before execution
+--ifchanged=filename  only execute when given file has changed (md checksum)
+--iftouched=old,new   only execute when given file has changed (time stamp)
+
+--make                create stubs for (context related) scripts
+--remove              remove stubs (context related) scripts
+--stubpath=binpath    paths where stubs wil be written
+--windows             create windows (mswin) stubs
+--unix                create unix (linux) stubs
+
+--verbose             give a bit more info
+--trackers=list       enable given trackers
+--engine=str          target engine
+--progname=str        format or backend
+
+--edit                launch editor with found file
+--launch (--all)      launch files like manuals, assumes os support
+
+--timedrun            run a script an time its run
+--autogenerate        regenerate databases if needed (handy when used to run context in an editor)
+
+--usekpse             use kpse as fallback (when no mkiv and cache installed, often slower)
+--forcekpse           force using kpse (handy when no mkiv and cache installed but less functionality)
+
+--prefixes            show supported prefixes
+]]
+
+runners.applications = {
+    ["lua"] = "luatex --luaonly",
+    ["luc"] = "luatex --luaonly",
+    ["pl"] = "perl",
+    ["py"] = "python",
+    ["rb"] = "ruby",
+}
+
+runners.suffixes = {
+    'rb', 'lua', 'py', 'pl'
+}
+
+runners.registered = {
+    texexec      = { 'texexec.rb',      false },  -- context mkii runner (only tool not to be luafied)
+    texutil      = { 'texutil.rb',      true  },  -- old perl based index sorter for mkii (old versions need it)
+    texfont      = { 'texfont.pl',      true  },  -- perl script that makes mkii font metric files
+    texfind      = { 'texfind.pl',      false },  -- perltk based tex searching tool, mostly used at pragma
+    texshow      = { 'texshow.pl',      false },  -- perltk based context help system, will be luafied
+ -- texwork      = { 'texwork.pl',      false },  -- perltk based editing environment, only used at pragma
+    makempy      = { 'makempy.pl',      true  },
+    mptopdf      = { 'mptopdf.pl',      true  },
+    pstopdf      = { 'pstopdf.rb',      true  },  -- converts ps (and some more) images, does some cleaning (replaced)
+--  examplex     = { 'examplex.rb',     false },
+    concheck     = { 'concheck.rb',     false },
+    runtools     = { 'runtools.rb',     true  },
+    textools     = { 'textools.rb',     true  },
+    tmftools     = { 'tmftools.rb',     true  },
+    ctxtools     = { 'ctxtools.rb',     true  },
+    rlxtools     = { 'rlxtools.rb',     true  },
+    pdftools     = { 'pdftools.rb',     true  },
+    mpstools     = { 'mpstools.rb',     true  },
+--  exatools     = { 'exatools.rb',     true  },
+    xmltools     = { 'xmltools.rb',     true  },
+--  luatools     = { 'luatools.lua',    true  },
+    mtxtools     = { 'mtxtools.rb',     true  },
+    pdftrimwhite = { 'pdftrimwhite.pl', false }
+}
+
+runners.launchers = {
+    windows = { },
+    unix = { }
+}
+
+-- like runners.libpath("framework"): looks on script's subpath
+
+function runners.libpath(...)
+    package.prepend_libpath(file.dirname(environment.ownscript),...)
+    package.prepend_libpath(file.dirname(environment.ownname)  ,...)
+end
+
+function runners.prepare()
+    local checkname = environment.argument("ifchanged")
+    if checkname and checkname ~= "" then
+        local oldchecksum = file.loadchecksum(checkname)
+        local newchecksum = file.checksum(checkname)
+        if oldchecksum == newchecksum then
+            logs.simple("file '%s' is unchanged",checkname)
+            return "skip"
+        else
+            logs.simple("file '%s' is changed, processing started",checkname)
+        end
+        file.savechecksum(checkname)
+    end
+    local oldname, newname = string.split(environment.argument("iftouched") or "", ",")
+    if oldname and newname and oldname ~= "" and newname ~= "" then
+        if not file.needs_updating(oldname,newname) then
+            logs.simple("file '%s' and '%s' have same age",oldname,newname)
+            return "skip"
+        else
+            logs.simple("file '%s' is older than '%s'",oldname,newname)
+        end
+    end
+    local tree = environment.argument('tree') or ""
+    if environment.argument('autotree') then
+        tree = os.getenv('TEXMFSTART_TREE') or os.getenv('TEXMFSTARTTREE') or tree
+    end
+    if tree and tree ~= "" then
+        resolvers.load_tree(tree)
+    end
+    local env = environment.argument('environment') or ""
+    if env and env ~= "" then
+        for _,e in pairs(string.split(env)) do
+            -- maybe force suffix when not given
+            resolvers.load_tree(e)
+        end
+    end
+    local runpath = environment.argument("path")
+    if runpath and not lfs.chdir(runpath) then
+        logs.simple("unable to change to path '%s'",runpath)
+        return "error"
+    end
+    return "run"
+end
+
+function runners.execute_script(fullname,internal,nosplit)
+    local noquote = environment.argument("noquotes")
+    if fullname and fullname ~= "" then
+        local state = runners.prepare()
+        if state == 'error' then
+            return false
+        elseif state == 'skip' then
+            return true
+        elseif state == "run" then
+            instance.progname = environment.argument("progname") or instance.progname
+            instance.format   = environment.argument("format")   or instance.format
+            local path, name, suffix, result = file.dirname(fullname), file.basename(fullname), file.extname(fullname), ""
+            if path ~= "" then
+                result = fullname
+            elseif name then
+                name = name:gsub("^int[%a]*:",function()
+                    internal = true
+                    return ""
+                end )
+                name = name:gsub("^script:","")
+                if suffix == "" and runners.registered[name] and runners.registered[name][1] then
+                    name = runners.registered[name][1]
+                    suffix = file.extname(name)
+                end
+                if suffix == "" then
+                    -- loop over known suffixes
+                    for _,s in pairs(runners.suffixes) do
+                        result = resolvers.find_file(name .. "." .. s, 'texmfscripts')
+                        if result ~= "" then
+                            break
+                        end
+                    end
+                elseif runners.applications[suffix] then
+                    result = resolvers.find_file(name, 'texmfscripts')
+                else
+                    -- maybe look on path
+                    result = resolvers.find_file(name, 'other text files')
+                end
+            end
+            if result and result ~= "" then
+                if not no_split then
+                    local before, after = environment.split_arguments(fullname) -- already done
+                    environment.arguments_before, environment.arguments_after = before, after
+                end
+                if internal then
+                    arg = { } for _,v in pairs(environment.arguments_after) do arg[#arg+1] = v end
+                    environment.ownscript = result
+                    dofile(result)
+                else
+                    local binary = runners.applications[file.extname(result)]
+                    if binary and binary ~= "" then
+                        result = binary .. " " .. result
+                    end
+                    local command = result .. " " .. environment.reconstruct_commandline(environment.arguments_after,noquote)
+                    if logs.verbose then
+                        logs.simpleline()
+                        logs.simple("executing: %s",command)
+                        logs.simpleline()
+                        logs.simpleline()
+                        io.flush()
+                    end
+                    -- no os.exec because otherwise we get the wrong return value
+                    local code = os.execute(command) -- maybe spawn
+                    if code == 0 then
+                        return true
+                    else
+                        if binary then
+                            binary = file.addsuffix(binary,os.binsuffix)
+                            for p in string.gmatch(os.getenv("PATH"),"[^"..io.pathseparator.."]+") do
+                                if lfs.isfile(file.join(p,binary)) then
+                                    return false
+                                end
+                            end
+                            logs.simpleline()
+                            logs.simple("This script needs '%s' which seems not to be installed.",binary)
+                            logs.simpleline()
+                        end
+                        return false
+                    end
+                end
+            end
+        end
+    end
+    return false
+end
+
+function runners.execute_program(fullname)
+    local noquote = environment.argument("noquotes")
+    if fullname and fullname ~= "" then
+        local state = runners.prepare()
+        if state == 'error' then
+            return false
+        elseif state == 'skip' then
+            return true
+        elseif state == "run" then
+            local before, after = environment.split_arguments(fullname)
+            environment.initialize_arguments(after)
+            fullname = fullname:gsub("^bin:","")
+            local command = fullname .. " " .. (environment.reconstruct_commandline(after or "",noquote) or "")
+            logs.simpleline()
+            logs.simple("executing: %s",command)
+            logs.simpleline()
+            logs.simpleline()
+            io.flush()
+            local code = os.exec(command) -- (fullname,unpack(after)) does not work / maybe spawn
+            return code == 0
+        end
+    end
+    return false
+end
+
+-- the --usekpse flag will fallback on kpse (hm, we can better update mtx-stubs)
+
+local windows_stub = '@echo off\013\010setlocal\013\010set ownpath=%%~dp0%%\013\010texlua "%%ownpath%%mtxrun.lua" --usekpse --execute %s %%*\013\010endlocal\013\010'
+local unix_stub    = '#!/bin/sh\010mtxrun --usekpse --execute %s \"$@\"\010'
+
+function runners.handle_stubs(create)
+    local stubpath = environment.argument('stubpath') or '.' -- 'auto' no longer subpathssupported
+    local windows  = environment.argument('windows') or environment.argument('mswin') or false
+    local unix     = environment.argument('unix') or environment.argument('linux') or false
+    if not windows and not unix then
+        if os.platform == "unix" then
+            unix = true
+        else
+            windows = true
+        end
+    end
+    for _,v in pairs(runners.registered) do
+        local name, doit = v[1], v[2]
+        if doit then
+            local base = string.gsub(file.basename(name), "%.(.-)$", "")
+            if create then
+                if windows then
+                    io.savedata(file.join(stubpath,base..".bat"),string.format(windows_stub,name))
+                    logs.simple("windows stub for '%s' created",base)
+                end
+                if unix then
+                    io.savedata(file.join(stubpath,base),string.format(unix_stub,name))
+                    logs.simple("unix stub for '%s' created",base)
+                end
+            else
+                if windows and (os.remove(file.join(stubpath,base..'.bat')) or os.remove(file.join(stubpath,base..'.cmd'))) then
+                    logs.simple("windows stub for '%s' removed", base)
+                end
+                if unix and (os.remove(file.join(stubpath,base)) or os.remove(file.join(stubpath,base..'.sh'))) then
+                    logs.simple("unix stub for '%s' removed",base)
+                end
+            end
+        end
+    end
+end
+
+function runners.resolve_string(filename)
+    if filename and filename ~= "" then
+        runners.report_location(resolvers.resolve(filename))
+    end
+end
+
+function runners.locate_file(filename)
+    -- differs from texmfstart where locate appends .com .exe .bat ... todo
+    if filename and filename ~= "" then
+        runners.report_location(resolvers.find_given_file(filename))
+    end
+end
+
+function runners.locate_platform()
+    runners.report_location(os.platform)
+end
+
+function runners.report_location(result)
+    if logs.verbose then
+        logs.simpleline()
+        if result and result ~= "" then
+            logs.simple(result)
+        else
+            logs.simple("not found")
+        end
+    else
+        io.write(result)
+    end
+end
+
+function runners.edit_script(filename) -- we assume that vim is present on most systems
+    local editor = os.getenv("MTXRUN_EDITOR") or os.getenv("TEXMFSTART_EDITOR") or os.getenv("EDITOR") or 'vim'
+    local rest = resolvers.resolve(filename)
+    if rest ~= "" then
+        local command = editor .. " " .. rest
+        if logs.verbose then
+            logs.simpleline()
+            logs.simple("starting editor: %s",command)
+            logs.simple_line()
+            logs.simple_line()
+        end
+        os.launch(command)
+    end
+end
+
+function runners.save_script_session(filename, list)
+    local t = { }
+    for i=1,#list do
+        local key = list[i]
+        t[key] = environment.arguments[key]
+    end
+    io.savedata(filename,table.serialize(t,true))
+end
+
+function runners.load_script_session(filename)
+    if lfs.isfile(filename) then
+        local t = io.loaddata(filename)
+        if t then
+            t = loadstring(t)
+            if t then t = t() end
+            for key, value in pairs(t) do
+                environment.arguments[key] = value
+            end
+        end
+    end
+end
+
+function resolvers.launch(str)
+    -- maybe we also need to test on mtxrun.launcher.suffix environment
+    -- variable or on windows consult the assoc and ftype vars and such
+    local launchers = runners.launchers[os.platform] if launchers then
+        local suffix = file.extname(str) if suffix then
+            local runner = launchers[suffix] if runner then
+                str = runner .. " " .. str
+            end
+        end
+    end
+    os.launch(str)
+end
+
+function runners.launch_file(filename)
+    instance.allresults = true
+    logs.setverbose(true)
+    local pattern = environment.arguments["pattern"]
+    if not pattern or pattern == "" then
+        pattern = filename
+    end
+    if not pattern or pattern == "" then
+        logs.simple("provide name or --pattern=")
+    else
+        local t = resolvers.find_files(pattern)
+        if not t or #t == 0 then
+            t = resolvers.find_files("*/" .. pattern)
+        end
+        if not t or #t == 0 then
+            t = resolvers.find_files("*/" .. pattern .. "*")
+        end
+        if t and #t > 0 then
+            if environment.arguments["all"] then
+                for _, v in pairs(t) do
+                    logs.simple("launching %s", v)
+                    resolvers.launch(v)
+                end
+            else
+                logs.simple("launching %s", t[1])
+                resolvers.launch(t[1])
+            end
+        else
+            logs.simple("no match for %s", pattern)
+        end
+    end
+end
+
+function runners.find_mtx_script(filename)
+    local function found(name)
+        local path = file.dirname(name)
+        if path and path ~= "" then
+            return false
+        else
+            local fullname = own and own.path and file.join(own.path,name)
+            return io.exists(fullname) and fullname
+        end
+    end
+    filename = file.addsuffix(filename,"lua")
+    local basename = file.removesuffix(file.basename(filename))
+    local suffix = file.extname(filename)
+    -- qualified path, raw name
+    local fullname = file.is_qualified_path(filename) and io.exists(filename) and filename
+    if fullname and fullname ~= "" then
+        return fullname
+    end
+    -- current path, raw name
+    fullname = "./" .. filename
+    fullname = io.exists(fullname) and fullname
+    if fullname and fullname ~= "" then
+        return fullname
+    end
+    -- mtx- prefix checking
+    local mtxprefix = (filename:find("^mtx%-") and "") or "mtx-"
+    -- context namespace, mtx-<filename>
+    fullname = mtxprefix .. filename
+    fullname = found(fullname) or resolvers.find_file(fullname)
+    if fullname and fullname ~= "" then
+        return fullname
+    end
+    -- context namespace, mtx-<filename>s
+    fullname = mtxprefix .. basename .. "s" .. "." .. suffix
+    fullname = found(fullname) or resolvers.find_file(fullname)
+    if fullname and fullname ~= "" then
+        return fullname
+    end
+    -- context namespace, mtx-<filename minus trailing s>
+    fullname = mtxprefix .. basename:gsub("s$","") .. "." .. suffix
+    fullname = found(fullname) or resolvers.find_file(fullname)
+    if fullname and fullname ~= "" then
+        return fullname
+    end
+    -- context namespace, just <filename>
+    fullname = resolvers.find_file(filename)
+    return fullname
+end
+
+function runners.execute_ctx_script(filename)
+    local arguments = environment.arguments_after
+    local fullname = runners.find_mtx_script(filename) or ""
+    if file.extname(fullname) == "cld" then
+        -- handy in editors where we force --autopdf
+        logs.simple("running cld script: %s",filename)
+        table.insert(arguments,1,fullname)
+        table.insert(arguments,"--autopdf")
+        fullname = runners.find_mtx_script("context") or ""
+    end
+    -- retry after generate but only if --autogenerate
+    if fullname == "" and environment.argument("autogenerate") then -- might become the default
+        instance.renewcache = true
+        logs.setverbose(true)
+        resolvers.load()
+        --
+        fullname = runners.find_mtx_script(filename) or ""
+    end
+    -- that should do it
+    if fullname ~= "" then
+        local state = runners.prepare()
+        if state == 'error' then
+            return false
+        elseif state == 'skip' then
+            return true
+        elseif state == "run" then
+            -- load and save ... kind of undocumented
+            arg = { } for _,v in pairs(arguments) do arg[#arg+1] = resolvers.resolve(v) end
+            environment.initialize_arguments(arg)
+            local loadname = environment.arguments['load']
+            if loadname then
+                if type(loadname) ~= "string" then loadname = file.basename(fullname) end
+                loadname = file.replacesuffix(loadname,"cfg")
+                runners.load_script_session(loadname)
+            end
+            filename = environment.files[1]
+            if logs.verbose then
+                logs.simple("using script: %s\n",fullname)
+            end
+            environment.ownscript = fullname
+            dofile(fullname)
+            local savename = environment.arguments['save']
+            if savename then
+                local save_list = runners.save_list
+                if save_list and next(save_list) then
+                    if type(savename) ~= "string" then savename = file.basename(fullname) end
+                    savename = file.replacesuffix(savename,"cfg")
+                    runners.save_script_session(savename,save_list)
+                end
+            end
+            return true
+        end
+    else
+    --  logs.setverbose(true)
+        if filename == "" or filename == "help" then
+            local context = resolvers.find_file("mtx-context.lua")
+            logs.setverbose(true)
+            if context ~= "" then
+                local result = dir.glob((string.gsub(context,"mtx%-context","mtx-*"))) -- () needed
+                local valid = { }
+                table.sort(result)
+                for i=1,#result do
+                    local scriptname = result[i]
+                    local scriptbase = string.match(scriptname,".*mtx%-([^%-]-)%.lua")
+                    if scriptbase then
+                        local data = io.loaddata(scriptname)
+                        local banner, version = string.match(data,"[\n\r]logs%.extendbanner%s*%(%s*[\"\']([^\n\r]+)%s*(%d+%.%d+)")
+                        if banner then
+                            valid[#valid+1] = { scriptbase, version, banner }
+                        end
+                    end
+                end
+                if #valid > 0 then
+                    logs.reportbanner()
+                    logs.reportline()
+                    logs.simple("no script name given, known scripts:")
+                    logs.simple()
+                    for k=1,#valid do
+                        local v = valid[k]
+                        logs.simple("%-12s  %4s  %s",v[1],v[2],v[3])
+                    end
+                end
+            else
+                logs.simple("no script name given")
+            end
+        else
+            filename = file.addsuffix(filename,"lua")
+            if file.is_qualified_path(filename) then
+                logs.simple("unknown script '%s'",filename)
+            else
+                logs.simple("unknown script '%s' or 'mtx-%s'",filename,filename)
+            end
+        end
+        return false
+    end
+end
+
+function runners.prefixes()
+    logs.reportbanner()
+    logs.reportline()
+    logs.simple(table.concat(resolvers.allprefixes(true)," "))
+end
+
+function runners.timedrun(filename) -- just for me
+    if filename and filename ~= "" then
+        runners.timed(function() os.execute(filename) end)
+    end
+end
+
+function runners.timed(action)
+    statistics.timed(action)
+end
+
+-- this is a bit dirty ... first we store the first filename and next we
+-- split the arguments so that we only see the ones meant for this script
+-- ... later we will use the second half
+
+local filename = environment.files[1] or ""
+local ok      = true
+
+local before, after = environment.split_arguments(filename)
+environment.arguments_before, environment.arguments_after = before, after
+environment.initialize_arguments(before)
+
+instance.engine   = environment.argument("engine")   or 'luatex'
+instance.progname = environment.argument("progname") or 'context'
+instance.lsrmode  = environment.argument("lsr")      or false
+
+-- maybe the unset has to go to this level
+
+local is_mkii_stub = runners.registered[file.removesuffix(file.basename(filename))]
+
+if environment.argument("usekpse") or environment.argument("forcekpse") or is_mkii_stub then
+
+    os.setenv("engine","")
+    os.setenv("progname","")
+
+    local remapper = {
+        otf = "opentype fonts",
+        ttf = "truetype fonts",
+        ttc = "truetype fonts",
+        pfb = "type1 fonts",
+        other = "other text files",
+    }
+
+    local function kpse_initialized()
+        texconfig.kpse_init = true
+        local t = os.clock()
+        local k = kpse.original.new("luatex",instance.progname)
+        local dummy = k:find_file("mtxrun.lua") -- so that we're initialized
+        logs.simple("kpse fallback with progname '%s' initialized in %s seconds",instance.progname,os.clock()-t)
+        kpse_initialized = function() return k end
+        return k
+    end
+
+    local find_file = resolvers.find_file
+    local show_path = resolvers.show_path
+
+    if environment.argument("forcekpse") then
+
+        function resolvers.find_file(name,kind)
+            return (kpse_initialized():find_file(resolvers.clean_path(name),(kind ~= "" and (remapper[kind] or kind)) or "tex") or "") or ""
+        end
+        function resolvers.show_path(name)
+            return (kpse_initialized():show_path(name)) or ""
+        end
+
+    elseif environment.argument("usekpse") or is_mkii_stub then
+
+        resolvers.load()
+
+        function resolvers.find_file(name,kind)
+            local found = find_file(name,kind) or ""
+            if found ~= "" then
+                return found
+            else
+                return (kpse_initialized():find_file(resolvers.clean_path(name),(kind ~= "" and (remapper[kind] or kind)) or "tex") or "") or ""
+            end
+        end
+        function resolvers.show_path(name)
+            local found = show_path(name) or ""
+            if found ~= "" then
+                return found
+            else
+                return (kpse_initialized():show_path(name)) or ""
+            end
+        end
+
+    end
+
+else
+
+    resolvers.load()
+
+end
+
+if environment.argument("selfmerge") then
+    -- embed used libraries
+    utils.merger.selfmerge(own.name,own.libs,own.list)
+elseif environment.argument("selfclean") then
+    -- remove embedded libraries
+    utils.merger.selfclean(own.name)
+elseif environment.argument("selfupdate") then
+    logs.setverbose(true)
+    resolvers.update_script(own.name,"mtxrun")
+elseif environment.argument("ctxlua") or environment.argument("internal") then
+    -- run a script by loading it (using libs)
+    ok = runners.execute_script(filename,true)
+elseif environment.argument("script") or environment.argument("scripts") then
+    -- run a script by loading it (using libs), pass args
+    if is_mkii_stub then
+    -- execute mkii script
+        ok = runners.execute_script(filename,false,true)
+    else
+        ok = runners.execute_ctx_script(filename)
+    end
+elseif environment.argument("execute") then
+    -- execute script
+    ok = runners.execute_script(filename)
+elseif environment.argument("direct") then
+    -- equals bin:
+    ok = runners.execute_program(filename)
+elseif environment.argument("edit") then
+    -- edit file
+    runners.edit_script(filename)
+elseif environment.argument("launch") then
+    runners.launch_file(filename)
+elseif environment.argument("make") then
+    -- make stubs
+    runners.handle_stubs(true)
+elseif environment.argument("remove") then
+    -- remove stub
+    runners.handle_stubs(false)
+elseif environment.argument("resolve") then
+    -- resolve string
+    runners.resolve_string(filename)
+elseif environment.argument("locate") then
+    -- locate file
+    runners.locate_file(filename)
+elseif environment.argument("platform")then
+    -- locate platform
+    runners.locate_platform()
+elseif environment.argument("prefixes") then
+    runners.prefixes()
+elseif environment.argument("timedrun") then
+    -- locate platform
+    runners.timedrun(filename)
+elseif environment.argument("help") or filename=='help' or filename == "" then
+    logs.help(messages.help)
+    -- execute script
+elseif filename:find("^bin:") then
+    ok = runners.execute_program(filename)
+elseif is_mkii_stub then
+    -- execute mkii script
+    ok = runners.execute_script(filename,false,true)
+else
+    ok = runners.execute_ctx_script(filename)
+    if not ok then
+        ok = runners.execute_script(filename)
+    end
+end
+
+if os.platform == "unix" then
+    io.write("\n")
+end
+
+if ok == false then ok = 1 elseif ok == true then ok = 0 end
+
+os.exit(ok)
diff --git a/scripts/context/lua/mtxrun.rme b/scripts/context/lua/mtxrun.rme
new file mode 100644
index 000000000..9850e389d
--- /dev/null
+++ b/scripts/context/lua/mtxrun.rme
@@ -0,0 +1,18 @@
+On MSWindows the mtxrun.lua script is called with
+mtxrun.exe. On Unix you can either rename mtxrun.lua
+to mtxrun, or use a symlink.
+
+You can create additional stubs, like
+
+copy mtxrun.exe luatools.exe
+copy mtxrun.exe texexec.exe
+copy mtxrun.exe context.exe
+copy mtxrun.exe mtx-server.exe
+
+The mtxrun.exe program is rather dump and only
+intercepts mtxrun, luatools and texmfstart (for
+old times sake) and passes the buck to mtxrun.lua
+which happens to know enough of mkii to deal
+with kpse based lookups and therefore acts like
+texmfstart but when used with mkiv it behaves
+more clever and looks for more.
diff --git a/scripts/context/lua/x-ldx.lua b/scripts/context/lua/x-ldx.lua
new file mode 100644
index 000000000..e0f21d68c
--- /dev/null
+++ b/scripts/context/lua/x-ldx.lua
@@ -0,0 +1,322 @@
+--[[ldx--
+<source>Lua Documentation Module</source>
+
+This file is part of the <logo label='context'/> documentation suite and
+itself serves as an example of using <logo label='lua'/> in combination
+with <logo label='tex'/>.
+
+I will rewrite this using lpeg once I have the time to study that nice new
+subsystem. On the other hand, we cannot expect proper <logo label='tex'/>
+ad for educational purposed the syntax migh be wrong.
+--ldx]]--
+
+banner = "version 1.0.1 - 2007+ - PRAGMA ADE / CONTEXT"
+
+--[[
+This script needs a few libraries. Instead of merging the code here
+we can use
+
+<typing>
+mtxrun --internal x-ldx.lua
+</typing>
+
+That way, the libraries included in the runner will be used.
+]]--
+
+-- libraries l-string.lua l-table.lua l-io.lua l-file.lua
+
+-- begin library merge
+-- end library merge
+
+--[[
+Just a demo comment line. We will handle such multiline comments but
+only when they start and end at the beginning of a line. More rich
+comments are tagged differently.
+]]--
+
+--[[ldx--
+First we define a proper namespace for this module. The <q>l</q> stands for
+<logo label='lua'/>, the <q>d</q> for documentation and the <q>x</q> for
+<logo label='xml'/>.
+--ldx]]--
+
+if not ldx then ldx = { } end
+
+--[[ldx--
+We load the lua file into a table. The entries in this table themselves are
+tables and have keys like <t>code</t> and <t>comment</t>.
+--ldx]]--
+
+function ldx.load(filename)
+    local data = file.readdata(filename)
+    local expr = "%s*%-%-%[%[ldx%-*%s*(.-)%s*%-%-ldx%]%]%-*%s*"
+    local i, j, t = 0, 0, { }
+    while true do
+        local comment, ni
+        ni, j, comment = data:find(expr, j)
+        if not ni then break end
+        t[#t+1] = { code = data:sub(i, ni-1) }
+        t[#t+1] = { comment = comment }
+        i = j + 1
+    end
+    local str = data:sub(i, #data)
+    str = str:gsub("^%s*(.-)%s*$", "%1")
+    if #str > 0 then
+        t[#t+1] = { code = str }
+    end
+    return t
+end
+
+--[[ldx--
+We will tag keywords so that we can higlight them using a special font
+or color. Users can extend this list when needed.
+--ldx]]--
+
+ldx.keywords = { }
+
+--[[ldx--
+Here come the reserved words:
+--ldx]]--
+
+ldx.keywords.reserved = {
+    ["and"]      = 1,
+    ["break"]    = 1,
+    ["do"]       = 1,
+    ["else"]     = 1,
+    ["elseif"]   = 1,
+    ["end"]      = 1,
+    ["false"]    = 1,
+    ["for"]      = 1,
+    ["function"] = 1,
+    ["if"]       = 1,
+    ["in"]       = 1,
+    ["local"]    = 1,
+    ["nil"]      = 1,
+    ["not"]      = 1,
+    ["or"]       = 1,
+    ["repeat"]   = 1,
+    ["return"]   = 1,
+    ["then"]     = 1,
+    ["true"]     = 1,
+    ["until"]    = 1,
+    ["while"]    = 1
+}
+
+--[[ldx--
+We need to escape a few tokens. We keep the hash local to the
+definition but set it up only once, hence the <key>do</key>
+construction.
+--ldx]]--
+
+do
+    local e = { [">"] = "&gt;", ["<"] = "&lt;", ["&"] = "&amp;" }
+    function ldx.escape(str)
+        return (str:gsub("([><&])",e))
+    end
+end
+
+--[[ldx--
+Enhancing the code is a bit tricky due to the fact that we have to
+deal with strings and escaped quotes within these strings. Before we
+mess around with the code, we hide the strings, and after that we
+insert them again. Single and double quoted strings are tagged so
+that we can use a different font to highlight them.
+--ldx]]--
+
+ldx.make_index = true
+
+function ldx.enhance(data) -- i need to use lpeg and then we can properly autoindent -)
+    local e = ldx.escape
+    for k=1,#data do
+        local v = data[k]
+        if v.code then
+            local dqs, sqs, com, cmt, cod = { }, { }, { }, { }, e(v.code)
+            cod = cod:gsub('\\"', "##d##")
+            cod = cod:gsub("\\'", "##s##")
+            cod = cod:gsub("%-%-%[%[.-%]%]%-%-", function(s)
+                cmt[#cmt+1] = s
+                return "<l<<<".. #cmt ..">>>l>"
+            end)
+            cod = cod:gsub("%-%-([^\n]*)", function(s)
+                com[#com+1] = s
+                return "<c<<<".. #com ..">>>c>"
+            end)
+            cod = cod:gsub("(%b\"\")", function(s)
+                dqs[#dqs+1] = s:sub(2,-2) or ""
+                return "<d<<<".. #dqs ..">>>d>"
+            end)
+            cod = cod:gsub("(%b\'\')", function(s)
+                sqs[#sqs+1] = s:sub(2,-2) or ""
+                return "<s<<<".. #sqs ..">>>s>"
+            end)
+            cod = cod:gsub("(%a+)",function(key)
+                local class = ldx.keywords.reserved[key]
+                if class then
+                    return "<key class='" .. class .. "'>" .. key .. "</key>"
+                else
+                    return key
+                end
+            end)
+            cod = cod:gsub("<s<<<(%d+)>>>s>", function(s)
+                return "<sqs>" .. sqs[tonumber(s)] .. "</sqs>"
+            end)
+            cod = cod:gsub("<d<<<(%d+)>>>d>", function(s)
+                return "<dqs>" .. dqs[tonumber(s)] .. "</dqs>"
+            end)
+            cod = cod:gsub("<c<<<(%d+)>>>c>", function(s)
+                return "<com>" .. com[tonumber(s)] .. "</com>"
+            end)
+            cod = cod:gsub("<l<<<(%d+)>>>l>", function(s)
+                return cmt[tonumber(s)]
+            end)
+            cod = cod:gsub("##d##", "\\\"")
+            cod = cod:gsub("##s##", "\\\'")
+            if ldx.make_index then
+                local lines = cod:split("\n")
+                local f = "(<key class='1'>function</key>)%s+([%w%.]+)%s*%("
+                for k=1,#lines do
+                    local v = lines[k]
+                    -- functies
+                    v = v:gsub(f,function(key, str)
+                        return "<function>" .. str .. "</function>("
+                    end)
+                    -- variables
+                    v = v:gsub("^([%w][%w%,%s]-)(=[^=])",function(str, rest)
+                        local t = string.split(str, ",%s*")
+                        for k=1,#t do
+                            t[k] = "<variable>" .. t[k] .. "</variable>"
+                        end
+                        return table.join(t,", ") .. rest
+                    end)
+                    -- so far
+                    lines[k] = v
+                end
+                v.code = table.concat(lines,"\n")
+            else
+                v.code = cod
+            end
+        end
+    end
+end
+
+--[[ldx--
+We're now ready to save the file in <logo label='xml'/> format. This boils
+down to wrapping the code and comment as well as the whole document. We tag
+lines in the code as such so that we don't need messy <t>CDATA</t> constructs
+and by calculating the indentation we also avoid space troubles. It also makes
+it possible to change the indentation afterwards.
+--ldx]]--
+
+function ldx.as_xml(data) -- ldx: not needed
+    local t, cmode = { }, false
+    t[#t+1] = "<?xml version='1.0' standalone='yes'?>\n"
+    t[#t+1] = "\n<document xmlns:ldx='http://www.pragma-ade.com/schemas/ldx.rng' xmlns='http://www.pragma-ade.com/schemas/ldx.rng'>\n"
+    for k=1,#data do
+        local v = data[k]
+        if v.code and not v.code:is_empty() then
+            t[#t+1] = "\n<code>\n"
+            local split = v.code:split("\n")
+            for k=1,#split do -- make this faster
+                local v = split[k]
+                local a, b = v:find("^(%s+)")
+                if v then v = v:gsub("[\n\r ]+$","") end
+                if a and b then
+                    v = v:sub(b+1,#v)
+                    if cmode then
+                        t[#t+1] = "<line comment='yes' n='" .. b .. "'>" .. v .. "</line>\n"
+                    else
+                        t[#t+1] = "<line n='" .. b .. "'>" .. v .. "</line>\n"
+                    end
+                elseif v:is_empty() then
+                    if cmode then
+                        t[#t+1] = "<line comment='yes'/>\n"
+                    else
+                        t[#t+1] = "<line/>\n"
+                    end
+                elseif v:find("^%-%-%[%[") then
+                    t[#t+1] = "<line comment='yes'>" .. v .. "</line>\n"
+                    cmode= true
+                elseif v:find("^%]%]%-%-") then
+                    t[#t+1] = "<line comment='yes'>" .. v .. "</line>\n"
+                    cmode= false
+                elseif cmode then
+                    t[#t+1] = "<line comment='yes'>" .. v .. "</line>\n"
+                else
+                    t[#t+1] = "<line>" .. v .. "</line>\n"
+                end
+            end
+            t[#t+1] = "</code>\n"
+        elseif v.comment then
+            t[#t+1] = "\n<comment>\n" .. v.comment .. "\n</comment>\n"
+        else
+            -- cannot happen
+        end
+    end
+    t[#t+1] = "\n</document>\n"
+    return table.concat(t,"")
+end
+
+--[[ldx--
+Saving the result is a trivial effort.
+--ldx]]--
+
+function ldx.save(filename,data)
+    file.savedata(filename,ldx.as_xml(data))
+end
+
+--[[ldx--
+The next function wraps it all in one call:
+--ldx]]--
+
+function ldx.convert(luaname,ldxname)
+    if not file.isreadable(luaname) then
+        luaname = luaname .. ".lua"
+    end
+    if file.isreadable(luaname) then
+        if not ldxname then
+            ldxname = file.replacesuffix(luaname,"ldx")
+        end
+        local data = ldx.load(luaname)
+        if data then
+            ldx.enhance(data)
+            if ldxname ~= luaname then
+                ldx.save(ldxname,data)
+            end
+        end
+    end
+end
+
+--[[ldx--
+This module can be used directly:
+
+<typing>
+mtxrun --internal x-ldx somefile.lua
+</typing>
+
+will produce an ldx file that can be processed with <logo label='context'/>
+by running:
+
+<typing>
+texexec --use=x-ldx --forcexml somefile.ldx
+</typing>
+
+You can do this in one step by saying:
+
+<typing>
+texmfstart texexec --ctx=x-ldx somefile.lua
+</typing>
+
+This will trigger <logo label='texexec'/> into loading the mentioned
+<logo label='ctx'/> file. That file describes the conversion as well
+as the module to be used.
+
+The main conversion call is:
+--ldx]]--
+
+-- todo: assume usage of "mtxrun --script x-ldx", maybe make it mtx-ldx
+
+if arg and arg[1] then
+    ldx.convert(arg[1],arg[2])
+end
+
+--~ exit(1)
diff --git a/scripts/context/perl/makempy.pl b/scripts/context/perl/makempy.pl
new file mode 100644
index 000000000..7cba2e1a6
--- /dev/null
+++ b/scripts/context/perl/makempy.pl
@@ -0,0 +1,361 @@
+eval '(exit $?0)' && eval 'exec perl -S $0 ${1+"$@"}' && eval 'exec perl -S $0 $argv:q'
+        if 0;
+
+#D \module
+#D   [       file=makempy.pl,
+#D        version=2000.12.14,
+#D          title=\METAFUN,
+#D       subtitle=\METAPOST\ Text Graphics,
+#D         author=Hans Hagen,
+#D           date=\currentdate,
+#D      copyright={PRAGMA / Hans Hagen \& Ton Otten}]
+#C
+#C This module is part of the \CONTEXT\ macro||package and is
+#C therefore copyrighted by \PRAGMA. See licen-en.pdf for
+#C details.
+
+# Tobias Burnus provided the code needed to proper testing
+# of binaries on UNIX as well as did some usefull suggestions
+# to improve the functionality.
+
+# This script uses GhostScript and PStoEdit as well as
+# pdfTeX, and if requested TeXEdit and ConTeXt.
+
+# todo: we can nowadays do without the intermediate step, because GS
+# can now handle PDF quite good
+
+use Getopt::Long ;
+use Config ;
+use strict ;
+
+$Getopt::Long::passthrough = 1 ; # no error message
+$Getopt::Long::autoabbrev  = 1 ; # partial switch accepted
+
+my $help    = 0 ;
+my $silent  = 0 ;
+my $force   = 0 ;
+my $noclean = 0 ;
+
+my $amethod = my $pmethod = my $gmethod = 0 ;
+
+my $format  = "plain" ; # can be "context" for plain users too
+
+&GetOptions
+  ( "help"        => \$help    ,
+    "silent"      => \$silent  ,
+    "force"       => \$force   ,
+    "pdftops"     => \$pmethod ,
+    "xpdf"        => \$pmethod ,
+    "gs"          => \$gmethod ,
+    "ghostscript" => \$gmethod ,
+    "noclean"     => \$noclean ) ;
+
+my $mpochecksum = 0 ;
+
+my %tex ; my %start ; my %stop ;
+
+$tex{plain}     = "pdftex" ;
+$tex{latex}     = "pdflatex" ;
+$tex{context}   = "texexec --batch --once --interface=en --pdf" ;
+
+$start{plain}   = '' ;
+$stop{plain}    = '\end' ;
+
+$start{latex}   = '\begin{document}' ;
+$stop{latex}    = '\end{document}' ;
+
+$start{context} = '\starttext' ;
+$stop{context}  = '\stoptext' ;
+
+my $ghostscript  = "" ;
+my $pstoedit     = "" ;
+my $pdftops      = "" ;
+my $acroread     = "" ;
+
+my $wereondos    = ($Config{'osname'} =~ /dos|mswin/io) ;
+
+# Unix only: assume that "gs" in the path. We could also
+# use $ghostscipt = system "which gs" but this would require
+# that which is installedd on the system.
+
+sub checkenv
+  { my ($var, $env) = @_ ;
+    if ($var)
+      { return $var }
+    elsif ($ENV{$env})
+      { return $ENV{$env} }
+    else
+      { return $var } }
+
+$ghostscript = checkenv ($ghostscript, "GS_PROG" ) ;
+$ghostscript = checkenv ($ghostscript, "GS"      ) ;
+$pstoedit    = checkenv ($pstoedit   , "PSTOEDIT") ;
+$pdftops     = checkenv ($pdftops    , "PDFTOPS" ) ;
+$acroread    = checkenv ($acroread   , "ACROREAD") ;
+
+sub setenv
+  { my ($var, $unix, $win) = @_ ;
+    if ($var)
+      { return $var }
+    elsif ($wereondos)
+      { return $win }
+    else
+      { return $unix } }
+
+$ghostscript = setenv($ghostscript, "gs"      , "gswin32c") ;
+$pstoedit    = setenv($pstoedit   , "pstoedit", "pstoedit") ;
+$pdftops     = setenv($pdftops    , "pdftops" , "pdftops" ) ;
+$acroread    = setenv($acroread   , "acroread", ""        ) ;
+
+# Force a method if unknown.
+
+unless ($pmethod||$amethod||$gmethod)
+  { if ($wereondos) { $pmethod = 1 } else { $amethod = 1 } }
+
+# Set the error redirection used under Unix:
+# stderr -> stdout
+
+my $logredirection = '>>' ;
+
+# This unfortunally doesn't work with the ksh and simple sh
+#
+# if (!$wereondos)
+#   { $logredirection = '2>&1 >>' ; # Bash
+#     $logredirection = '>>&' ;     # tcsh, Bash
+#     default $logredirection. }
+
+# Some TeX Code Snippets.
+
+my $macros = '
+
+% auxiliary macros
+
+\input supp-mis.tex
+
+\def\startTEXpage[scale=#1]%
+  {\output{}
+   \batchmode
+   \pdfoutput=1
+   \pdfcompresslevel=9
+   \hoffset=-1in
+   \voffset=\hoffset
+   \scratchcounter=#1
+   \divide\scratchcounter1000
+   \edef\TEXscale{\the\scratchcounter\space}
+   \forgetall
+   \setbox0=\vbox\bgroup}
+
+\def\stopTEXpage
+  {\egroup
+   \dimen0=\ht0 \advance\dimen0 \dp0
+   \setbox2=\vbox to 10\dimen0
+     {\pdfliteral{\TEXscale 0 0 \TEXscale 0 0 cm}
+      \copy0
+      \pdfliteral{1 0 0 1 0 0 cm}
+      \vfill}
+   \wd2=10\wd0
+   \pdfpageheight=\ht2
+   \pdfpagewidth=\wd2
+   \ScaledPointsToBigPoints{\number\pdfpageheight}\pdfcropheight
+   \ScaledPointsToBigPoints{\number\pdfpagewidth }\pdfcropwidth
+   \expanded{\pdfpageattr{/CropBox [0 0 \pdfcropwidth \space \pdfcropheight]}}
+   \shipout\hbox{\box2}}
+
+% end of auxiliary macros' ;
+
+sub report
+  { return if $silent ;
+    my $str = shift ;
+    if ($str =~ /(.*?)\s+([\:\/])\s+(.*)/o)
+      { if ($1 eq "") { $str = " " } else { $str = $2 }
+        print sprintf("%22s $str %s\n",$1,$3) } }
+
+sub error
+  { report("processing aborted : " . shift) ;
+    exit }
+
+sub process
+  { report("generating : " . shift) }
+
+sub banner
+  { return if $silent ;
+    print "\n" ;
+    report ("MakeMPY 1.1 - MetaFun / PRAGMA ADE 2000-2004") ;
+    print "\n" }
+
+my $metfile = "" ; # main metapost file
+my $mpofile = "" ; # metapost text specifiation file (provided)
+my $mpyfile = "" ; # metapost text picture file (generated)
+my $texfile = "" ; # temporary tex file
+my $pdffile = "" ; # temporary pdf file
+my $tmpfile = "" ; # temporary metapost file
+my $posfile = "" ; # temporary postscript file
+my $logfile = "" ; # temporary log file
+my $errfile = "" ; # final log file (with suffix log)
+
+sub show_help_info
+  { banner ;
+    report ("--help : this message" ) ;
+    report ("--noclean : don't remove temporary files" ) ;
+    report ("--force : force processing (ignore checksum)" ) ;
+    report ("--silent : don't show messages" ) ;
+    print "\n" ;
+    report ("--pdftops : use pdftops (xpdf) pdf->ps") ;
+    report ("--ghostscript : use ghostscript (gs) for pdf->ps") ;
+    print "\n" ;
+    report ("input file : metapost file with graphics") ;
+    report ("programs needed : texexec and english context") ;
+    report (" : pdftops from the xpdf suite, or") ; # page size buggy
+    report (" : pdf2ps and ghostscript, and") ;
+    report (" : pstoedit and ghostscript") ;
+    report ("output file : metapost file with pictures") ;
+    exit }
+
+sub check_input_file
+  { my $file = $ARGV[0] ;
+    if ((!defined($file))||($file eq ""))
+      { banner ; error("no filename given") }
+    else
+      { $file =~ s/\.mp.*$//o ;
+        $metfile =     "$file.mp"  ;
+        $mpofile =     "$file.mpo" ;
+        $mpyfile =     "$file.mpy" ;
+        $logfile =     "$file.log" ;
+        $texfile = "mpy-$file.tex" ;
+        $pdffile = "mpy-$file.pdf" ;
+        $posfile = "mpy-$file.pos" ;
+        $tmpfile = "mpy-$file.tmp" ;
+        $errfile = "mpy-$file.log" ;
+        if (! -f $metfile)
+          { banner ; error("$metfile is empty") }
+        elsif (-s $mpofile < 32)
+          { unlink $mpofile ; # may exist with zero length
+            unlink $mpyfile ; # get rid of left overs
+            exit }
+        else
+          { banner ; report("processing file : $mpofile") } } }
+
+sub verify_check_sum # checksum calculation from perl documentation
+  { return unless (open (MPO,"$mpofile")) ;
+    $mpochecksum = do { local $/ ; unpack("%32C*",<MPO>) % 65535 } ;
+    close (MPO) ;
+    return unless open (MPY,"$mpyfile") ;
+    my $str = <MPY> ; chomp $str ;
+    close (MPY) ;
+    if ($str =~ /^\%\s*mpochecksum\s*\:\s*(\d+)/o)
+      { if ($mpochecksum eq $1)
+          { report("mpo checksum : $mpochecksum / unchanged") ;
+            exit unless $force }
+        else
+          { report("mpo checksum : $mpochecksum / changed") } } }
+
+sub cleanup_files
+  { my @files = <mpy-*.*> ;
+    foreach (@files) { unless (/\.log/o) { unlink $_ } } }
+
+sub construct_tex_file
+  { my $n = 0 ;
+    unless (open (MPO, "<$mpofile"))
+      { error("unable to open $mpofile") }
+    unless (open (TEX, ">$texfile"))
+      { error("unable to open $texfile") }
+    my $textext = "" ;
+    while (<MPO>)
+     { s/\s*$//mois ;
+       if (/\%\s*format=(\w+)/)
+         { $format = $1 }
+       else # if (!/^\%/)
+         { if (/startTEXpage/o)
+             { ++$n ;
+               $textext .= "$start{$format}\n" ;
+               $start{$format} = "" }
+           $textext .= "$_\n" } }
+    unless (defined($tex{$format})) { $format = "plain" }
+    if ($format eq "context") { $macros = "" }
+  # print TEX "$start{$format}\n$macros\n$textext\n$stop{$format}\n" ;
+    print TEX "$start{$format}\n\n" if $start{$format} ;
+    print TEX "$macros\n"           if $macros ;
+    print TEX "$textext\n"          if $textext ;
+    print TEX "$stop{$format}\n"    if $stop{$format} ;
+    close (MPO) ;
+    close (TEX) ;
+    report("tex format : $format") ;
+    report("requested texts : $n") }
+
+sub construct_mpy_file
+  { unless (open (TMP, "<$tmpfile"))
+      { error("unable to open $tmpfile file") }
+    unless (open (MPY, ">$mpyfile"))
+      { error("unable to open $mpyfile file") }
+    print MPY "% mpochecksum : $mpochecksum\n" ;
+    my $copying = my $n = 0 ;
+    while (<TMP>) # a simple sub is faster
+     { if (s/beginfig/begingraphictextfig/o)
+         { print MPY $_ ; $copying = 1 ; ++$n }
+       elsif (s/endfig/endgraphictextfig/o)
+         { print MPY $_ ; $copying = 0 }
+       elsif ($copying)
+         { print MPY $_ } }
+   close (TMP) ;
+   close (MPY) ;
+   report("processed texts : $n") ;
+   report("produced file : $mpyfile") }
+
+sub run
+  { my ($resultfile, $program,$arguments) = @_ ;
+    my $result = system("$program $arguments $logredirection $logfile") ;
+    unless (-f $resultfile) { error("invalid `$program' run") } }
+
+sub make_pdf_pages
+  { process ("pdf file") ;
+    run ($pdffile, "$tex{$format}", "$texfile") }
+
+sub make_mp_figures
+  { process ("postscript file") ;
+    if ($pmethod) { run($posfile, "$pdftops",
+      "-paper match $pdffile $posfile") }
+    if ($gmethod) { run($posfile, "$ghostscript",
+      "-q -sOutputFile=$posfile -dNOPAUSE -dBATCH -dSAFER -sDEVICE=pswrite $pdffile") }
+    if ($amethod) { run($posfile, "$acroread",
+      "-toPostScript -pairs $pdffile $posfile") } }
+
+sub make_mp_pictures_ps
+  { process ("metapost file") ;
+    run ($tmpfile, "$pstoedit", "-ssp -dt -f mpost $posfile $tmpfile") }
+
+sub make_mp_pictures_pdf
+  { process ("metapost file") ;
+    run ($tmpfile, "$pstoedit", "-ssp -dt -f mpost $pdffile $tmpfile") }
+
+if ($help) { show_help_info }
+
+check_input_file ;
+verify_check_sum ;
+cleanup_files ;
+construct_tex_file ;
+make_pdf_pages ;
+if (1)
+  { make_mp_pictures_pdf ; }
+else
+  { make_mp_figures ;
+    make_mp_pictures_ps ; }
+construct_mpy_file ; # less save : rename $tmpfile, $mpyfile ;
+unless ($noclean) { cleanup_files }
+
+# a simple test file (needs context)
+#
+# % output=pdftex
+#
+# \starttext
+#
+# \startMPpage
+#   graphictext
+#     "\bf MAKE"
+#     scaled 8
+#     zscaled (1,2)
+#     withdrawcolor \MPcolor{blue}
+#     withfillcolor \MPcolor{gray}
+#     withpen pencircle scaled 5pt ;
+# \stopMPpage
+#
+# \stoptext
diff --git a/scripts/context/perl/mptopdf.pl b/scripts/context/perl/mptopdf.pl
new file mode 100644
index 000000000..41d1ae1f7
--- /dev/null
+++ b/scripts/context/perl/mptopdf.pl
@@ -0,0 +1,160 @@
+eval '(exit $?0)' && eval 'exec perl -S $0 ${1+"$@"}' && eval 'exec perl -S $0 $argv:q'
+        if 0;
+
+# MikTeX users can set environment variable TEXSYSTEM to "miktex".
+
+#D \module
+#D   [       file=mptopdf.pl,
+#D        version=2000.05.29,
+#D          title=converting MP to PDF,
+#D       subtitle=\MPTOPDF,
+#D         author=Hans Hagen,
+#D           date=\currentdate,
+#D            url=www.pragma-ade.nl,
+#D      copyright={PRAGMA ADE / Hans Hagen \& Ton Otten}]
+#C
+#C This module is part of the \CONTEXT\ macro||package and is
+#C therefore copyrighted by \PRAGMA. See licen-en.pdf for
+#C details.
+
+# use File::Copy ; # not in every perl
+
+use Config ;
+use Getopt::Long ;
+use strict ;
+use File::Basename ;
+
+$Getopt::Long::passthrough = 1 ; # no error message
+$Getopt::Long::autoabbrev  = 1 ; # partial switch accepted
+
+my $Help = my $Latex = my $RawMP = my $MetaFun = 0 ;
+my $PassOn = '' ;
+
+&GetOptions
+  ( "help"    => \$Help  ,
+    "rawmp"   => \$RawMP,
+    "metafun" => \$MetaFun,
+    "passon"  => \$PassOn,
+    "latex"   => \$Latex ) ;
+
+my $program = "MPtoPDF 1.3.3" ;
+my $pattern = "@ARGV" ; # was $ARGV[0]
+my $miktex  = 0 ;
+my $done    = 0 ;
+my $report  = '' ;
+my $mplatexswitch = " --tex=latex " ;
+
+my $dosish      = ($Config{'osname'} =~/^(ms)?dos|^os\/2|^mswin/i) ;
+my $escapeshell = (($ENV{'SHELL'}) && ($ENV{'SHELL'} =~ m/sh/i ));
+
+if ($ENV{"TEXSYSTEM"}) {
+    $miktex = ($ENV{"TEXSYSTEM"} =~ /miktex/io) ;
+}
+
+my @files ;
+my $command = my $mpbin = ''  ;
+
+# agressive copy, works for open files like in gs
+
+sub CopyFile {
+    my ($From,$To) = @_ ;
+    return unless open(INP,"<$From") ;
+    return unless open(OUT,">$To") ;
+    binmode INP ;
+    binmode OUT ;
+    while (<INP>) {
+        print OUT $_ ;
+    }
+    close (INP) ;
+    close (OUT) ;
+}
+
+if (($pattern eq '')||($Help)) {
+    print "\n$program : provide MP output file (or pattern)\n" ;
+    exit ;
+} elsif ($pattern =~ /\.mp$/io) {
+    shift @ARGV ; my $rest = join(" ", @ARGV) ;
+    if (open(INP,$pattern)) {
+        while (<INP>) {
+            if (/(documentstyle|documentclass|begin\{document\})/io) {
+                $Latex = 1 ; last ;
+            }
+        }
+        close (INP) ;
+    }
+    if ($RawMP) {
+        if ($Latex) {
+            $rest .= " $mplatexswitch" ;
+        }
+        if ($MetaFun) {
+            $mpbin = "mpost --progname=mpost --mem=metafun" ;
+        } else {
+            $mpbin = "mpost --mem=mpost" ;
+        }
+    } else {
+        if ($Latex) {
+            $rest .= " $mplatexswitch" ;
+            $mpbin = "mpost --mem=mpost" ;
+        } else {
+            $mpbin = "texexec --mptex $PassOn " ;
+        }
+    }
+    my $runner = "$mpbin $rest $pattern" ;
+    print "\n$program : running '$runner'\n" ;
+    my $error =  system ($runner) ;
+    if ($error) {
+        print "\n$program : error while processing mp file\n" ;
+        exit 1 ;
+    } else {
+        $pattern =~ s/\.mp$//io ;
+        @files = glob "$pattern.*" ;
+    }
+} elsif (-e $pattern) {
+    @files = ($pattern) ;
+} elsif ($pattern =~ /.\../o) {
+    @files = glob "$pattern" ;
+} else {
+    $pattern .= '.*' ;
+    @files = glob "$pattern" ;
+}
+
+foreach my $file (@files) {
+    $_ = $file ;
+    if (s/\.(\d+|mps)$// && -e $file) {
+        if ($miktex) {
+            $command = "pdftex -undump=mptopdf" ;
+        } else {
+            $command = "pdftex -fmt=mptopdf -progname=context" ;
+        }
+        if ($dosish) {
+            $command = "$command \\relax $file" ;
+        } else {
+            $command = "$command \\\\relax $file" ;
+        }
+        my $error = system($command) ;
+        if ($error) {
+            print "\n$program : error while processing tex file\n" ;
+            exit 1 ;
+        }
+        my $pdfsrc = basename($_).".pdf";
+        rename ($pdfsrc, "$_-$1.pdf") ;
+        if (-e $pdfsrc) {
+            CopyFile ($pdfsrc, "$_-$1.pdf") ;
+        }
+        if ($done) {
+            $report .= " +" ;
+        }
+        $report .= " $_-$1.pdf" ;
+        ++$done  ;
+    }
+}
+
+if ($report eq '') {
+    $report = '*' ;
+}
+
+if ($done) {
+    print "\n$program : $pattern is converted to$report\n" ;
+} else {
+    print "\n$program : no filename matches $pattern\n" ;
+}
diff --git a/scripts/context/perl/path_tre.pm b/scripts/context/perl/path_tre.pm
new file mode 100644
index 000000000..546afcd27
--- /dev/null
+++ b/scripts/context/perl/path_tre.pm
@@ -0,0 +1,36 @@
+#D \module
+#D   [       file=path\_tre.pm,
+#D        version=1999.05.05,
+#D          title=Path modules, 
+#D       subtitle=selecting a path, 
+#D         author=Hans Hagen,
+#D           date=\currentdate,
+#D      copyright={PRAGMA / Hans Hagen \& Ton Otten}]
+#C
+#C This module is part of the \CONTEXT\ macro||package and is
+#C therefore copyrighted by \PRAGMA. See licen-en.pdf for 
+#C details. 
+
+#D Not yet documented, source will be cleaned up. 
+
+package Tk::path_tre ; 
+
+use Tk;
+require Tk::DirTree ;
+
+use base  qw(Tk::DirTree);
+use strict;
+
+Construct Tk::Widget 'PathTree';
+
+sub ClassInit
+  { my ($class,$mw) = @_ ;
+    return $class -> SUPER::ClassInit ($mw) }
+
+sub dirnames
+  { my ( $w, $dir ) = @_ ;
+    unless ($dir=~/\//) { $dir .= '/' }
+    my @names = $w->Callback("-dircmd", $dir, $w->cget("-showhidden"));
+    return( @names ) }
+
+__END__
diff --git a/scripts/context/perl/pdftrimwhite.pl b/scripts/context/perl/pdftrimwhite.pl
new file mode 100644
index 000000000..6ac4f70c5
--- /dev/null
+++ b/scripts/context/perl/pdftrimwhite.pl
@@ -0,0 +1,525 @@
+eval '(exit $?0)' && eval 'exec perl -S $0 ${1+"$@"}' && eval 'exec perl -S $0 $argv:q'
+        if 0;
+
+#D \module
+#D   [       file=pdftrimwhite.pl,
+#D        version=2000.07.13,
+#D          title=PDF postprocessing,
+#D       subtitle=cropping whitespace from pdf files,
+#D         author=Hans Hagen,
+#D           date=\currentdate,
+#D      copyright=PRAGMA ADE]
+
+#C This module is part of the \CONTEXT\ macro||package and is
+#C therefore copyrighted by \PRAGMA. See readme.pdf for
+#C details.
+
+#D This script can be used to crop margins that contain
+#D useless information from a \PDF\ image. It does so by:
+#D
+#D \startitemize[packed,n]
+#D \som  cropping the image into an alternative file
+#D \som  determining the boundingbox of the alternative
+#D \som  cropping the image into a resulting file
+#D \stoppacked
+#D
+#D In the process, some checks are carried out. Step~1 is
+#D taken care of by \PDFTEX, step~2 by \GHOSTSCRIPT, using a
+#D file generated by \PDFTOPS, and \PDFTEX\ is responsible
+#D for step~3.
+#D
+#D \startuseMPgraphic{original}
+#D   numeric n ; n = 1cm ;
+#D   path p ; p := fullsquare xyscaled (8n,12n) ;
+#D   path q ; q := fullsquare xyscaled (2n,3n) shifted (n,n) ;
+#D   path r ; r := ((0,0)--(3n,0)) shifted (0, 5.5n) ;
+#D   path s ; s := ((0,0)--(3n,0)) shifted (0,-5.5n) ;
+#D   path t ; t := (-2n,-4n) ;
+#D   path u ; u := p enlarged -.75n ;
+#D   path v ; v := p enlarged (-1.75n,-2n) shifted (n,1.25n) ;
+#D   path w ; w := q enlarged .25n ;
+#D   fill p                               withcolor .7white ;
+#D   fill q                               withcolor .7green ;
+#D   draw r withpen pencircle scaled .25n withcolor .7green ;
+#D   draw s withpen pencircle scaled .25n withcolor .7green ;
+#D   draw t withpen pencircle scaled .50n withcolor .7green ;
+#D   draw u withpen pencircle scaled .10n withcolor   white ;
+#D   draw v withpen pencircle scaled .10n withcolor .7red   ;
+#D   draw w withpen pencircle scaled .10n ;
+#D   verbatimtex \tttf \setupframed[frame=off,align=left] etex ;
+#D   label    (btex \framed{crap}       etex, center   r) ;
+#D   label    (btex \framed{crap}       etex, center   s) ;
+#D   label    (btex \framed{crap}       etex, center   t) ;
+#D   label    (btex \framed{graphic}    etex, center   q) ;
+#D   label.urt(btex \framed{page}       etex, llcorner p) ;
+#D   label.urt(btex \framed{crop}       etex, llcorner u) ;
+#D   label.lft(btex \framed{leftcrop\\
+#D                          rightcrop\\
+#D                          topcrop\\
+#D                          bottomcrop} etex, .5[ulcorner v,llcorner v]) ;
+#D   label.bot(btex \framed{offset}     etex, .5[llcorner w,lrcorner w]) ;
+#D \stopuseMPgraphic
+#D
+#D \placefigure
+#D   [here][fig:pdftrimwhite]
+#D   {Crops and offsets.}
+#D   {\useMPgraphic{original}}
+#D
+#D The \TEX\ part has two alternatives, one using \CONTEXT, and
+#D another using plain \TEX. The \CONTEXT\ method is slower but
+#D can be extended more easily.
+#D
+#D The script is executed as follows:
+#D
+#D \starttyping
+#D pdftrimwhite <original> [<result>] [<switches>]
+#D \stoptyping
+#D
+#D The next call crops \type {test.pdf} to its natural
+#D boundingbox.
+#D
+#D \starttyping
+#D pdftrimwhite test
+#D \stoptyping
+#D
+#D If the file has some crap at the bottom, you can say:
+#D
+#D \starttyping
+#D pdftrimwhite test --bottomcrop=2cm
+#D \stoptyping
+#D
+#D This clips 2cm from the bottom. You can clip on all sides
+#D individually, in combination or at once, like in:
+#D
+#D \starttyping
+#D pdftrimwhite test --bottomcrop=2cm --crop=1cm
+#D \stoptyping
+#D
+#D The final result is a tightly cropped image. In order to get
+#D a 5mm margin around this image, you can say:
+#D
+#D \starttyping
+#D pdftrimwhite test --bottomcrop=2cm --offset=5mm
+#D \stoptyping
+#D
+#D By default, the script intercepts logging messages and
+#D writes them to a logfile with the same name as the
+#D resulting image and the prefix \type {log}. If no name is
+#D given, the name \type {pdftrimwhite} is used for all resulting
+#D files.
+#D
+#D By default, \CONTEXT\ is used. When installed properly, you
+#D can also use plain \TEX, by adding a switch \type
+#D {--plain}. Partial switched are accepted, so the next call
+#D is valid:
+#D
+#D \starttyping
+#D pdftrimwhite test result --bot=2cm --off=5mm --plain
+#D \stoptyping
+#D
+#D The current implementation uses an intermediate \POSTSCRIPT\
+#D file. This may change as \GHOSTSCRIPT\ gets more clever with
+#D \PDF\ files.
+#D
+#D In \in {figure} [fig:pdftrimwhite] the green rectangle is the
+#D picture we want to keep. Around this picture, we want a
+#D margin, represented by the black rectangle, and specified by
+#D \type {--offset}. The white rectangle is the cropbox
+#D defined by \type {--crop}. That way we get rid of header
+#D and footerlines. The red rectangle results from an
+#D additional \type {--leftcrop} and \type {-bottomcrop} and
+#D takes care of some content, as represented by the green
+#D dot.
+#D
+#D The \type {--verbose} switch can be used to disable the
+#D interception of log messages.
+
+#D We load a few \PERL\ modules \unknown\
+
+use Config ;
+use Getopt::Long ;
+
+use strict ;
+
+#D \unknown\ and initialize them.
+
+Getopt::Long::Configure
+  ("auto_abbrev",
+   "ignore_case",
+   "pass_through") ;
+
+#D Before fetching the switches, we initialize the
+#D variables.
+
+my $Crop       = "0cm" ;
+
+my $LeftCrop   = "0cm" ;
+my $RightCrop  = "0cm" ;
+my $TopCrop    = "0cm" ;
+my $BottomCrop = "0cm" ;
+
+my $Offset     = "0cm" ;
+
+my $GSbin      = "" ;
+my $Verbose    = 0 ;
+my $Help       = 0 ;
+my $UsePlain   = 0 ;
+
+my $Page       = 1 ;
+
+#D On \MSWINDOWS\ and \UNIX\ the following defaults, combined
+#D with the check later, should work out okay.
+
+my $pdfps = "pdftops" ;
+my $gs    = "gs" ;
+
+my $thisisunix  = $Config{'osname'} !~ /dos|mswin/i ;
+
+#D When no resulting file is given, we use \type {pdftrimwhite}
+#D as name (checked later).
+
+my $figurefile = "" ;
+my $resultfile = "" ;
+my $tempfile   = "" ;
+
+my $programname = "pdftrimwhite" ;
+
+#D Messages are temporarily saved and written to a log file
+#D afterwards.
+
+my $results = "" ;
+my $pipe    = "" ;
+my $result  = "" ;
+
+#D Unfortunately we need this information, first since
+#D \PDFTOPS\ does not honor the cropbox, and second because
+#D the vertical coordinated are swapped.
+
+my $pwidth   = 597 ;
+my $pheight  = 847 ;
+my $hoffset  =   0 ;
+my $voffset  =   0 ;
+
+#D A few more variables.
+
+my $width = my $height = my $llx = my $lly = my $urx = my $ury = 0 ;
+
+#D Here are the switches we accept. The \type {--gsbin} switch
+#D is a bonus one, and the \type {--help} switch comes
+#D naturally.
+
+&GetOptions
+  ( "leftcrop=s"   => \$LeftCrop  ,
+    "rightcrop=s"  => \$RightCrop ,
+    "topcrop=s"    => \$TopCrop   ,
+    "bottomcrop=s" => \$BottomCrop,
+    "crop=s"       => \$Crop      ,
+    "offset=s"     => \$Offset    ,
+    "verbose"      => \$Verbose   ,
+    "gsbin=s"      => \$GSbin     ,
+    "plain"        => \$UsePlain  ,
+    "page=i"       => \$Page      ,
+    "help"         => \$Help      ) ;
+
+#D If asked for, or if no file is given, we provide some
+#D help information.
+
+sub PrintHelp
+  { print "This is PdfTrimWhite\n\n" .
+          "usage:\n\n" .
+          "pdftrimwhite [switches] filename result\n\n" .
+          "switches:\n\n" .
+          "--crop=<dimen>\n" .
+          "--offset=<dimen>\n" .
+          "--leftcrop=<dimen>\n" .
+          "--rightcrop=<dimen>\n" .
+          "--topcrop=<dimen>\n" .
+          "--bottomcrop=<dimen>\n" .
+          "--gsbin=<string>\n" .
+          "--page=<number>\n" .
+          "--plain\n" .
+          "--verbose\n" }
+
+#D The preparations:
+
+sub GetItRight
+  { if ($Help)
+      { PrintHelp() ; exit }
+    $figurefile = $ARGV[0] ; $figurefile =~ s/\.pdf$//oi ;
+    $resultfile = $ARGV[1] ; $resultfile =~ s/\.pdf$//oi ;
+    $tempfile = "pdftrimwhite-$resultfile" ;
+    if ($figurefile eq '')
+      { PrintHelp() ; exit }
+    unless ($thisisunix)
+      { $gs = "gswin32c" }
+    if ($GSbin ne '')
+      { $gs = $GSbin }
+    unless (-e "$figurefile.pdf")
+      { print "Something is terribly wrong: no file found\n" ;
+        exit }
+    if (($resultfile eq '')||($resultfile=~/(^\-|\.)/io))
+      { $resultfile = $programname }
+    $pipe = "2>&1" ;
+    if ($thisisunix)
+      { $pipe = "2>&1" } }
+
+#D Something common.
+
+sub SavePageData
+  { return "% saving page data
+\\immediate\\openout\\scratchwrite=$figurefile.tmp
+\\immediate\\write\\scratchwrite
+   {\\HOffsetBP\\space\\VOffsetBP\\space
+    \\FigureWidthBP\\space\\FigureHeightBP}
+\\immediate\\closeout\\scratchwrite\n" }
+
+sub MakePageConTeXt
+  { return "% the real work
+\\definepapersize
+  [Crap]
+  [width=\\FigureWidth,
+   height=\\FigureHeight]
+\\setuppapersize
+  [Crap][Crap]
+\\setuplayout
+  [topspace=0cm,backspace=0pt,
+   height=middle,width=middle,
+   header=0pt,footer=0pt]
+\\starttext
+  \\startstandardmakeup
+    \\clip
+      [voffset=\\VOffset,
+       hoffset=\\HOffset,
+       width=\\FigureWidth,
+       height=\\FigureHeight]
+      {\\externalfigure[$figurefile.pdf][page=$Page]\\hss}
+  \\stopstandardmakeup
+\\stoptext\n" }
+
+sub MakePagePlainTeX
+  { return "% the real work
+\\output{}
+\\hoffset=-1in
+\\voffset=\\hoffset
+\\pdfpageheight=\\FigureHeight
+\\pdfpagewidth=\\FigureWidth
+\\vbox to \\pdfpageheight
+  {\\offinterlineskip
+   \\vskip-\\VOffset
+   \\hbox to \\pdfpagewidth{\\hskip-\\HOffset\\box0\\hss}
+   \\vss}
+\\end\n" }
+
+sub CalculateClip
+  { return "% some calculations
+\\dimen0=\\figurewidth
+\\dimen2=\\figureheight
+\\dimen4=$Crop
+\\dimen6=$Crop
+\\advance\\dimen4 by $LeftCrop
+\\advance\\dimen6 by $TopCrop
+\\advance\\dimen0 by -\\dimen4
+\\advance\\dimen0 by -$Crop
+\\advance\\dimen0 by -$RightCrop
+\\advance\\dimen2 by -\\dimen6
+\\advance\\dimen2 by -$Crop
+\\advance\\dimen2 by -$BottomCrop
+\\edef\\FigureWidth {\\the\\dimen0}
+\\edef\\FigureHeight{\\the\\dimen2}
+\\edef\\HOffset     {\\the\\dimen4}
+\\edef\\VOffset     {\\the\\dimen6}
+\\ScaledPointsToWholeBigPoints{\\number\\dimen0}\\FigureWidthBP
+\\ScaledPointsToWholeBigPoints{\\number\\dimen2}\\FigureHeightBP
+\\ScaledPointsToWholeBigPoints{\\number\\dimen4}\\HOffsetBP
+\\ScaledPointsToWholeBigPoints{\\number\\dimen6}\\VOffsetBP\n" }
+
+sub RecalculateClip
+  { return "% some calculations
+\\dimen0=${width}bp
+\\dimen2=${height}bp
+\\dimen4=${hoffset}bp
+\\dimen6=${pheight}bp
+\\advance\\dimen0 by  $Offset
+\\advance\\dimen0 by  $Offset
+\\advance\\dimen2 by  $Offset
+\\advance\\dimen2 by  $Offset
+\\advance\\dimen4 by  ${llx}bp
+\\advance\\dimen4 by -$Offset
+\\advance\\dimen6 by -${lly}bp
+\\advance\\dimen6 by  $Offset
+\\advance\\dimen6 by -\\dimen2
+\\advance\\dimen6 by  $TopCrop
+\\edef\\FigureWidth {\\the\\dimen0}
+\\edef\\FigureHeight{\\the\\dimen2}
+\\edef\\HOffset     {\\the\\dimen4}
+\\edef\\VOffset     {\\the\\dimen6}\n" }
+
+#D The previous scripts could be more sparse, but for the
+#D moment we prefer readability. Both scripts save some
+#D information in temporary file. We choose between them with
+#D the following sub routine.
+
+#D The first pass:
+
+sub PrepareConTeXt
+  { return "% interface=en
+\\setupoutput[pdftex]
+\\getfiguredimensions[$figurefile.pdf][page=$Page]\n" }
+
+sub PreparePlainTeX
+  { return "% plain tex alternative, needs recent supp-mis
+\\input supp-mis
+\\pdfoutput=1
+\\newdimen\\figurewidth
+\\newdimen\\figureheight
+\\setbox0=\\hbox
+   {\\immediate\\pdfximage page $Page {$figurefile.pdf}\\pdfrefximage\\pdflastximage}
+\\figurewidth=\\wd0
+\\figureheight=\\ht0\n" }
+
+sub PrepareFirstPass
+  { open (TEX, ">$tempfile.tex") ;
+    if ($UsePlain)
+      { print TEX
+          PreparePlainTeX  .
+          CalculateClip    .
+          SavePageData     .
+          MakePagePlainTeX }
+    else
+      { print TEX
+          PrepareConTeXt  .
+          CalculateClip   .
+          SavePageData    .
+          MakePageConTeXt }
+    close TEX }
+
+#D The second pass looks much like the first one, but this
+#D time we don't save information, use the natural
+#D boundingbox, and provide the offset.
+
+sub SetupConTeXt
+  { return "% interface=en
+\\setupoutput[pdftex]\n" }
+
+sub SetupPlainTeX
+  { return "% plain tex alternative
+\\pdfoutput=1
+\\setbox0=\\hbox
+  {\\immediate\\pdfximage page $Page {$figurefile.pdf}\\pdfrefximage\\pdflastximage}\n" }
+
+sub PrepareSecondPass
+  { open (TEX, ">$tempfile.tex") ;
+    if ($UsePlain)
+      { print TEX
+          SetupPlainTeX    .
+          RecalculateClip  .
+          MakePagePlainTeX }
+    else
+      { print TEX
+          SetupConTeXt    .
+          RecalculateClip .
+          MakePageConTeXt }
+    close TEX }
+
+#D The information we save in the first pass, is loaded here.
+
+sub FetchPaperSize
+  { open (TMP,"$figurefile.tmp") ;
+    while (<TMP>)
+      { chomp ;
+        if (/^(\d+) (\d+) (\d+) (\d+) *$/oi)
+          { $hoffset = $1 ;
+            $voffset = $2 ;
+            $pwidth  = $3 ;
+            $pheight = $4 ;
+            last } }
+    close (TMP) }
+
+#D Here we try to find the natural boundingbox. We need to
+#D pick up the page dimensions here.
+
+sub RunTeX
+  { if ($UsePlain)
+      { $result = `pdftex -prog=pdftex -fmt=plain -int=batchmode $tempfile` }
+    else
+      { $result = `texexec --batch --once --purge $tempfile` }
+    print $result if $Verbose ; $results .= "$result\n" }
+
+sub FindBoundingBox
+  { $result = `$gs -sDEVICE=bbox -dNOPAUSE -dBATCH $tempfile.pdf $pipe` ;
+    print $result if $Verbose ; $results .= "$result\n" }
+
+sub IdentifyCropBox
+  { RunTeX() ;
+    FetchPaperSize () ;
+    FindBoundingBox() }
+
+#D Just to be sure, we check if there is some image data, so
+#D that we can retry if something went wrong. Unfortunately we cannot
+#D safely check on a high res boundingbox.
+
+my $digits = '([\-\d\.]+)' ;
+
+sub ValidatedCropBox
+  { if ($result =~ /BoundingBox:\s*$digits\s+$digits\s+$digits\s+$digits\s*/mois)
+      { $llx = $1 ; $lly = $2 ; $urx = $3 ; $ury = $4 }
+    else
+      { print "Something is terribly wrong: no boundingbox:\n$result\n" ; exit }
+    $width  = abs($urx - $llx) ;
+    $height = abs($ury - $lly) ;
+    if ($width&&$height)
+      { return 1 }
+    else
+      { unless ($width)
+          { print "Something seems wrong: no width\n" ;
+            $LeftCrop = "0cm" ; $RightCrop  = "0cm" ; $Crop = "0cm" }
+        unless ($height)
+          { print "Something seems wrong: no height\n" ;
+            $TopCrop  = "0cm" ; $BottomCrop = "0cm" ; $Crop = "0cm" }
+        return 0 } }
+
+#D This is the main cropping routine.
+
+sub FixCropBox
+  { RunTeX() }
+
+#D For error tracing we save the log information in a file.
+
+sub RenameResult
+  { unlink "$resultfile.pdf" ;
+    rename "$tempfile.pdf", "$resultfile.pdf" }
+
+sub SaveLogInfo
+  { open (LOG, ">$resultfile.log") ;
+    print LOG $results ;
+    close (LOG) }
+
+#D We remove all temporary files.
+
+sub CleanUp
+  { unless ($Verbose)
+      { unlink "$tempfile.tex" ;
+        unlink "$tempfile.tuo" ;
+        unlink "$tempfile.tui" ;
+        unlink "$figurefile.tmp" } }
+
+#D Here it all comes together.
+
+GetItRight() ;
+
+PrepareFirstPass() ;
+
+IdentifyCropBox () ;
+
+unless (ValidatedCropBox())
+  { PrepareFirstPass() ;
+    IdentifyCropBox () }
+
+if (ValidatedCropBox())
+  { PrepareSecondPass() ;
+    FixCropBox() }
+
+RenameResult() ;
+SaveLogInfo() ;
+
+CleanUp () ;
diff --git a/scripts/context/perl/texfind.pl b/scripts/context/perl/texfind.pl
new file mode 100644
index 000000000..53a560c79
--- /dev/null
+++ b/scripts/context/perl/texfind.pl
@@ -0,0 +1,270 @@
+eval '(exit $?0)' && eval 'exec perl -S $0 ${1+"$@"}' && eval 'exec perl -S $0 $argv:q'
+        if 0;
+
+#D \module
+#D   [       file=texfind.pl,
+#D        version=1998.05.10,
+#D          title=\TEXFIND, 
+#D       subtitle=searching files, 
+#D         author=Hans Hagen,
+#D           date=\currentdate,
+#D      copyright={PRAGMA / Hans Hagen \& Ton Otten}]
+#C
+#C This module is part of the \CONTEXT\ macro||package and is
+#C therefore copyrighted by \PRAGMA. See licen-en.pdf for 
+#C details. 
+
+# test with "doif(un|)defined"
+
+use strict ; 
+use Getopt::Long ;
+use File::Find ;
+use Cwd ;
+use Tk ; 
+use Tk::widgets  ; 
+use Tk::ROText ;
+
+use FindBin ;
+use lib $FindBin::Bin ;
+use path_tre ; 
+
+my $FileSuffix    = 'tex' ; 
+my $SearchString  = '' ; 
+my $Recurse       = 0 ; 
+my $NumberOfHits  = 0 ;
+my $QuitSearch    = 0 ; 
+my $Location      = '' ;
+my $currentpath   = '.' ;
+
+my @FileList ; 
+
+my ($dw, $mw, $log, $sea, $fil, $num, $but, $dir, $loc) ; 
+
+$mw = MainWindow -> new () ; 
+$dw = MainWindow -> new () ; 
+
+$mw -> protocol( 'WM_DELETE_WINDOW' => sub { exit } ) ;
+$dw -> protocol( 'WM_DELETE_WINDOW' => sub { exit } ) ;
+
+$log = $mw -> Scrolled  ( 'ROText' ,
+                          -scrollbars => 'se'           ,
+                                -font => 'courier'      ,
+                                -wrap => 'none'         , 
+                               -width => 65             ,
+                              -height => 22             )
+           -> pack      (       -side => 'bottom'       ,
+                                -padx => 2              , 
+                                -pady => 2              ,
+                              -expand => 1              ,
+                                -fill => 'both'         ) ;
+
+$sea = $mw -> Entry  (  -textvariable => \$SearchString , 
+                                -font => 'courier'      ,
+                               -width => 20             )
+           -> pack   (          -side => 'left'         , 
+                                -padx => 2              , 
+                                -pady => 2              ) ;
+
+$fil = $mw -> Entry  (  -textvariable => \$FileSuffix   ,
+                                -font => 'courier'      ,
+                               -width => 5              ) 
+           -> pack   (          -side => 'left'         , 
+                                -padx => 2              , 
+                                -pady => 2              ) ;
+
+$but = $mw -> Checkbutton ( -variable => \$Recurse      ,
+                                -text => 'recurse'      )  
+           -> pack   (          -side => 'left'         ) ; 
+
+$num = $mw -> Entry  (  -textvariable => \$NumberOfHits , 
+                                -font => 'courier'      ,
+                             -justify => 'right'        ,
+                               -width => 5              )
+           -> pack   (          -side => 'right'        , 
+                                -padx => 2              , 
+                                -pady => 2              ) ;
+
+$loc = $mw -> Entry  (  -textvariable => \$Location     , 
+                                -font => 'courier'      ,
+                               -width => 8              )
+           -> pack   (          -side => 'right'        , 
+                                -padx => 2              , 
+                                -pady => 2              ) ;
+
+sub BuildDir 
+  {  if (Exists($dir)) { $dir -> destroy } ;
+     $dir = $dw -> Scrolled ( 'PathTree' ,
+                               -scrollbars => 'se'         ) 
+                -> pack     (      -expand => 1            , 
+                                     -fill => 'both'       ,
+                                     -padx => 2            , 
+                                     -pady => 2            ) ;
+     $dir -> configure      (        -font => 'courier'    ,
+                                   -height => 24           , 
+                                    -width => 65           , 
+                         -selectbackground => 'blue3'      ,
+                                -browsecmd => \&ChangePath ) ;
+     $dir -> bind ('<Return>'   , \&ShowFile  ) ; 
+     $dir -> bind ('<Double-1>' , \&ShowFile  ) }
+
+BuildDir ;
+
+sub ShowFile { $mw -> raise ; $sea -> focusForce } 
+sub ShowPath { $dw -> raise ; $dir -> focusForce } 
+
+$log -> tagConfigure ( 'found', -foreground => 'green3' ) ;
+$log -> tagConfigure ( 'title', -foreground => 'blue3' ) ;
+
+$sea -> bind ('<Return>'   , \&LocateStrings  ) ;
+$fil -> bind ('<Return>'   , \&LocateStrings  ) ;
+$loc -> bind ('<Return>'   , \&ChangeLocation ) ; 
+$log -> bind ('<Return>'   , \&ShowPath       ) ; 
+
+$sea -> bind ('<KeyPress>' , \&QuitSearch     ) ;
+$fil -> bind ('<KeyPress>' , \&QuitSearch     ) ;
+$loc -> bind ('<KeyPress>' , \&QuitSearch     ) ; 
+
+$sea -> bind ('<Escape>'   , \&QuitSearch     ) ;
+$fil -> bind ('<Escape>'   , \&QuitSearch     ) ;
+$loc -> bind ('<Escape>'   , \&QuitSearch     ) ;
+$log -> bind ('<Escape>'   , \&QuitSearch     ) ;
+
+$sea -> bind ('<Double-1>' , \&LocateStrings  ) ; 
+$fil -> bind ('<Double-1>' , \&LocateStrings  ) ; 
+$loc -> bind ('<Double-1>' , \&ChangeLocation ) ; 
+$log -> bind ('<Double-1>' , \&ShowPath       ) ; 
+
+sub ChangePath 
+  { my $currentpath = shift ; 
+chdir($currentpath) ; 
+    $QuitSearch = 1 ; 
+    $log -> delete ('1.0', 'end') ;
+    $log -> insert ('end', "$currentpath\n\n", 'title') }
+
+sub ChangeLocation
+  { $QuitSearch = 1 ;
+    $log -> delete ('1.0', 'end') ;
+    $Location =~ s/^\s*//o ;
+    $Location =~ s/\s*$//o ;    
+    $Location =~ s/(\\|\/\/)/\//go ;    
+    unless (-d $Location) 
+      { unless ($Location =~ /\//) { $Location .= '/' } }
+    if (-d $Location) 
+      { $log -> insert ('end', "changed to location '$Location'\n\n", 'title') ;
+        $currentpath = $Location ;
+        chdir ($currentpath) ;
+        $dir -> destroy ; 
+        BuildDir ;  
+        $dw -> raise ; 
+        $dw -> focusForce } 
+    else
+      { $log -> insert ('end', "unknown location '$Location'\n\n", 'title') ;
+        $Location = '' } }
+
+sub QuitSearch 
+  { $QuitSearch = 1 } 
+
+sub SearchFile 
+  { my ($FileName, $SearchString) = @_ ; 
+    my $Ok = 0 ; my $len ; 
+    open (TEX, $FileName) ; 
+    my $LineNumber = 0 ; 
+    while (<TEX>) 
+      { ++$LineNumber ; 
+        if ($QuitSearch) 
+          { if ($Ok) { $log -> see ('end') }
+            last } 
+        if (/$SearchString/i)
+          { ++$NumberOfHits ; $num -> update ; 
+            unless ($Ok) 
+              { $Ok = 1 ; 
+                $log -> insert ('end', "$FileName\n\n",'title') }
+            $log -> insert ('end', sprintf("%5i : ",$LineNumber), 'title') ;
+            s/^\s*//o ;
+#
+            $len = 0 ; 
+            while (/(.*?)($SearchString)/gi)
+              { $len += length($1) + length($2) ;  
+                $log -> insert ('end', "$1") ; 
+                $log -> insert ('end', "$2", 'found' ) }
+            $_ = substr($_,$len) ;  
+            $log -> insert ('end', "$_") ;
+#
+            $log -> update ;
+            $log -> see ('end') } } 
+    if ($Ok) { $log -> insert ('end', "\n") }
+    close (TEX) } 
+
+sub DoLocateFiles 
+  { @FileList = () ; 
+    $NumberOfHits = 0 ; 
+    if ($FileSuffix ne "") 
+      { $log -> delete ('1.0', 'end') ;
+        if ($Recurse)
+          { $log -> insert ('end', "recursively identifying files\n", 'title') ;
+            $log -> see ('end') ;
+            find (\&wanted, $currentpath) ;
+            sub wanted 
+              { if ($QuitSearch) { last ; return } 
+                if (/.*\.$FileSuffix/i) 
+                  { ++$NumberOfHits ; $num -> update ; 
+                    push @FileList, $File::Find::name } } } 
+        else  
+          { $log -> insert ('end', "identifying files\n", 'title') ;
+            $log -> see ('end') ;
+            opendir(DIR, $currentpath) ; my @TEMPLIST = readdir(DIR) ; closedir(DIR) ;
+            foreach my $FileName (@TEMPLIST) 
+              { if ($FileName =~ /.*\.$FileSuffix/i) 
+                  { ++$NumberOfHits ; $num -> update ; 
+                    if ($QuitSearch) 
+                      { last } 
+                    push @FileList, $FileName } } } 
+        @FileList = sort @FileList } }
+
+sub DoLocateStrings
+  { $log -> delete ('1.0', 'end') ; 
+    $log -> update ; 
+    $log -> see ('end') ; 
+    $NumberOfHits = 0 ; 
+    if ($SearchString ne "") 
+      { foreach my $FileName (@FileList) 
+          { if ($QuitSearch) 
+              { $log -> insert ('end', "search aborted\n", 'title') ;
+                $log -> see ('end') ; 
+                last } 
+            SearchFile($FileName,$SearchString) } } 
+    unless ($QuitSearch) 
+      { $log -> insert ('end', "done\n", 'title') ;
+        $log -> see ('end') } }
+
+sub LocateStrings
+  { $QuitSearch = 0 ; 
+    DoLocateFiles() ; 
+    DoLocateStrings() } 
+
+$log -> insert ('end', 
+
+  "data fields\n\n" , '' ,  
+
+
+  "string   :", 'title', " regular expression to search for\n"   , '' ,
+  "suffix   :", 'title', " type of file to search in\n"          , '' ,
+  "recurse  :", 'title', " enable searching subpaths\n"          , '' ,
+  "location :", 'title', " drive of root path\n"                 , '' ,
+  "counter  :", 'title', " file/hit counter\n\n"                 , '' ,
+
+  "key bindings\n\n" , '' ,  
+
+  "double 1 :", 'title', " directory window <-> search window\n" , '' ,
+  "enter    :", 'title', " start searching\n"                    , '' ,
+  "escape   :", 'title', " quit searching\n\n"                   , '' ,
+  
+  "current path\n\n" , '' , 
+ 
+  cwd(), 'title', "\n\n" , 'title' ) ; 
+
+$log -> update ; 
+
+ShowPath ; 
+
+MainLoop() ;
diff --git a/scripts/context/perl/texfont.pl b/scripts/context/perl/texfont.pl
new file mode 100644
index 000000000..5b3c1f1d7
--- /dev/null
+++ b/scripts/context/perl/texfont.pl
@@ -0,0 +1,1373 @@
+eval '(exit $?0)' && eval 'exec perl -S $0 ${1+"$@"}' && eval 'exec perl -S $0 $argv:q'
+        if 0;
+
+# This is an example of a crappy unstructured file but once
+# I know what should happen exactly, I will clean it up.
+
+# once it works all right, afmpl will be default
+
+# todo : ttf (partially doen already)
+
+# added: $pattern in order to avoid fuzzy shelle expansion of
+# filenames (not consistent over perl and shells); i hate that
+# kind of out of control features.
+
+#D \module
+#D   [       file=texfont.pl,
+#D        version=2004.02.06, % 2000.12.14
+#D          title=Font Handling,
+#D       subtitle=installing and generating,
+#D         author=Hans Hagen ++,
+#D           date=\currentdate,
+#D      copyright={PRAGMA / Hans Hagen \& Ton Otten}]
+#C
+#C This module is part of the \CONTEXT\ macro||package and is
+#C therefore copyrighted by \PRAGMA. See licen-en.pdf for
+#C details.
+
+#D For usage information, see \type {mfonts.pdf}.
+
+#D Todo : copy afm/pfb from main to local files to ensure metrics
+#D Todo : Wybo's help system
+#D Todo : list of encodings [texnansi, ec, textext]
+
+#D Thanks to George N. White III for solving a couple of bugs.
+#D Thanks to Adam T. Lindsay for adding Open Type support (and more).
+
+use strict ;
+
+my $savedoptions = join (" ",@ARGV) ;
+
+use Config ;
+use FindBin ;
+use File::Copy ;
+use Getopt::Long ;
+use Data::Dumper;
+
+$Getopt::Long::passthrough = 1 ; # no error message
+$Getopt::Long::autoabbrev  = 1 ; # partial switch accepted
+
+# Unless a user has specified an installation path, we take
+# the dedicated font path or the local path.
+
+## $dosish = ($Config{'osname'} =~ /dos|mswin/i) ;
+my $dosish = ($Config{'osname'} =~ /^(ms)?dos|^os\/2|^(ms|cyg)win/i) ;
+
+my $IsWin32 = ($^O =~ /MSWin32/i);
+my $SpacyPath = 0 ;
+
+# great, the win32api is not present in all perls
+
+BEGIN {
+    $IsWin32 = ($^O =~ /MSWin32/i) ;
+    $SpacyPath = 0 ;
+    if ($IsWin32) {
+        my $str = `kpsewhich -expand-path=\$TEXMF` ;
+        $SpacyPath = ($str =~ / /) ;
+        if ($SpacyPath) {
+            require Win32::API; import Win32::API;
+        }
+    }
+}
+
+# great, glob changed to bsd glob in an incompatible way ... sigh, we now
+# have to catch a failed glob returning the pattern
+#
+# to stupid either:
+#
+# sub validglob {
+#     my @globbed = glob(shift) ;
+#     if ((@globbed) &&  (! -e $globbed[0])) {
+#        return () ;
+#     } else {
+#        return @globbed ;
+#     }
+# }
+#
+# so now we have:
+
+sub validglob {
+    my @globbed = glob(shift) ;
+    my @globout = () ;
+    foreach my $file (@globbed) {
+        push (@globout,$file) if (-e $file) ;
+    }
+    return @globout ;
+}
+
+sub GetShortPathName {
+    my ($filename) = @_ ;
+    return $filename unless (($IsWin32)&&($SpacyPath)) ;
+    my $GetShortPathName = new Win32::API('kernel32', 'GetShortPathName', 'PPN', 'N') ;
+    if(not defined $GetShortPathName) {
+      die "Can't import API GetShortPathName: $!\n" ;
+    }
+    my $buffer = " " x 260;
+    my $len = $GetShortPathName->Call($filename, $buffer, 260) ;
+    return substr($buffer, 0, $len) ;
+}
+
+my $installpath = "" ;
+
+if (defined($ENV{TEXMFLOCAL})) {
+    $installpath = "TEXMFLOCAL" ;
+}
+
+if (defined($ENV{TEXMFFONTS})) {
+    $installpath = "TEXMFFONTS" ;
+}
+
+if ($installpath eq "") {
+    $installpath = "TEXMFLOCAL" ; # redundant
+}
+
+my $encoding        = "texnansi" ;
+my $vendor          = "" ;
+my $collection      = "" ;
+my $fontroot        = "" ; #/usr/people/gwhite/texmf-fonts" ;
+my $help            = 0 ;
+my $makepath        = 0 ;
+my $show            = 0 ;
+my $install         = 0 ;
+my $sourcepath      = "." ;
+my $passon          = "" ;
+my $extend          = "" ;
+my $narrow          = "" ;
+my $slant           = "" ;
+my $spaced          = "" ;
+my $caps            = "" ;
+my $noligs          = 0 ;
+my $nofligs         = 0 ;
+my $test            = 0 ;
+my $virtual         = 0 ;
+my $novirtual       = 0 ;
+my $listing         = 0 ;
+my $remove          = 0 ;
+my $expert          = 0 ;
+my $trace           = 0 ;
+my $afmpl           = 0 ;
+my $trees           = 'TEXMFFONTS,TEXMFLOCAL,TEXMFEXTRA,TEXMFMAIN,TEXMFDIST' ;
+my $pattern         = '' ;
+my $uselmencodings  = 0 ;
+
+my $fontsuffix  = "" ;
+my $namesuffix  = "" ;
+
+my $batch = "" ;
+
+my $weight = "" ;
+my $width = "" ;
+
+my $preproc         = 0 ;     # atl: formerly OpenType switch
+my $variant         = "" ;    # atl: encoding variant
+my $extension       = "pfb" ; # atl: default font extension
+my $lcdf            = "" ;    # atl: trigger for lcdf otftotfm
+
+my @cleanup         = () ;    # atl: build list of generated files to delete
+
+# todo: parse name for style, take face from command line
+#
+# @Faces  = ("Serif","Sans","Mono") ;
+# @Styles = ("Slanted","Spaced", "Italic","Bold","BoldSlanted","BoldItalic") ;
+#
+# for $fac (@Faces) { for $sty (@Styles) { $FacSty{"$fac$sty"} = "" } }
+
+&GetOptions
+  ( "help"         => \$help,
+    "makepath"     => \$makepath,
+    "noligs"       => \$noligs,
+    "nofligs"      => \$nofligs,
+    "show"         => \$show,
+    "install"      => \$install,
+    "encoding=s"   => \$encoding,
+    "variant=s"    => \$variant,  # atl: used as a suffix to $encfile only
+    "vendor=s"     => \$vendor,
+    "collection=s" => \$collection,
+    "fontroot=s"   => \$fontroot,
+    "sourcepath=s" => \$sourcepath,
+    "passon=s"     => \$passon,
+    "slant=s"      => \$slant,
+    "spaced=s"     => \$spaced,
+    "extend=s"     => \$extend,
+    "narrow=s"     => \$narrow,
+    "listing"      => \$listing,
+    "remove"       => \$remove,
+    "test"         => \$test,
+    "virtual"      => \$virtual,
+    "novirtual"    => \$novirtual,
+    "caps=s"       => \$caps,
+    "batch"        => \$batch,
+    "weight=s"     => \$weight,
+    "width=s"      => \$width,
+    "expert"       => \$expert,
+    "afmpl"        => \$afmpl,
+    "afm2pl"       => \$afmpl,
+    "lm"           => \$uselmencodings,
+    "rootlist=s"   => \$trees,
+    "pattern=s"    => \$pattern,
+    "trace"        => \$trace,    # --verbose conflicts with --ve
+    "preproc"      => \$preproc,  # atl: trigger conversion to pfb
+    "lcdf"         => \$lcdf ) ;  # atl: trigger use of lcdf fonttoools
+
+# for/from Fabrice:
+
+my $own_path = "$FindBin::Bin/" ;
+
+$FindBin::RealScript =~ m/([^\.]*)(\.pl|\.bat|\.exe|)/io ;
+
+my $own_name = $1 ;
+my $own_type = $2 ;
+my $own_stub = "" ;
+
+if ($own_type =~ /pl/oi) {
+    $own_stub = "perl "
+}
+
+if ($caps) { $afmpl = 0 } # for the moment
+
+# so we can use both combined
+
+if ($lcdf) {
+    $novirtual = 1 ;
+}
+
+if (!$novirtual) {
+    $virtual = 1 ;
+}
+
+# A couple of routines.
+
+sub report {
+    my $str = shift ;
+    $str =~ s/  / /goi ;
+    if ($str =~ /(.*?)\s+([\:\/])\s+(.*)/o) {
+        if ($1 eq "") {
+            $str = " " ;
+        } else {
+            $str = $2 ;
+        }
+        print sprintf("%22s $str %s\n",$1,$3) ;
+    }
+}
+
+sub error {
+    report("processing aborted : " . shift) ;
+    print "\n" ;
+    report "--help : show some more info" ;
+    exit ;
+}
+
+# The banner.
+
+print "\n" ;
+report ("TeXFont 2.2.1 - ConTeXt / PRAGMA ADE 2000-2004") ;
+print "\n" ;
+
+# Handy for scripts: one can provide a preferred path, if it
+# does not exist, the current path is taken.
+
+if (!(-d $sourcepath)&&($sourcepath ne 'auto')) { $sourcepath = "." }
+
+# Let's make multiple masters if requested.
+
+sub create_mm_font
+  { my ($name,$weight,$width) = @_ ; my $flag = my $args = my $tags = "" ;
+    my $ok ;
+    if ($name ne "")
+      { report ("mm source file : $name") }
+    else
+      { error ("missing mm source file") }
+    if ($weight ne "")
+      { report ("weight : $weight") ;
+        $flag .= " --weight=$weight " ;
+        $tags .= "-weight-$weight" }
+    if ($width ne "")
+      { report ("width : $width") ;
+        $flag .= " --width=$width " ;
+        $tags .= "-width-$width" }
+    error ("no specification given") if ($tags eq "") ;
+    error ("no amfm file found") unless (-f "$sourcepath/$name.amfm") ;
+    error ("no pfb file found") unless (-f "$sourcepath/$name.pfb") ;
+    $args = "$flag --precision=5 --kern-precision=0 --output=$sourcepath/$name$tags.afm" ;
+    my $command = "mmafm $args $sourcepath/$name.amfm" ;
+    print "$command\n" if $trace ;
+    $ok = `$command` ; chomp $ok ;
+    if ($ok ne "") { report ("warning $ok") }
+    $args = "$flag --precision=5 --output=$sourcepath/$name$tags.pfb" ;
+    $command = "mmpfb $args $sourcepath/$name.pfb" ;
+    print "$command\n" if $trace ;
+    $ok = `$command` ; chomp $ok ;
+    if ($ok ne "") { report ("warning $ok") }
+    report ("mm result file : $name$tags") }
+
+if (($weight ne "")||($width ne ""))
+  { create_mm_font($ARGV[0],$weight,$width) ;
+    exit }
+
+# go on
+
+if (($listing||$remove)&&($sourcepath eq "."))
+  { $sourcepath = "auto" }
+
+if ($fontroot eq "")
+  { if ($dosish)
+      { $fontroot = `kpsewhich -expand-path=\$$installpath` }
+    else
+      { $fontroot = `kpsewhich -expand-path=\\\$$installpath` }
+    chomp $fontroot }
+
+
+if ($fontroot =~ /\s+/)  # needed for windows, spaces in name
+  { $fontroot = &GetShortPathName($fontroot) } # but ugly when not needed
+
+if ($test)
+  { $vendor = $collection = "test" ;
+    $install = 1 }
+
+if (($spaced ne "") && ($spaced !~ /\d/)) { $spaced = "50" }
+if (($slant  ne "") && ($slant  !~ /\d/)) { $slant  = "0.167" }
+if (($extend ne "") && ($extend !~ /\d/)) { $extend = "1.200" }
+if (($narrow ne "") && ($narrow !~ /\d/)) { $narrow = "0.800" }
+if (($caps   ne "") && ($caps   !~ /\d/)) { $caps   = "0.800" }
+
+$encoding   = lc $encoding ;
+$vendor     = lc $vendor ;
+$collection = lc $collection ;
+
+if ($encoding =~ /default/oi) { $encoding = "texnansi" }
+
+my $lcfontroot = lc $fontroot ;
+
+# Auto search paths
+
+my @trees = split(/\,/,$trees) ;
+
+# Test for help asked.
+
+if ($help)
+  { report "--fontroot=path     : texmf destination font root (default: $lcfontroot)" ;
+    report "--rootlist=paths    : texmf source roots (default: $trees)" ;
+    report "--sourcepath=path   : when installing, copy from this path (default: $sourcepath)" ;
+    report "--sourcepath=auto   : locate and use vendor/collection" ;
+    print  "\n" ;
+    report "--vendor=name       : vendor name/directory" ;
+    report "--collection=name   : font collection" ;
+    report "--encoding=name     : encoding vector (default: $encoding)" ;
+    report "--variant=name      : encoding variant (.enc file or otftotfm features)" ;
+    print  "\n" ;
+    report "--spaced=s          : space glyphs in font by promille of em (0 - 1000)" ;
+    report "--slant=s           : slant glyphs in font by factor (0.0 - 1.5)" ;
+    report "--extend=s          : extend glyphs in font by factor (0.0 - 1.5)" ;
+    report "--caps=s            : capitalize lowercase chars by factor (0.5 - 1.0)" ;
+    report "--noligs --nofligs  : remove ligatures" ;
+    print  "\n" ;
+    report "--install           : copy files from source to font tree" ;
+    report "--listing           : list files on auto sourcepath" ;
+    report "--remove            : remove files on auto sourcepath" ;
+    report "--makepath          : when needed, create the paths" ;
+    print  "\n" ;
+    report "--test              : use test paths for vendor/collection" ;
+    report "--show              : run tex on texfont.tex" ;
+    print  "\n" ;
+    report "--batch             : process given batch file" ;
+    print  "\n" ;
+    report "--weight            : multiple master weight" ;
+    report "--width             : multiple master width" ;
+    print  "\n" ;
+    report "--expert            : also handle expert fonts" ;
+    print  "\n" ;
+    report "--afmpl             : use afm2pl instead of afm2tfm" ;
+    report "--preproc           : pre-process ttf/otf, converting them to pfb" ;
+    report "--lcdf              : use lcdf fonttools to create virtual encoding" ;
+    exit }
+
+if (($batch)||(($ARGV[0]) && ($ARGV[0] =~ /.+\.dat$/io)))
+  { my $batchfile = $ARGV[0] ;
+    unless (-f $batchfile)
+      { if ($batchfile !~ /\.dat$/io) { $batchfile .= ".dat" } }
+    unless (-f $batchfile)
+      { report ("trying to locate : $batchfile") ;
+        $batchfile = `kpsewhich -format="other text files" -progname=context $batchfile` ;
+        chomp $batchfile }
+    error ("unknown batch file $batchfile") unless -e $batchfile ;
+    report ("processing batch file : $batchfile") ;
+    my $select = (($vendor ne "")||($collection ne "")) ;
+    my $selecting = 0 ;
+    if (open(BAT, $batchfile))
+      { while (<BAT>)
+          { chomp ;
+            s/(.+)\#.*/$1/o ;
+            next if (/^\s*$/io) ;
+            if ($select)
+              { if ($selecting)
+                  { if (/^\s*[\#\%]/io) { if (!/\-\-/o) { last } else { next } } }
+                elsif ((/^\s*[\#\%]/io)&&(/$vendor/i)&&(/$collection/i))
+                  { $selecting = 1 ; next }
+                else
+                  { next } }
+            else
+              { next if (/^\s*[\#\%]/io) ;
+                next unless (/\-\-/oi) }
+                s/\s+/ /gio ;
+                s/(--en.*\=)\?/$1$encoding/io ;
+                report ("batch line : $_") ;
+              # system ("perl $0 --fontroot=$fontroot $_") }
+	        my $own_quote = ( $own_path =~ m/^[^\"].* / ? "\"" : "" );
+            my $switches = '' ;
+            $switches .= "--afmpl " if $afmpl ;
+            system ("$own_stub$own_quote$own_path$own_name$own_type$own_quote $switches --fontroot=$fontroot $_") }
+            close (BAT) }
+    exit }
+
+error ("unknown vendor     $vendor")     unless    $vendor ;
+error ("unknown collection $collection") unless    $collection ;
+error ("unknown tex root   $lcfontroot") unless -d $fontroot ;
+
+my $varlabel = $variant ;
+
+if ($lcdf)
+  { $varlabel =~ s/,/-/goi ;
+    $varlabel =~ tr/a-z/A-Z/ }
+
+if ($varlabel ne "")
+  { $varlabel = "-$varlabel" }
+
+my $identifier = "$encoding$varlabel-$vendor-$collection" ;
+
+my $outlinepath = $sourcepath ; my $path = "" ;
+
+my $shape = "" ;
+
+if ($noligs||$nofligs)
+  { report ("ligatures : removed") ;
+    $fontsuffix .= "-unligatured" ;
+    $namesuffix .= "-NoLigs" }
+
+if ($caps ne "")
+  {    if ($caps <0.5) { $caps = 0.5 }
+    elsif ($caps >1.0) { $caps = 1.0 }
+    $shape .= " -c $caps " ;
+    report ("caps factor : $caps") ;
+    $fontsuffix .= "-capitalized-" . int(1000*$caps)  ;
+    $namesuffix .= "-Caps" }
+
+if ($extend ne "")
+  { if    ($extend<0.0) { $extend = 0.0 }
+    elsif ($extend>1.5) { $extend = 1.5 }
+    report ("extend factor : $extend") ;
+    if ($lcdf)
+      { $shape .= " -E $extend " }
+    else
+      { $shape .= " -e $extend " }
+    $fontsuffix .= "-extended-" . int(1000*$extend) ;
+    $namesuffix .= "-Extended" }
+
+if ($narrow ne "") # goodie
+  { $extend = $narrow ;
+    if    ($extend<0.0) { $extend = 0.0 }
+    elsif ($extend>1.5) { $extend = 1.5 }
+    report ("narrow factor : $extend") ;
+    if ($lcdf)
+      { $shape .= " -E $extend " }
+    else
+      { $shape .= " -e $extend " }
+    $fontsuffix .= "-narrowed-" . int(1000*$extend) ;
+    $namesuffix .= "-Narrowed" }
+
+if ($slant ne "")
+  {    if ($slant <0.0) { $slant = 0.0 }
+    elsif ($slant >1.5) { $slant = 1.5 }
+    report ("slant factor : $slant") ;
+    if ($lcdf)
+      { $shape .= " -S $slant " }
+    else
+      { $shape .= " -s $slant " }
+    $fontsuffix .= "-slanted-" . int(1000*$slant) ;
+    $namesuffix .= "-Slanted" }
+
+if ($spaced ne "")
+  {    if ($spaced <   0) { $spaced =    0 }
+    elsif ($spaced >1000) { $spaced = 1000 }
+    report ("space factor : $spaced") ;
+    if ($lcdf)
+      { $shape .= " -L $spaced " }
+    else
+      { $shape .= " -m $spaced " }
+    $fontsuffix .= "-spaced-" . $spaced ;
+    $namesuffix .= "-Spaced" }
+
+if ($sourcepath eq "auto") # todo uppercase root
+  { foreach my $root (@trees)
+      { if ($dosish)
+          { $path = `kpsewhich -expand-path=\$$root` }
+        else
+          { $path = `kpsewhich -expand-path=\\\$$root` }
+        chomp $path ;
+        $path = $ENV{$root} if (($path eq '') && defined($ENV{$root})) ;
+        report ("checking root : $root") ;
+        if ($preproc)
+          { $sourcepath = "$path/fonts/truetype/$vendor/$collection" }
+        else
+          { $sourcepath = "$path/fonts/afm/$vendor/$collection" }
+        unless (-d $sourcepath)
+          { my $ven = $vendor ; $ven =~ s/(........).*/$1/ ;
+            my $col = $collection ; $col =~ s/(........).*/$1/ ;
+            $sourcepath = "$path/fonts/afm/$ven/$col" ;
+            if (-d $sourcepath)
+              { $vendor = $ven ; $collection = $col } }
+        $outlinepath = "$path/fonts/type1/$vendor/$collection" ;
+        if (-d $sourcepath)
+          { # $install = 0 ;  # no copy needed
+            $makepath = 1 ; # make on local if needed
+	    my @files = validglob("$sourcepath/*.afm") ;
+	    if ($preproc)
+	      { @files = validglob("$sourcepath/*.otf") ;
+	        report("locating : otf files") }
+	    unless (@files)
+          { @files = validglob("$sourcepath/*.ttf") ;
+	        report("locating : ttf files") }
+        if (@files)
+          { if ($listing)
+              { report ("fontpath : $sourcepath" ) ;
+                print "\n" ;
+                foreach my $file (@files)
+                  { if (open(AFM,$file))
+                      { my $name = "unknown name" ;
+                        while (<AFM>)
+                          { chomp ;
+                            if (/^fontname\s+(.*?)$/oi)
+                              { $name = $1 ; last } }
+                        close (AFM) ;
+                        if ($preproc)
+                          { $file =~ s/.*\/(.*)\..tf/$1/io }
+                        else
+                          { $file =~ s/.*\/(.*)\.afm/$1/io }
+                        report ("$file : $name") } }
+                exit }
+            elsif ($remove)
+              { error ("no removal from : $root") if ($root eq 'TEXMFMAIN') ;
+                foreach my $file (@files)
+                  { if ($preproc)
+                      { $file =~ s/.*\/(.*)\..tf/$1/io }
+                    else
+                      { $file =~ s/.*\/(.*)\.afm/$1/io }
+                    foreach my $sub ("tfm","vf")
+                      { foreach my $typ ("","-raw")
+                          { my $nam = "$path/fonts/$sub/$vendor/$collection/$encoding$varlabel$typ-$file.$sub" ;
+                            if (-s $nam)
+                              { report ("removing : $encoding$varlabel$typ-$file.$sub") ;
+                                unlink $nam } } } }
+                my $nam = "$encoding$varlabel-$vendor-$collection.tex" ;
+                if (-e $nam)
+                  { report ("removing : $nam") ;
+                    unlink "$nam" }
+                my $mapfile = "$encoding$varlabel-$vendor-$collection" ;
+                foreach my $map ("pdftex","dvips", "dvipdfm")
+                  { my $maproot = "$fontroot/fonts/map/$map/context/";
+                    if (-e "$maproot$mapfile.map")
+                       { report ("renaming : $mapfile.map -> $mapfile.bak") ;
+                         unlink "$maproot$mapfile.bak" ;
+                         rename "$maproot$mapfile.map", "$maproot$mapfile.bak" } }
+                exit }
+            else
+              { last } } } }
+    error ("unknown subpath ../fonts/afm/$vendor/$collection") unless -d $sourcepath }
+
+error ("unknown source path $sourcepath") unless -d $sourcepath ;
+error ("unknown option $ARGV[0]")         if (($ARGV[0]||'') =~ /\-\-/) ;
+
+my $afmpath = "$fontroot/fonts/afm/$vendor/$collection" ;
+my $tfmpath = "$fontroot/fonts/tfm/$vendor/$collection" ;
+my $vfpath  = "$fontroot/fonts/vf/$vendor/$collection" ;
+my $pfbpath = "$fontroot/fonts/type1/$vendor/$collection" ;
+my $ttfpath = "$fontroot/fonts/truetype/$vendor/$collection" ;
+my $otfpath = "$fontroot/fonts/opentype/$vendor/$collection" ;
+my $encpath = "$fontroot/fonts/enc/dvips/context" ;
+
+sub mappath
+  { my $str = shift ;
+    return "$fontroot/fonts/map/$str/context" }
+
+# are not on local path ! ! ! !
+
+foreach my $path ($afmpath, $pfbpath)
+  { my @gzipped = <$path/*.gz> ;
+    foreach my $file (@gzipped)
+      { print "file = $file\n";
+	system ("gzip -d $file") } }
+
+# For gerben, we only generate a new database when an lsr file is present but for
+# myself we force this when texmf-fonts is used (else I get compatibility problems).
+
+if (($fontroot =~ /texmf\-fonts/o) || (-e "$fontroot/ls-R") || (-e "$fontroot/ls-r") || (-e "$fontroot/LS-R")) {
+    system ("mktexlsr $fontroot") ;
+}
+
+sub do_make_path
+  { my $str = shift ;
+    if ($str =~ /^(.*)\/.*?$/)
+      { do_make_path($1); }
+    mkdir $str, 0755 unless -d $str }
+
+sub make_path
+  { my $str = shift ;
+    do_make_path("$fontroot/fonts/$str/$vendor/$collection") }
+
+if ($makepath&&$install)
+  { make_path ("afm") ; make_path ("type1") }
+
+do_make_path(mappath("pdftex")) ;
+do_make_path(mappath("dvips")) ;
+do_make_path(mappath("dvipdfm")) ;
+do_make_path($encpath) ;
+
+# now fonts/map and fonts/enc
+
+make_path ("vf") ;
+make_path ("tfm") ;
+
+if ($install)
+  { error ("unknown afm path $afmpath") unless -d $afmpath ;
+    error ("unknown pfb path $pfbpath") unless -d $pfbpath }
+
+error ("unknown tfm path $tfmpath") unless -d $tfmpath ;
+error ("unknown vf  path $vfpath" ) unless -d $vfpath  ;
+error ("unknown map path " . mappath("pdftex"))  unless -d mappath("pdftex");
+error ("unknown map path " . mappath("dvips"))   unless -d mappath("dvips");
+error ("unknown map path " . mappath("dvipdfm")) unless -d mappath("dvipdfm");
+
+my $mapfile = "$identifier.map" ;
+my $bakfile = "$identifier.bak" ;
+my $texfile = "$identifier.tex" ;
+
+                report "encoding vector : $encoding" ;
+if ($variant) { report "encoding variant : $variant" }
+                report      "vendor name : $vendor" ;
+                report  "    source path : $sourcepath" ;
+                report  "font collection : $collection" ;
+                report  "texmf font root : $lcfontroot" ;
+                report  "  map file name : $mapfile" ;
+
+if ($install) { report "source path : $sourcepath" }
+
+my $fntlist = "" ;
+
+my $runpath = $sourcepath ;
+
+my @files ;
+
+sub UnLink
+  { foreach my $f (@_)
+     { if (unlink $f)
+          { report "deleted : $f" if $trace } } }
+
+sub globafmfiles
+  { my ($runpath, $pattern)  = @_ ;
+    my @files = validglob("$runpath/$pattern.afm") ;
+    report("locating afm files : using pattern $runpath/$pattern.afm");
+    if ($preproc && !$lcdf)
+      { @files = validglob("$runpath/$pattern.*tf") ;
+        report("locating otf files : using pattern $runpath/$pattern.*tf");
+        unless (@files)
+          { @files = validglob("$sourcepath/$pattern.ttf") ;
+	        report("locating ttf files : using pattern $sourcepath/$pattern.ttf") }
+          }
+    if (@files) # also elsewhere
+      { report("locating afm files : using pattern $pattern") }
+    else
+      { @files = validglob("$runpath/$pattern.ttf") ;
+        if (@files)
+          { report("locating afm files : using ttf files") ;
+            $extension = "ttf" ;
+            foreach my $file (@files)
+                   { $file =~ s/\.ttf$//io ;
+                     report ("generating afm file : $file.afm") ;
+                     my $command = "ttf2afm \"$file.ttf\" -o \"$file.afm\"" ;
+                     system($command) ;
+                     print "$command\n" if $trace ;
+                     push(@cleanup, "$file.afm") }
+            @files = validglob("$runpath/$pattern.afm") }
+        else # try doing the pre-processing earlier
+          { report("locating afm files : using otf files") ;
+            $extension = "otf" ;
+            @files = validglob("$runpath/$pattern.otf") ;
+            foreach my $file (@files)
+              { $file =~ s/\.otf$//io ;
+            if (!$lcdf)
+            { report ("generating afm file : $file.afm") ;
+              preprocess_font("$file.otf", "$file.bdf") ;
+              push(@cleanup,"$file.afm") }
+            if ($preproc)
+            { my $command = "cfftot1 --output=$file.pfb $file.otf" ;
+                      print "$command\n" if $trace ;
+              report("converting : $file.otf to $file.pfb") ;
+                      system($command) ;
+                      push(@cleanup, "$file.pfb") ;
+                }
+              }
+                if ($lcdf)
+            { @files = validglob("$runpath/$pattern.otf") }
+            else
+            { @files = validglob("$runpath/$pattern.afm") }
+          }
+       }
+    return @files }
+
+if ($pattern eq '') { if ($ARGV[0]) { $pattern = $ARGV[0] } }
+
+if ($pattern ne '')
+  { report ("processing files : all in pattern $pattern") ;
+    @files = globafmfiles($runpath,$pattern) }
+elsif ("$extend$narrow$slant$spaced$caps" ne "")
+  { error ("transformation needs file spec") }
+else
+  { $pattern = "*" ;
+    report ("processing files : all on afm path") ;
+    @files = globafmfiles($runpath,$pattern) }
+
+sub copy_files
+  { my ($suffix,$sourcepath,$topath) = @_ ;
+    my @files = validglob("$sourcepath/$pattern.$suffix") ;
+    return if ($topath eq $sourcepath) ;
+    report ("copying files : $suffix") ;
+    foreach my $file (@files)
+      { my $ok = $file =~ /(.*)\/(.+?)\.(.*)/ ;
+        my ($path,$name,$suffix) = ($1,$2,$3) ;
+        UnLink "$topath/$name.$suffix" ;
+        report ("copying : $name.$suffix") ;
+        copy ($file,"$topath/$name.$suffix") } }
+
+if ($install)
+  { copy_files("afm",$sourcepath,$afmpath) ;
+#   copy_files("tfm",$sourcepath,$tfmpath) ; # raw supplied names
+    copy_files("pfb",$outlinepath,$pfbpath) ;
+    if ($extension eq "ttf")
+      { make_path("truetype") ;
+        copy_files("ttf",$sourcepath,$ttfpath) }
+    if ($extension eq "otf")
+      { make_path("truetype") ;
+	    copy_files("otf",$sourcepath,$ttfpath) } }
+
+error ("no afm files found") unless @files ;
+
+sub open_mapfile
+  { my $type = shift;
+	my $mappath = mappath($type);
+    my $mapdata = "";
+	my $mapptr = undef;
+	my $fullmapfile = $mapfile;
+	$fullmapfile = "$type-$fullmapfile" unless $type eq "pdftex";
+	if ($install)
+	  { copy ("$mappath/$mapfile","$mappath/$bakfile") ; }
+    if (open ($mapptr,"<$mappath/$mapfile"))
+      { report ("extending map file : $mappath/$mapfile") ;
+        while (<$mapptr>) { unless (/^\%/o) { $mapdata .= $_ } }
+        close ($mapptr) }
+    else
+      { report ("no map file at : $mappath/$mapfile") }
+    #~ unless (open ($mapptr,">$fullmapfile") )
+do_make_path($mappath) ;
+    unless (open ($mapptr,">$mappath/$fullmapfile") )
+      { report "warning : can't open $fullmapfile" }
+    else
+      { if ($type eq "pdftex")
+          { print $mapptr "% This file is generated by the TeXFont Perl script.\n";
+            print $mapptr "%\n" ;
+            print $mapptr "% You need to add the following line to your file:\n" ;
+            print $mapptr "%\n" ;
+            print $mapptr "%   \\pdfmapfile{+$mapfile}\n" ;
+            print $mapptr "%\n" ;
+            print $mapptr "% In ConTeXt you can best use:\n" ;
+            print $mapptr "%\n" ;
+            print $mapptr "%   \\loadmapfile\[$mapfile\]\n\n" } }
+    return ($mapptr,$mapdata) ; }
+
+sub finish_mapfile
+  { my ($type, $mapptr, $mapdata ) = @_;
+	my $fullmapfile = $mapfile;
+	$fullmapfile = "$type-$fullmapfile" unless $type eq "pdftex";
+    if (defined $mapptr)
+      { report ("updating map file : $mapfile (for $type)") ;
+        while ($mapdata =~ s/\n\n+/\n/mois) {} ;
+        $mapdata =~ s/^\s*//gmois ;
+        print $mapptr $mapdata ;
+        close ($mapptr) ;
+        if ($install)
+          { copy ("$fullmapfile", mappath($type) . "/$mapfile") ; } } }
+
+
+my ($PDFTEXMAP,$pdftexmapdata)   = open_mapfile("pdftex");
+my ($DVIPSMAP,$dvipsmapdata)     = open_mapfile("dvips");
+my ($DVIPDFMMAP,$dvipdfmmapdata) = open_mapfile("dvipdfm");
+
+my $tex = 0 ;
+my $texdata = "" ;
+
+if (open (TEX,"<$texfile"))
+  { while (<TEX>) { unless (/stoptext/o) { $texdata .= $_ } }
+    close (TEX) }
+
+$tex = open (TEX,">$texfile") ;
+
+unless ($tex) { report "warning : can't open $texfile" }
+
+if ($tex)
+  { if ($texdata eq "")
+      { print TEX "% interface=en\n" ;
+        print TEX "\n" ;
+        print TEX "\\usemodule[fnt-01]\n" ;
+        print TEX "\n" ;
+        print TEX "\\loadmapfile[$mapfile]\n" ;
+        print TEX "\n" ;
+        print TEX "\\starttext\n\n" }
+    else
+      { print TEX "$texdata" ;
+        print TEX "\n\%appended section\n\n\\page\n\n" } }
+
+sub removeligatures
+  { my $filename = shift ; my $skip = 0 ;
+    copy ("$filename.vpl","$filename.tmp") ;
+    if ((open(TMP,"<$filename.tmp"))&&(open(VPL,">$filename.vpl")))
+      { report "removing ligatures : $filename" ;
+        while (<TMP>)
+         { chomp ;
+           if ($skip)
+             { if (/^\s*\)\s*$/o) { $skip = 0 ; print VPL "$_\n" } }
+           elsif (/\(LIGTABLE/o)
+             { $skip = 1 ; print VPL "$_\n" }
+           else
+             { print VPL "$_\n" } }
+        close(TMP) ; close(VPL) }
+    UnLink ("$filename.tmp") }
+
+my $raw = my $use = my $maplist = my $texlist = my $report = "" ;
+
+$use = "$encoding$varlabel-" ; $raw = $use . "raw-" ;
+
+my $encfil = "" ;
+
+if ($encoding ne "") # evt -progname=context
+  { $encfil = `kpsewhich -progname=pdftex $encoding$varlabel.enc` ;
+    chomp $encfil ; if ($encfil eq "") { $encfil = "$encoding$varlabel.enc" } }
+
+sub build_pdftex_mapline
+  { my ($option, $usename, $fontname, $rawname, $cleanfont, $encoding, $varlabel, $strange)  = @_;
+    my $cleanname = $fontname;
+	$cleanname =~ s/\_//gio ;
+    $option =~ s/^\s+(.*)/$1/o ;
+    $option =~ s/(.*)\s+$/$1/o ;
+    $option =~ s/  / /g ;
+    if ($option ne "")
+      { $option = "\"$option\" 4" }
+    else
+      { $option = "4" }
+    # adding cleanfont is kind of dangerous
+    my $thename = "";
+	my $str = "";
+    my $theencoding = "" ;
+    if ($strange ne "")
+      { $thename = $cleanname ; $theencoding = "" ; }
+    elsif ($lcdf)
+      { $thename = $usename ; $theencoding = " $encoding$varlabel-$cleanname.enc" }
+    elsif ($afmpl)
+      { $thename = $usename ; $theencoding = " $encoding$varlabel.enc" }
+    elsif ($virtual)
+      { $thename = $rawname ; $theencoding = " $encoding$varlabel.enc" }
+    else
+      { $thename = $usename ; $theencoding = " $encoding$varlabel.enc" }
+if ($uselmencodings) {
+    $theencoding =~ s/^(ec)\.enc/lm\-$1.enc/ ;
+}
+    # quit rest if no type 1 file
+    my $pfb_sourcepath = $sourcepath ;
+    $pfb_sourcepath =~ s@/afm/@/type1/@ ;
+    unless ((-e "$pfbpath/$fontname.$extension")||
+			(-e "$pfb_sourcepath/$fontname.$extension")||
+			(-e "$sourcepath/$fontname.$extension")||
+			(-e "$ttfpath/$fontname.$extension"))
+	  { if ($tex) { $report .= "missing file: \\type \{$fontname.pfb\}\n" }
+		report ("missing pfb file : $fontname.pfb") }
+    # now add entry to map
+    if ($strange eq "") {
+	  if ($extension eq "otf") {
+		if ($lcdf) {
+		  my $mapline = "" ;
+		  if (open(ALTMAP,"texfont.map")) {
+			while (<ALTMAP>) {
+			  chomp ;
+			  # atl: we assume this b/c we always force otftotfm --no-type1
+			  if (/<<(.*)\.otf$/oi) {
+				$mapline = $_ ; last ;
+			  }
+			}
+			close(ALTMAP) ;
+		  } else {
+			report("no mapfile from otftotfm : texfont.map") ;
+		  }
+		  if ($preproc) {
+			$mapline =~ s/<\[/</;
+			$mapline =~ s/<<(\S+)\.otf$/<$1\.pfb/ ;
+		  } else {
+			$mapline =~ s/<<(\S+)\.otf$/<< $ttfpath\/$fontname.$extension/ ;
+		  }
+		  $str = "$mapline\n" ;
+		} else {
+		  if ($preproc) {
+			$str = "$thename $cleanfont $option < $fontname.pfb$theencoding\n" ;
+		  } else {
+			# PdfTeX can't subset OTF files, so we have to include the whole thing
+			# It looks like we also need to be explicit on where to find the file
+			$str = "$thename $cleanfont $option << $ttfpath/$fontname.$extension <$theencoding\n" ;
+		  }
+		}
+	  } else {
+		$str = "$thename $cleanfont $option < $fontname.$extension$theencoding\n" ;
+	  }
+    } else {
+	  $str = "$thename $cleanfont < $fontname.$extension\n" ;
+    }
+    return ($str, $thename); }
+
+sub build_dvips_mapline
+  { my ($option, $usename, $fontname, $rawname, $cleanfont, $encoding, $varlabel, $strange)  = @_;
+    my $cleanname = $fontname;
+	$cleanname =~ s/\_//gio ;
+    $option =~ s/^\s+(.*)/$1/o ;
+    $option =~ s/(.*)\s+$/$1/o ;
+    $option =~ s/  / /g ;
+    # adding cleanfont is kind of dangerous
+    my $thename = "";
+	my $str = "";
+    my $optionencoding = "" ;
+	my $encname = "";
+    my $theencoding = "" ;
+	if ($encoding ne "") # evt -progname=context
+	  { $encfil = `kpsewhich -progname=dvips $encoding$varlabel.enc` ;
+		chomp $encfil ;
+		if ($encfil eq "")
+         { $encfil = "$encoding$varlabel.enc" ; }
+		if (open(ENC,"<$encfil"))
+		  { while (<ENC>)
+			{ if (/^\/([^ ]+)\s*\[/)
+              { $encname = $1;
+                last; } }
+			close ENC; } }
+    if ($strange ne "")
+      { $thename = $cleanname ;
+		$optionencoding = "\"$option\""  if length($option)>1; }
+    elsif ($lcdf)
+      { $thename = $usename ;
+		$optionencoding = "\"$option $encname ReEncodeFont\" <$encoding$varlabel-$cleanname.enc" }
+    elsif ($afmpl)
+      { $thename = $usename ;
+		$optionencoding = "\"$option $encname ReEncodeFont\" <$encoding$varlabel.enc" }
+    elsif ($virtual)
+      { $thename = $rawname ;
+		$optionencoding = "\"$option $encname ReEncodeFont\" <$encoding$varlabel.enc" }
+    else
+      { $thename = $usename ;
+		$optionencoding = "\"$option $encname ReEncodeFont\" <$encoding$varlabel.enc" }
+if ($uselmencodings) {
+    $theencoding =~ s/^(ec)\.enc/lm\-$1.enc/ ;
+}
+    # quit rest if no type 1 file
+    my $pfb_sourcepath = $sourcepath ;
+    $pfb_sourcepath =~ s@/afm/@/type1/@ ;
+    unless ((-e "$pfbpath/$fontname.$extension")||
+			(-e "$pfb_sourcepath/$fontname.$extension")||
+			(-e "$sourcepath/$fontname.$extension")||
+			(-e "$ttfpath/$fontname.$extension"))
+	  { if ($tex) { $report .= "missing file: \\type \{$fontname.pfb\}\n" }
+       report ("missing pfb file : $fontname.pfb") }
+    # now add entry to map
+    if ($strange eq "") {
+	  if ($extension eq "otf") {
+		if ($lcdf) {
+		  my $mapline = "" ;
+		  if (open(ALTMAP,"texfont.map")) {
+			while (<ALTMAP>) {
+			  chomp ;
+			  # atl: we assume this b/c we always force otftotfm --no-type1
+			  if (/<<(.*)\.otf$/oi) {
+				$mapline = $_ ; last ;
+			  }
+			}
+			close(ALTMAP) ;
+		  } else {
+			report("no mapfile from otftotfm : texfont.map") ;
+		  }
+		  if ($preproc) {
+			$mapline =~ s/<\[/</;
+			$mapline =~ s/<<(\S+)\.otf$/<$1\.pfb/ ;
+		  } else {
+			$mapline =~ s/<<(\S+)\.otf$/<< $ttfpath\/$fontname.$extension/ ;
+		  }
+		  $str = "$mapline\n" ;
+		} else {
+		  if ($preproc) {
+			$str = "$thename $cleanfont $optionencoding <$fontname.pfb\n" ;
+		  } else {
+			# dvips can't subset OTF files, so we have to include the whole thing
+			# It looks like we also need to be explicit on where to find the file
+			$str = "$thename $cleanfont $optionencoding << $ttfpath/$fontname.$extension \n" ;
+		  }
+		}
+	  } else {
+		$str = "$thename $cleanfont $optionencoding <$fontname.$extension\n" ;
+	  }
+	} else {
+	  $str = "$thename $cleanfont $optionencoding <$fontname.$extension\n" ;
+	}
+    return ($str, $thename); }
+#	return $str; }
+
+
+sub build_dvipdfm_mapline
+  { my ($option, $usename, $fontname, $rawname, $cleanfont, $encoding, $varlabel, $strange)  = @_;
+    my $cleanname = $fontname;
+	$cleanname =~ s/\_//gio ;
+	$option =~ s/([\d\.]+)\s+SlantFont/ -s $1 /;
+	$option =~ s/([\d\.]+)\s+ExtendFont/ -e $1 /;
+    $option =~ s/^\s+(.*)/$1/o ;
+    $option =~ s/(.*)\s+$/$1/o ;
+    $option =~ s/  / /g ;
+    # adding cleanfont is kind of dangerous
+    my $thename = "";
+	my $str = "";
+    my $theencoding = "" ;
+    if ($strange ne "")
+      { $thename = $cleanname ; $theencoding = "" ; }
+    elsif ($lcdf)
+      { $thename = $usename ; $theencoding = " $encoding$varlabel-$cleanname" }
+    elsif ($afmpl)
+      { $thename = $usename ; $theencoding = " $encoding$varlabel" }
+    elsif ($virtual)
+      { $thename = $rawname ; $theencoding = " $encoding$varlabel" }
+    else
+      { $thename = $usename ; $theencoding = " $encoding$varlabel" }
+if ($uselmencodings) {
+    $theencoding =~ s/^(ec)\.enc/lm\-$1.enc/ ;
+}
+    # quit rest if no type 1 file
+    my $pfb_sourcepath = $sourcepath ;
+    $pfb_sourcepath =~ s@/afm/@/type1/@ ;
+    unless ((-e "$pfbpath/$fontname.$extension")||
+			(-e "$pfb_sourcepath/$fontname.$extension")||
+			(-e "$sourcepath/$fontname.$extension")||
+			(-e "$ttfpath/$fontname.$extension"))
+	  { if ($tex) { $report .= "missing file: \\type \{$fontname.pfb\}\n" }
+		report ("missing pfb file : $fontname.pfb") }
+    # now add entry to map
+    if ($strange eq "") {
+	  if ($extension eq "otf") {
+		#TH: todo
+	  } else {
+		$str = "$thename $theencoding $fontname $option\n" ;
+	  }
+	} else {
+	  $str = "$thename $fontname $option\n" ;
+	}
+    return ($str, $thename); }
+#	return $str; }
+
+
+sub preprocess_font
+  { my ($infont,$pfbfont) = @_ ;
+    if ($infont ne "")
+      { report ("otf/ttf source file : $infont") ;
+        report ("destination file : $pfbfont") ; }
+    else
+      { error ("missing otf/ttf source file") }
+    open (CONVERT, "| pfaedit -script -") || error ("couldn't open pipe to pfaedit") ;
+    report ("pre-processing with : pfaedit") ;
+    print CONVERT "Open('$infont');\n Generate('$pfbfont', '', 1) ;\n" ;
+    close (CONVERT) }
+
+foreach my $file (@files)
+  { my $option = my $slant = my $spaced = my $extend = my $vfstr = my $encstr = "" ;
+    my $strange = "" ; my ($rawfont,$cleanfont,$restfont) ;
+    $file = $file ;
+    my $ok = $file =~ /(.*)\/(.+?)\.(.*)/ ;
+    my ($path,$name,$suffix) = ($1,$2,$3) ;
+    # remove trailing _'s
+    my $fontname = $name ;
+    my $cleanname = $fontname ;
+    $cleanname =~ s/\_//gio ;
+    # atl: pre-process an opentype or truetype file by converting to pfb
+    if ($preproc && !$lcdf)
+      { unless (-f "$afmpath/$cleanname.afm" && -f "$pfbpath/$cleanname.pfb")
+          { preprocess_font("$path/$name.$suffix", "$pfbpath/$cleanname.pfb") ;
+            rename("$pfbpath/$cleanname.afm", "$afmpath/$cleanname.afm")
+	      || error("couldn't move afm product of pre-process.") }
+        $path = $afmpath ;
+        $file = "$afmpath/$cleanname.afm" }
+    # cleanup
+    foreach my $suf ("tfm", "vf", "vpl")
+      { UnLink "$raw$cleanname$fontsuffix.$suf" ;
+        UnLink "$use$cleanname$fontsuffix.$suf" }
+    UnLink "texfont.log" ;
+    # set switches
+    if ($encoding ne "")
+      { $encstr = " -T $encfil" }
+    if ($caps ne "")
+      { $vfstr = " -V $use$cleanname$fontsuffix" }
+    else # if ($virtual)
+      { $vfstr = " -v $use$cleanname$fontsuffix" }
+    my $font = "";
+    # let's see what we have here (we force texnansi.enc to avoid error messages)
+    if ($lcdf)
+      { my $command = "otfinfo -p $file" ;
+        print "$command\n" if $trace;
+        $font = `$command` ;
+        chomp $font ;
+        $cleanname = $cleanfont = $font }
+    else
+      { my $command = "afm2tfm \"$file\" -p texnansi.enc texfont.tfm" ;
+        print "$command (for testing)\n" if $trace ;
+        $font = `$command` ;
+        UnLink "texfont.tfm" ;
+        ($rawfont,$cleanfont,$restfont) = split(/\s/,$font) }
+    if ($font =~ /(math|expert)/io) { $strange = lc $1 }
+    $cleanfont =~ s/\_/\-/goi ;
+    $cleanfont =~ s/\-+$//goi ;
+    print "\n" ;
+    if (($strange eq "expert")&&($expert))
+      { report ("font identifier : $cleanfont$namesuffix -> $strange -> tfm") }
+    elsif ($strange ne "")
+      { report ("font identifier : $cleanfont$namesuffix -> $strange -> skipping") }
+    elsif ($afmpl)
+      { report ("font identifier : $cleanfont$namesuffix -> text -> tfm") }
+    elsif ($virtual)
+      { report ("font identifier : $cleanfont$namesuffix -> text -> tfm + vf") }
+    else
+      { report ("font identifier : $cleanfont$namesuffix -> text -> tfm") }
+    # don't handle strange fonts
+    if ($strange eq "")
+      { # atl: support for lcdf otftotfm
+        if ($lcdf && $extension eq "otf")
+          { # no vf, bypass afm, use otftotfm to get encoding and tfm
+            my $varstr = my $encout = my $tfmout = "" ;
+            report "processing files : otf -> tfm + enc" ;
+            if ($encoding ne "")
+              { $encfil = `kpsewhich -progname=pdftex $encoding.enc` ;
+                chomp $encfil ; if ($encfil eq "") { $encfil = "$encoding.enc" }
+                $encstr = " -e $encfil " }
+            if ($variant ne "")
+              { ( $varstr = $variant ) =~ s/,/ -f /goi ;
+                $varstr = " -f $varstr" }
+            $encout = "$encpath/$use$cleanfont.enc" ;
+            if (-e $encout)
+              { report ("renaming : $encout -> $use$cleanfont.bak") ;
+                UnLink "$encpath/$use$cleanfont.bak" ;
+                rename $encout, "$encpath/$use$cleanfont.bak" }
+    	    UnLink "texfont.map" ;
+            $tfmout = "$use$cleanfont$fontsuffix" ;
+            my $otfcommand = "otftotfm -a $varstr $encstr $passon $shape --name=\"$tfmout\" --encoding-dir=\"$encpath/\" --tfm-dir=\"$tfmpath/\" --vf-dir=\"$vfpath/\" --no-type1 --map-file=./texfont.map \"$file\"" ;
+            print "$otfcommand\n"  if $trace ;
+            system("$otfcommand") ;
+            $encfil = $encout }
+        else
+          { # generate tfm and vpl, $file is on afm path
+            my $font = '' ;
+            if ($afmpl)
+              { report "         generating pl : $cleanname$fontsuffix (from $cleanname)" ;
+                $encstr = " -p $encfil" ;
+                if ($uselmencodings) {
+                    $encstr =~ s/(ec)\.enc$/lm\-$1\.enc/ ;
+                }
+                my $command = "afm2pl -f afm2tfm $shape $passon $encstr $file $cleanname$fontsuffix.vpl" ;
+                print "$command\n" if $trace ;
+                my $ok = `$command` ;
+                if (open (TMP,"$cleanname$fontsuffix.map"))
+                   { $font = <TMP> ;
+                     close(TMP) ;
+                     UnLink "$cleanname$fontsuffix.map" } }
+            else
+              { report "generating raw tfm/vpl : $raw$cleanname$fontsuffix (from $cleanname)" ;
+                my $command = "afm2tfm $file $shape $passon $encstr $vfstr $raw$cleanname$fontsuffix" ;
+                print "$command\n" if $trace ;
+                $font = `$command` }
+			# generate vf file if needed
+			chomp $font ;
+			if ($font =~ /.*?([\d\.]+)\s*ExtendFont/io) { $extend = $1 }
+			if ($font =~ /.*?([\d\.]+)\s*SlantFont/io)  { $slant  = $1 }
+			if ($extend ne "") { $option .= " $extend ExtendFont " }
+			if ($slant ne "")  { $option .= " $slant SlantFont " }
+			if ($afmpl)
+			  { if ($noligs||$nofligs) { removeligatures("$cleanname$fontsuffix") }
+                report "generating new tfm : $use$cleanname$fontsuffix" ;
+				my $command = "pltotf $cleanname$fontsuffix.vpl $use$cleanname$fontsuffix.tfm" ;
+				print "$command\n" if $trace ;
+				my $ok = `$command` }
+			elsif ($virtual)
+			  { if ($noligs||$nofligs) { removeligatures("$use$cleanname$fontsuffix") }
+                report "generating new vf : $use$cleanname$fontsuffix (from $use$cleanname)" ;
+				my $command = "vptovf $use$cleanname$fontsuffix.vpl $use$cleanname$fontsuffix.vf $use$cleanname$fontsuffix.tfm" ;
+				print "$command\n" if $trace ;
+				my $ok = `$command` }
+			else
+			  { if ($noligs||$nofligs) { removeligatures("$raw$cleanname$fontsuffix") }
+                report "generating new tfm : $use$cleanname$fontsuffix (from $raw$cleanname)" ;
+				my $command = "pltotf $raw$cleanname$fontsuffix.vpl $use$cleanname$fontsuffix.tfm" ;
+				print "$command\n" if $trace ;
+				my $ok = `$command` } } }
+    elsif (-e "$sourcepath/$cleanname.tfm" )
+      { report "using existing tfm : $cleanname.tfm" }
+    elsif (($strange eq "expert")&&($expert))
+      { report "creating tfm file : $cleanname.tfm" ;
+        my $command = "afm2tfm $file $cleanname.tfm" ;
+        print "$command\n" if $trace ;
+        my $font = `$command` }
+    else
+      { report "use supplied tfm : $cleanname" }
+    # report results
+    if (!$lcdf)
+    { ($rawfont,$cleanfont,$restfont) = split(/\s/,$font) }
+    $cleanfont =~ s/\_/\-/goi ;
+    $cleanfont =~ s/\-+$//goi ;
+    # copy files
+    my $usename = "$use$cleanname$fontsuffix" ;
+    my $rawname = "$raw$cleanname$fontsuffix" ;
+
+    if ($lcdf eq "")
+    { if ($strange ne "")
+        { UnLink ("$vfpath/$cleanname.vf", "$tfmpath/$cleanname.tfm") ;
+          copy ("$cleanname.tfm","$tfmpath/$cleanname.tfm") ;
+          copy ("$usename.tfm","$tfmpath/$usename.tfm") ;
+          # or when available, use vendor one :
+          copy ("$sourcepath/$cleanname.tfm","$tfmpath/$cleanname.tfm") }
+      elsif ($virtual)
+        { UnLink ("$vfpath/$rawname.vf", "$vfpath/$usename.vf") ;
+          UnLink ("$tfmpath/$rawname.tfm", "$tfmpath/$usename.tfm") ;
+          copy ("$usename.vf" ,"$vfpath/$usename.vf") ;
+          copy ("$rawname.tfm","$tfmpath/$rawname.tfm") ;
+          copy ("$usename.tfm","$tfmpath/$usename.tfm") }
+      elsif ($afmpl)
+        { UnLink ("$vfpath/$rawname.vf", "$vfpath/$usename.vf", "$vfpath/$cleanname.vf") ;
+          UnLink ("$tfmpath/$rawname.tfm", "$tfmpath/$usename.tfm", "$tfmpath/$cleanname.tfm") ;
+          copy ("$usename.tfm","$tfmpath/$usename.tfm") }
+      else
+        { UnLink ("$vfpath/$usename.vf", "$tfmpath/$usename.tfm") ;
+          # slow but prevents conflicting vf's
+          my $rubish = `kpsewhich $usename.vf` ; chomp $rubish ;
+          if ($rubish ne "") { UnLink $rubish }
+          #
+          copy ("$usename.tfm","$tfmpath/$usename.tfm") } }
+    # cleanup
+    foreach my $suf ("tfm", "vf", "vpl")
+      { UnLink ("$rawname.$suf", "$usename.$suf") ;
+        UnLink ("$cleanname.$suf", "$fontname.$suf") ;
+        UnLink ("$cleanname$fontsuffix.$suf", "$fontname$fontsuffix.$suf") }
+    # add line to map files
+	my $str = my $thename = "";
+    ($str, $thename) = build_pdftex_mapline($option, $usename, $fontname, $rawname, $cleanfont, $encoding, $varlabel, $strange);
+	# check for redundant entries
+    if (defined $PDFTEXMAP) {
+	  $pdftexmapdata =~ s/^$thename\s.*?$//gmis ;
+	  if ($afmpl) {
+		if ($pdftexmapdata =~ s/^$rawname\s.*?$//gmis) {
+		  report ("removing raw file : $rawname") ;
+		}
+	  }
+	  $maplist .= $str ;
+	  $pdftexmapdata .= $str ;
+    }
+    ($str, $thename) = build_dvips_mapline($option, $usename, $fontname, $rawname, $cleanfont, $encoding, $varlabel, $strange);
+	# check for redundant entries
+    if (defined $DVIPSMAP) {
+	  $dvipsmapdata =~ s/^$thename\s.*?$//gmis ;
+	  if ($afmpl) {
+		if ($dvipsmapdata =~ s/^$rawname\s.*?$//gmis) {
+		  report ("removing raw file : $rawname") ;
+		}
+	  }
+	  $dvipsmapdata .= $str ;
+    }
+    ($str, $thename) = build_dvipdfm_mapline($option, $usename, $fontname, $rawname, $cleanfont, $encoding, $varlabel, $strange);
+	# check for redundant entries
+    if (defined $DVIPDFMMAP) {
+	  $dvipdfmmapdata =~ s/^$thename\s.*?$//gmis ;
+	  if ($afmpl) {
+		if ($dvipdfmmapdata =~ s/^$rawname\s.*?$//gmis) {
+		  report ("removing raw file : $rawname") ;
+		}
+	  }
+	  $dvipdfmmapdata .= $str ;
+    }
+
+    # write lines to tex file
+    if (($strange eq "expert")&&($expert)) {
+        $fntlist .= "\\definefontsynonym[$cleanfont$namesuffix][$cleanname] \% expert\n" ;
+    } elsif ($strange ne "") {
+        $fntlist .= "\%definefontsynonym[$cleanfont$namesuffix][$cleanname]\n" ;
+    } else {
+        $fntlist .= "\\definefontsynonym[$cleanfont$namesuffix][$usename][encoding=$encoding]\n" ;
+    }
+    next unless $tex ;
+    if (($strange eq "expert")&&($expert)) {
+        $texlist .= "\\ShowFont[$cleanfont$namesuffix][$cleanname]\n" ;
+    } elsif ($strange ne "") {
+        $texlist .= "\%ShowFont[$cleanfont$namesuffix][$cleanname]\n" ;
+    } else {
+        $texlist .= "\\ShowFont[$cleanfont$namesuffix][$usename][$encoding]\n"
+    }
+}
+
+finish_mapfile("pdftex",  $PDFTEXMAP,  $pdftexmapdata);
+finish_mapfile("dvipdfm", $DVIPDFMMAP, $dvipdfmmapdata);
+finish_mapfile("dvips",   $DVIPSMAP,   $dvipsmapdata);
+
+if ($tex)
+  { my $mappath = mappath("pdftex");
+    $mappath =~ s/\\/\//go ;
+    $savedoptions =~ s/^\s+//gmois ; $savedoptions =~ s/\s+$//gmois ;
+    $fntlist      =~ s/^\s+//gmois ; $fntlist      =~ s/\s+$//gmois ;
+    $maplist      =~ s/^\s+//gmois ; $maplist      =~ s/\s+$//gmois ;
+    print TEX "$texlist" ;
+    print TEX "\n" ;
+    print TEX "\\setupheadertexts[\\tttf example definitions]\n" ;
+    print TEX "\n" ;
+    print TEX "\\starttyping\n" ;
+    print TEX "texfont $savedoptions\n" ;
+    print TEX "\\stoptyping\n" ;
+    print TEX "\n" ;
+    print TEX "\\starttyping\n" ;
+    print TEX "$mappath/$mapfile\n" ;
+    print TEX "\\stoptyping\n" ;
+    print TEX "\n" ;
+    print TEX "\\starttyping\n" ;
+    print TEX "$fntlist\n" ;
+    print TEX "\\stoptyping\n" ;
+    print TEX "\n" ;
+    print TEX "\\page\n" ;
+    print TEX "\n" ;
+    print TEX "\\setupheadertexts[\\tttf $mapfile]\n" ;
+    print TEX "\n" ;
+    print TEX "\\starttyping\n" ;
+    print TEX "$maplist\n" ;
+    print TEX "\\stoptyping\n" ;
+    print TEX "\n" ;
+    print TEX "\\stoptext\n" }
+
+if ($tex) { close (TEX) }
+
+# atl: global cleanup with generated files (afm & ttf don't mix)
+
+UnLink(@cleanup) ;
+
+print "\n" ; report ("generating : ls-r databases") ;
+
+# Refresh database.
+
+print "\n" ; system ("mktexlsr $fontroot") ; print "\n" ;
+
+# Process the test file.
+
+if ($show) { system ("texexec --once --silent $texfile") }
+
+@files = validglob("$identifier.* *-$identifier.map") ;
+
+foreach my $file (@files)
+  { unless ($file =~ /(tex|pdf|log|mp|tmp)$/io) { UnLink ($file) } }
+
+exit ;
diff --git a/scripts/context/ruby/base/ctx.rb b/scripts/context/ruby/base/ctx.rb
new file mode 100644
index 000000000..13b4045af
--- /dev/null
+++ b/scripts/context/ruby/base/ctx.rb
@@ -0,0 +1,462 @@
+# module    : base/ctx
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+# todo: write systemcall for mpost to file so that it can be run
+# faster
+
+# report ?
+
+require 'base/system'
+require 'base/file'
+require 'base/switch' # has needsupdate, bad place
+
+require 'rexml/document'
+
+class CtxRunner
+
+    attr_reader :environments, :modules, :filters, :flags, :modes
+
+    @@suffix = 'prep'
+
+    def initialize(jobname=nil,logger=nil)
+        if @logger = logger then
+            def report(str='')
+                @logger.report(str)
+            end
+        else
+            def report(str='')
+                puts(str)
+            end
+        end
+        @jobname = jobname
+        @ctxname = nil
+        @xmldata = nil
+        @prepfiles = Hash.new
+        @environments = Array.new
+        @modules = Array.new
+        @filters = Array.new
+        @flags = Array.new
+        @modes = Array.new
+        @local = false
+        @paths = Array.new
+    end
+
+    def register_path(str)
+        @paths << str
+    end
+
+    def manipulate(ctxname=nil,defaultname=nil)
+
+        if ctxname then
+            @ctxname = ctxname
+            @jobname = File.suffixed(@ctxname,'tex') unless @jobname
+        else
+            @ctxname = File.suffixed(@jobname,'ctx') if @jobname
+        end
+
+        if not @ctxname then
+            report('no ctx file specified')
+            return
+        end
+
+        if @ctxname !~ /\.[a-z]+$/ then
+            @ctxname += ".ctx"
+        end
+
+        # name can be kpse:res-make.ctx
+        if not FileTest.file?(@ctxname) then
+            fullname, done = '', false
+            if @ctxname =~ /^kpse:/ then
+                begin
+                    if fullname = Kpse.found(@ctxname.sub(/^kpse:/,'')) then
+                        @ctxname, done = fullname, true
+                    end
+                rescue
+                    # should not happen
+                end
+            else
+                ['..','../..'].each do |path|
+                    begin
+                        fullname = File.join(path,@ctxname)
+                        if FileTest.file?(fullname) then
+                            @ctxname, done = fullname, true
+                        end
+                    rescue
+                        # probably strange join
+                    end
+                    break if done
+                end
+                if ! done then
+                    fullname = Kpse.found(@ctxname)
+                    if FileTest.file?(fullname) then
+                        @ctxname, done = fullname, true
+                    end
+                end
+            end
+            if ! done && defaultname && FileTest.file?(defaultname) then
+                report("using default ctxfile #{defaultname}")
+                @ctxname, done = defaultname, true
+            end
+            if not done then
+                report('no ctx file found')
+                return false
+            end
+        end
+
+        if FileTest.file?(@ctxname) then
+            @xmldata = IO.read(@ctxname)
+        else
+            report('no ctx file found')
+            return false
+        end
+
+        unless @xmldata =~ /^.*<\?xml.*?\?>/moi then
+            report("ctx file #{@ctxname} is no xml file, skipping")
+            return
+        else
+            report("loading ctx file #{@ctxname}")
+        end
+
+        if @xmldata then
+            # out if a sudden rexml started to be picky about namespaces
+            @xmldata.gsub!(/<ctx:job>/,"<ctx:job xmlns:ctx='http://www.pragma-ade.com/rng/ctx.rng'>")
+        end
+
+        begin
+            @xmldata = REXML::Document.new(@xmldata)
+        rescue
+            report('provide valid ctx file (xml error)')
+            return
+        else
+            include(@xmldata,'ctx:include','name')
+        end
+
+        begin
+            variables = Hash.new
+            if @jobname then
+                variables['job'] = @jobname
+            end
+            root = @xmldata.root
+            REXML::XPath.each(root,"/ctx:job//ctx:flags/ctx:flag") do |flg|
+                @flags << justtext(flg)
+            end
+            REXML::XPath.each(root,"/ctx:job//ctx:resources/ctx:environment") do |sty|
+                @environments << justtext(sty)
+            end
+            REXML::XPath.each(root,"/ctx:job//ctx:resources/ctx:module") do |mod|
+                @modules << justtext(mod)
+            end
+            REXML::XPath.each(root,"/ctx:job//ctx:resources/ctx:filter") do |fil|
+                @filters << justtext(fil)
+            end
+            REXML::XPath.each(root,"/ctx:job//ctx:resources/ctx:mode") do |fil|
+                @modes << justtext(fil)
+            end
+            begin
+                REXML::XPath.each(root,"//ctx:block") do |blk|
+                    if @jobname && blk.attributes['pattern'] then
+                        root.delete(blk) unless @jobname =~ /#{blk.attributes['pattern']}/
+                    else
+                        root.delete(blk)
+                    end
+                end
+            rescue
+            end
+            REXML::XPath.each(root,"//ctx:value[@name='job']") do |val|
+                substititute(val,variables['job'])
+            end
+            REXML::XPath.each(root,"/ctx:job//ctx:message") do |mes|
+                report("preprocessing: #{justtext(mes)}")
+            end
+            REXML::XPath.each(root,"/ctx:job//ctx:process/ctx:resources/ctx:environment") do |sty|
+                @environments << justtext(sty)
+            end
+            REXML::XPath.each(root,"/ctx:job//ctx:process/ctx:resources/ctx:module") do |mod|
+                @modules << justtext(mod)
+            end
+            REXML::XPath.each(root,"/ctx:job//ctx:process/ctx:resources/ctx:filter") do |fil|
+                @filters << justtext(fil)
+            end
+            REXML::XPath.each(root,"/ctx:job//ctx:process/ctx:resources/ctx:mode") do |fil|
+                @modes << justtext(fil)
+            end
+            REXML::XPath.each(root,"/ctx:job//ctx:process/ctx:flags/ctx:flag") do |flg|
+                @flags << justtext(flg)
+            end
+            commands = Hash.new
+            REXML::XPath.each(root,"/ctx:job//ctx:preprocess/ctx:processors/ctx:processor") do |pre|
+                begin
+                    commands[pre.attributes['name']] = pre
+                rescue
+                end
+            end
+            suffix = @@suffix
+            begin
+                suffix = REXML::XPath.match(root,"/ctx:job//ctx:preprocess/@suffix").to_s
+            rescue
+                suffix = @@suffix
+            else
+                if suffix && suffix.empty? then suffix = @@suffix end
+            end
+            if (REXML::XPath.first(root,"/ctx:job//ctx:preprocess/ctx:processors/@local").to_s =~ /(yes|true)/io rescue false) then
+                @local = true
+            else
+                @local = false
+            end
+            REXML::XPath.each(root,"/ctx:job//ctx:preprocess/ctx:files") do |files|
+                REXML::XPath.each(files,"ctx:file") do |pattern|
+                    suffix = @@suffix
+                    begin
+                        suffix = REXML::XPath.match(root,"/ctx:job//ctx:preprocess/@suffix").to_s
+                    rescue
+                        suffix = @@suffix
+                    else
+                        if suffix && suffix.empty? then suffix = @@suffix end
+                    end
+                    preprocessor = pattern.attributes['processor']
+                    if preprocessor and not preprocessor.empty? then
+                        begin
+                            variables['old'] = @jobname
+                            variables['new'] = ""
+                            REXML::XPath.each(pattern,"ctx:value") do |value|
+                                if name = value.attributes['name'] then
+                                    substititute(value,variables[name.to_s])
+                                end
+                            end
+                        rescue
+                            report('unable to resolve file pattern')
+                            return
+                        end
+                        pattern = justtext(pattern)
+                        oldfiles = Dir.glob(pattern)
+                        pluspath = false
+                        if oldfiles.length == 0 then
+                            report("no files match #{pattern}")
+                            if @paths.length > 0 then
+                                @paths.each do |p|
+                                    oldfiles = Dir.glob("#{p}/#{pattern}")
+                                    if oldfiles.length > 0 then
+                                        pluspath = true
+                                        break
+                                    end
+                                end
+                                if oldfiles.length == 0 then
+                                    report("no files match #{pattern} on path")
+                                end
+                            end
+                        end
+                        oldfiles.each do |oldfile|
+                            newfile = "#{oldfile}.#{suffix}"
+                            newfile = File.basename(newfile) if @local # or pluspath
+                            if File.expand_path(oldfile) != File.expand_path(newfile) && File.needsupdate(oldfile,newfile) then
+                                report("#{oldfile} needs preprocessing")
+                                begin
+                                    File.delete(newfile)
+                                rescue
+                                    # hope for the best
+                                end
+                                # there can be a sequence of processors
+                                preprocessor.split(',').each do |pp|
+                                    if command = commands[pp] then
+                                        # a lie: no <?xml ...?>
+                                        command = REXML::Document.new(command.to_s) # don't infect original
+                                        # command = command.deep_clone() # don't infect original
+                                        command = command.elements["ctx:processor"]
+                                        if suf = command.attributes['suffix'] then
+                                            newfile = "#{oldfile}.#{suf}"
+                                        end
+                                        begin
+                                            newfile = File.basename(newfile) if @local
+                                        rescue
+                                        end
+                                        REXML::XPath.each(command,"ctx:old") do |value| replace(value,oldfile) end
+                                        REXML::XPath.each(command,"ctx:new") do |value| replace(value,newfile) end
+                                        report("preprocessing #{oldfile} into #{newfile} using #{pp}")
+                                        variables['old'] = oldfile
+                                        variables['new'] = newfile
+                                        REXML::XPath.each(command,"ctx:value") do |value|
+                                            if name = value.attributes['name'] then
+                                                substititute(value,variables[name.to_s])
+                                            end
+                                        end
+                                        command = justtext(command)
+                                        report(command)
+                                        unless ok = System.run(command) then
+                                            report("error in preprocessing file #{oldfile}")
+                                        end
+                                        begin
+                                            oldfile = File.basename(oldfile) if @local
+                                        rescue
+                                        end
+                                    end
+                                end
+                                if FileTest.file?(newfile) then
+                                    File.syncmtimes(oldfile,newfile)
+                                else
+                                    report("check target location of #{newfile}")
+                                end
+                            else
+                                report("#{oldfile} needs no preprocessing (same file)")
+                            end
+                            @prepfiles[oldfile] = FileTest.file?(newfile)
+                        end
+                    end
+                end
+            end
+        rescue
+            report("fatal error in preprocessing #{@ctxname}: #{$!}")
+        end
+    end
+
+    def savelog(ctlname=nil)
+        unless ctlname then
+            if @jobname then
+                ctlname = File.suffixed(@jobname,'ctl')
+            elsif @ctxname then
+                ctlname = File.suffixed(@ctxname,'ctl')
+            else
+                return
+            end
+        end
+        if @prepfiles.length > 0 then
+            if log = File.open(ctlname,'w') then
+                log << "<?xml version='1.0' standalone='yes'?>\n\n"
+                if @local then
+                    log << "<ctx:preplist local='yes'>\n"
+                else
+                    log << "<ctx:preplist local='no'>\n"
+                end
+                @prepfiles.keys.sort.each do |prep|
+                    # log << "\t<ctx:prepfile done='#{yes_or_no(@prepfiles[prep])}'>#{File.basename(prep)}</ctx:prepfile>\n"
+                    log << "\t<ctx:prepfile done='#{yes_or_no(@prepfiles[prep])}'>#{prep}</ctx:prepfile>\n"
+                end
+                log << "</ctx:preplist>\n"
+                log.close
+            end
+        else
+            begin
+                File.delete(ctlname)
+            rescue
+            end
+        end
+    end
+
+    private
+
+    def include(xmldata,element='ctx:include',attribute='name')
+        loop do
+            begin
+                more = false
+                REXML::XPath.each(xmldata.root,element) do |e|
+                    begin
+                        name = e.attributes.get_attribute(attribute).to_s
+                        name = e.text.to_s if name.empty?
+                        name.strip! if name
+                        done = false
+                        if name and not name.empty? then
+                            ['.',File.dirname(@ctxname),'..','../..'].each do |path|
+                                begin
+                                    fullname = if path == '.' then name else File.join(path,name) end
+                                    if FileTest.file?(fullname) then
+                                        if f = File.open(fullname,'r') and i = REXML::Document.new(f) then
+                                            report("including ctx file #{name}")
+                                            REXML::XPath.each(i.root,"*") do |ii|
+                                                xmldata.root.insert_before(e,ii)
+                                                more = true
+                                            end
+                                        end
+                                        done = true
+                                    end
+                                rescue
+                                end
+                                break if done
+                            end
+                        end
+                        report("no valid ctx inclusion file #{name}") unless done
+                    rescue Exception
+                        # skip this file
+                    ensure
+                        xmldata.root.delete(e)
+                    end
+                end
+                break unless more
+            rescue Exception
+                break # forget about inclusion
+            end
+        end
+    end
+
+    private
+
+    def yes_or_no(b)
+        if b then 'yes' else 'no' end
+    end
+
+    private # copied from rlxtools.rb
+
+    def justtext(str)
+        str = str.to_s
+        str.gsub!(/<[^>]*?>/o, '')
+        str.gsub!(/\s+/o, ' ')
+        str.gsub!(/&lt;/o, '<')
+        str.gsub!(/&gt;/o, '>')
+        str.gsub!(/&amp;/o, '&')
+        str.gsub!(/&quot;/o, '"')
+        str.gsub!(/[\/\\]+/o, '/')
+        return str.strip
+    end
+
+    def substititute(value,str)
+        if str then
+            begin
+                if value.attributes.key?('method') then
+                    str = filtered(str.to_s,value.attributes['method'].to_s)
+                end
+                if str.empty? && value.attributes.key?('default') then
+                    str = value.attributes['default'].to_s
+                end
+                value.insert_after(value,REXML::Text.new(str.to_s))
+            rescue Exception
+            end
+        end
+    end
+
+    def replace(value,str)
+        if str then
+            begin
+                value.insert_after(value,REXML::Text.new(str.to_s))
+            rescue Exception
+            end
+        end
+    end
+
+    def filtered(str,method)
+        str = str.to_s # to be sure
+        case method
+            when 'name' then # no path, no suffix
+                case str
+                    when /^.*[\\\/](.+?)\..*?$/o then $1
+                    when /^.*[\\\/](.+?)$/o      then $1
+                    when /^(.*)\..*?$/o          then $1
+                    else                              str
+                end
+            when 'path'     then if str =~ /^(.+)([\\\/])(.*?)$/o then $1 else ''  end
+            when 'suffix'   then if str =~ /^.*\.(.*?)$/o         then $1 else ''  end
+            when 'nosuffix' then if str =~ /^(.*)\..*?$/o         then $1 else str end
+            when 'nopath'   then if str =~ /^.*[\\\/](.*?)$/o     then $1 else str end
+            when 'base'     then if str =~ /^.*[\\\/](.*?)$/o     then $1 else str end
+            when 'full'     then str
+            when 'complete' then str
+            when 'expand'   then File.expand_path(str).gsub(/\\/,"/")
+            else                 str
+        end
+    end
+
+end
diff --git a/scripts/context/ruby/base/exa.rb b/scripts/context/ruby/base/exa.rb
new file mode 100644
index 000000000..7ba990cf9
--- /dev/null
+++ b/scripts/context/ruby/base/exa.rb
@@ -0,0 +1,407 @@
+# \setuplayout[width=3cm]
+#
+# tex.setup.setuplayout.width.[integer|real|dimension|string|key]
+# tex.[mp]var.whatever.width.[integer|real|dimension|string|key]
+
+require 'fileutils'
+# require 'ftools'
+require 'digest/md5'
+
+# this can become a lua thing
+
+# no .*? but 0-9a-z\:\. because other too slow (and greedy)
+
+class Hash
+
+    def subset(pattern)
+        h = Hash.new
+        r = /^#{pattern.gsub('.','\.')}/
+        self.keys.each do |k|
+            h[k] = self[k].dup if k =~ r
+        end
+        return h
+    end
+
+end
+
+module ExaEncrypt
+
+    def ExaEncrypt.encrypt_base(logger, oldfilename, newfilename)
+        if FileTest.file?(oldfilename) then
+            logger.report("checking #{oldfilename}") if logger
+            if data = IO.read(oldfilename) then
+                done = false
+                # cfg file:
+                #
+                # banner : exa configuration file
+                # user   : domain, name = password, projectlist
+                #
+                if data =~ /^\s*banner\s*\:\s*exa\s*configuration\s*file/ then
+                    data.gsub!(/^(\s*user\s*\:\s*.+?\s*\,\s*.+?\s*\=\s*)(.+?)(\s*\,\s*.+\s*)$/) do
+                        pre, password, post = $1, $2, $3
+                        unless password =~ /MD5:/i then
+                            done = true
+                            password = "MD5:" + Digest::MD5.hexdigest(password).upcase
+                        end
+                        "#{pre}#{password}#{post}"
+                    end
+                else
+                    data.gsub!(/<exa:password([^>]*?)>(.*?)<\/exa:password>/moi) do
+                        attributes, password = $1, $2
+                        unless password =~ /^([0-9A-F][0-9A-F])+$/ then
+                            done = true
+                            password = Digest::MD5.hexdigest(password).upcase
+                            attributes = " encryption='md5'#{attributes}"
+                        end
+                        "<exa:password#{attributes}>#{password}</exa:password>"
+                    end
+                end
+                begin
+                    File.open(newfilename,'w') do |f|
+                        f.puts(data)
+                    end
+                rescue
+                    logger.report("#{newfilename} cannot be written") if logger
+                else
+                    logger.report("#{oldfilename} encrypted into #{newfilename}") if done and logger
+                end
+            end
+        end
+    end
+
+end
+
+module ExaModes
+
+    @@modefile = 'examodes.tex'
+
+    @@request = /(<exa:request.*?)(>.*?<\/exa:request>)/mo
+    @@redone  = /<exa:request[^>]*?texified=([\'\"])yes\1.*?>/mo
+    @@reload  = /<(exa:variable)([^>]+?label\=)([\"\'])([0-9A-Za-z\-\.\:]+?)(\3[^\/]*?)>(.*?)<(\/exa:variable)>/mo
+    @@recalc  = /<(exa:variable)([^>]+?label\=)([\"\'])([0-9A-Za-z\-\.\:]+?)([\.\:]calcmath)(\3[^\/]*?)>(.*?)<(\/exa:variable)>/mo
+    @@rename  = /<(exa:variable)([^>]+?label\=)([\"\'])([0-9A-Za-z\-\.\:]+?)(\3[^\/]*?)>(.*?)<(\/exa:variable)>/mo
+    @@refile  = /<(exa:filename|exa:filelist)>(.*?)<(\/\1)>/mo
+
+    def ExaModes.cleanup_request(logger,filename='request.exa',modefile=@@modefile)
+        begin File.delete(filename+'.raw') ; rescue ; end
+        begin File.delete(modefile)        ; rescue ; end
+        if FileTest.file?(filename) then
+            data, done = nil, false
+            begin
+                data = IO.read(filename)
+            rescue
+                data = nil
+            end
+            if data =~ @@request and data !~ @@redone then
+                data.gsub!(@@rename) do
+                    done = true
+                    '<' + $1 + $2 + $3 + $4 + $5 + '>' +
+                    texifiedstr($4,$6) +
+                    '<' + $7 + '>'
+                end
+                data.gsub!(@@refile) do
+                    done = true
+                    '<' + $1 + '>' +
+                    cleanpath($2) +
+                    '<' + $3 + '>'
+                end
+                data.gsub!(@@recalc) do
+                    done = true
+                    '<' + $1 + $2 + $3 + $4 + ":raw" + $6 + '>' + $7 + '<' + $8 + '>' +
+                    '<' + $1 + $2 + $3 + $4 + $6 + '>' +
+                    calculatortexmath($7,false) +
+                    '<' + $8 + '>'
+                end
+                if done then
+                    data.gsub!(@@request) do
+                        $1 + " texified='yes'" + $2
+                    end
+                    begin File.copy(filename, filename+'.raw') ; rescue ; end
+                    begin
+                        logger.report("rewriting #{filename}") if logger
+                        File.open(filename,'w') do |f|
+                            f.puts(data)
+                        end
+                    rescue
+                        logger.report("#{filename} cannot be rewritten") if logger
+                    end
+                end
+            else
+                logger.report("#{filename} is already ok") if logger
+            end
+            @variables = Hash.new
+            data.scan(@@reload) do
+                @variables[$4] = $5
+            end
+            vars   = @variables.subset('data.tex.var')
+            mpvars = @variables.subset('data.tex.mpvar')
+            modes  = @variables.subset('data.tex.mode')
+            setups = @variables.subset('data.tex.setup')
+            if not (modes.empty? and setups.empty? and vars.empty? and mpvars.empty?) then
+                begin
+                    File.open(modefile,'w') do |mod|
+                        logger.report("saving modes and setups in #{modefile}") if logger
+                        if not modes.empty? then
+                            for key in modes.keys do
+                                k = key.dup
+                                k.gsub!(/\./,'-')
+                                mod.puts("\\enablemode[#{k}-#{modes[key]}]\n")
+                                if modes[key] =~ /(on|yes|start)/o then # ! ! ! ! !
+                                    mod.puts("\\enablemode[#{k}]\n")
+                                end
+                            end
+                            mod.puts("\n\\readfile{cont-mod}{}{}\n")
+                        end
+                        if not setups.empty? then
+                            for key in setups.keys
+                                if key =~ /^(.+?)\.(.+?)\.(.+?)$/o then
+                                    command, key, type, value = $1, $2, $3, setups[key]
+                                    value = cleanedup(key,type,value)
+                                    mod.puts("\\#{$1}[#{key}=#{value}]\n")
+                                elsif key =~ /^(.+?)\.(.+?)$/o then
+                                    command, type, value = $1, $2, setups[key]
+                                    mod.puts("\\#{$1}[#{value}]\n")
+                                end
+                            end
+                        end
+                        savevaroptions(vars,  'setvariables',  mod)
+                        savevaroptions(mpvars,'setMPvariables',mod)
+                    end
+                rescue
+                    logger.report("#{modefile} cannot be saved") if logger
+                end
+            else
+                logger.report("#{modefile} is not created") if logger
+            end
+        end
+    end
+
+    private
+
+    def ExaModes.autoparenthesis(str)
+        if str =~ /[\+\-]/o then '[1]' + str + '[1]' else str end
+    end
+
+    def ExaModes.cleanedup(key,type,value)
+        if type == 'dimension' then
+            unless value =~ /(cm|mm|in|bp|sp|pt|dd|em|ex)/o
+                value + 'pt'
+            else
+                value
+            end
+        elsif type == 'calcmath' then
+            '{' + calculatortexmath(value,true) + '}'
+        elsif type =~ /^filename|filelist$/ or key =~ /^filename|filelist$/ then
+            cleanpath(value)
+        else
+            value
+        end
+    end
+
+    def ExaModes.cleanpath(str)
+        (str ||'').gsub(/\\/o,'/')
+    end
+
+    def ExaModes.texifiedstr(key,val)
+        case key
+            when 'filename' then
+                cleanpath(val)
+            when 'filelist' then
+                cleanpath(val)
+            else
+                val
+        end
+    end
+
+    def ExaModes.savevaroptions(vars,setvariables,mod)
+        if not vars.empty? then
+            for key in vars.keys do
+                # var.whatever.width.dimension.value
+                if key =~ /^(.+?)\.(.+?)\.(.+?)$/o then
+                    tag, key, type, value = $1, $2, $3, vars[key]
+                    value = cleanedup(key,type,value)
+                    mod.puts("\\#{setvariables}[#{tag}][#{key}=#{value}]\n")
+                elsif key =~ /^(.+?)\.(.+?)$/o then
+                    tag, key, value = $1, $2, vars[key]
+                    mod.puts("\\#{setvariables}[#{tag}][#{key}=#{value}]\n")
+                end
+            end
+        end
+    end
+
+    def ExaModes.calculatortexmath(str,tx=true)
+        if tx then
+            bdisp, edisp = "\\displaymath\{", "\}"
+            binln, einln = "\\inlinemath\{" , "\}"
+            egraf        = "\\endgraf"
+        else
+            bdisp, edisp = "<displaytexmath>", "</displaytexmath>"
+            binln, einln = "<inlinetexmath>" , "</inlinetexmath>"
+            egraf        = "<p/>"
+        end
+        str.gsub!(/\n\s*\n+/moi, "\\ENDGRAF ")
+        str.gsub!(/(\[\[)\s*(.*?)\s*(\]\])/mos) do
+            $1 + docalculatortexmath($2) + $3
+        end
+        str.gsub!(/(\\ENDGRAF)+\s*(\[\[)\s*(.*?)\s*(\]\])/moi) do
+            $1 + bdisp + $3 + edisp
+        end
+        str.gsub!(/(\[\[)\s*(.*?)\s*(\]\])/o) do
+            binln + $2 + einln
+        end
+        str.gsub!(/\\ENDGRAF/mos, egraf)
+        str
+    end
+
+    def ExaModes.docalculatortexmath(str)
+        str.gsub!(/\n/o) { ' ' }
+        str.gsub!(/\s+/o) { ' ' }
+        str.gsub!(/&gt;/o) { '>' }
+        str.gsub!(/&lt;/o) { '<' }
+        str.gsub!(/&.*?;/o) { }
+        level = 0
+        str.gsub!(/([\(\)])/o) do |chr|
+            if    chr == '(' then
+                level = level + 1
+                chr = '[' + level.to_s + ']'
+            elsif chr == ')' then
+                chr = '[' + level.to_s + ']'
+                level = level - 1
+            end
+            chr
+        end
+        # ...E...
+        loop do
+            break unless str.gsub!(/([\d\.]+)E([\-\+]{0,1}[\d\.]+)/o) do
+                "\{\\SCINOT\{#{$1}\}\{#{$2}\}\}"
+            end
+        end
+        # ^-..
+        loop do
+            break unless str.gsub!(/\^([\-\+]*\d+)/o) do
+                "\^\{#{$1}\}"
+            end
+        end
+        # ^(...)
+        loop do
+            break unless str.gsub!(/\^(\[\d+\])(.*?)\1/o) do
+                "\^\{#{$2}\}"
+            end
+        end
+        # 1/x^2
+        loop do
+            break unless str.gsub!(/([\d\w\.]+)\/([\d\w\.]+)\^([\d\w\.]+)/o) do
+                "@\{#{$1}\}\{#{$2}\^\{#{$3}\}\}"
+            end
+        end
+        # int(a,b,c)
+        loop do
+            break unless str.gsub!(/(int|sum|prod)(\[\d+\])(.*?),(.*?),(.*?)\2/o) do
+                "\\#{$1.upcase}\^\{#{$4}\}\_\{#{$5}\}\{#{autoparenthesis($3)}\}"
+            end
+        end
+        # int(a,b)
+        loop do
+            break unless str.gsub!(/(int|sum|prod)(\[\d+\])(.*?),(.*?)\2/o) do
+                "\\#{$1.upcase}\_\{#{$4}\}\{#{autoparenthesis($3)}\}"
+            end
+        end
+        # int(a)
+        loop do
+            break unless str.gsub!(/(int|sum|prod)(\[\d+\])(.*?)\2/o) do
+                "\\#{$1.upcase}\{#{autoparenthesis($3)}\}"
+            end
+        end
+        # sin(x) => {\sin(x)}
+        loop do
+            break unless str.gsub!(/(median|min|max|round|sqrt|sin|cos|tan|sinh|cosh|tanh|ln|log)\s*(\[\d+\])(.*?)\2/o) do
+                "\{\\#{$1.upcase}\{#{$2}#{$3}#{$2}\}\}"
+            end
+        end
+        # mean
+        str.gsub!(/(mean)(\[\d+\])(.*?)\2/o) do
+            "\{\\OVERLINE\{#{$3}\}\}"
+        end
+        # sin x  => {\sin(x)}
+        # ...
+        # (1+x)/(1+x) => \frac{1+x}{1+x}
+        loop do
+            break unless str.gsub!(/(\[\d+\])(.*?)\1\/(\[\d+\])(.*?)\3/o) do
+                "@\{#{$2}\}\{#{$4}\}"
+            end
+        end
+        # (1+x)/x => \frac{1+x}{x}
+        loop do
+            break unless str.gsub!(/(\[\d+\])(.*?)\1\/([a-zA-Z0-9]+)/o) do
+                "@\{#{$2}\}\{#{$3}\}"
+            end
+        end
+        # 1/(1+x) => \frac{1}{1+x}
+        loop do
+            break unless str.gsub!(/([a-zA-Z0-9]+)\/(\[\d+\])(.*?)\2/o) do
+                "@\{#{$1}\}\{#{$3}\}"
+            end
+        end
+        # 1/x => \frac{1}{x}
+        loop do
+            break unless str.gsub!(/([a-zA-Z0-9]+)\/([a-zA-Z0-9]+)/o) do
+                "@\{#{$1}\}\{#{$2}\}"
+            end
+        end
+        #
+        str.gsub!(/\@/o) do
+            "\\FRAC "
+        end
+        str.gsub!(/\*/o) do
+            " "
+        end
+        str.gsub!(/\<\=/o) do
+            "\\LE "
+        end
+        str.gsub!(/\>\=/o) do
+            "\\GE "
+        end
+        str.gsub!(/\=/o) do
+            "\\EQ "
+        end
+        str.gsub!(/\</o) do
+            "\\LT "
+        end
+        str.gsub!(/\>/) do
+            "\\GT "
+        end
+        str.gsub!(/(D)(\[\d+\])(.*?)\2/o) do
+            "\{\\FRAC\{\\MBOX{d}\}\{\\MBOX{d}x\}\{#{$2}#{$3}#{$2}\}\}"
+        end
+        str.gsub!(/(exp)(\[\d+\])(.*?)\2/o) do
+            "\{e^\{#{$3}\}\}"
+        end
+        str.gsub!(/(abs)(\[\d+\])(.*?)\2/o) do
+            "\{\\left\|#{$3}\\right\|\}"
+        end
+        str.gsub!(/D([x|y])/o) do
+            "\\FRAC\{\{\\rm d\}#{$1}\}\{\{\\rm d\}x\}"
+        end
+        str.gsub!(/D([f|g])(\[\d+\])(.*?)\2/o) do
+            "\{\\rm #{$1}\}'#{$2}#{$3}#{$2}"
+        end
+        str.gsub!(/([f|g])(\[\d+\])(.*?)\2/o) do
+            "\{\\rm #{$1}\}#{$2}#{$3}#{$2}"
+        end
+        str.gsub!(/(pi|inf)/io) do
+            "\\#{$1} "
+        end
+        loop do
+            break unless str.gsub!(/(\[\d+?\])(.*?)\1/o) do
+                "\\left(#{$2}\\right)"
+            end
+        end
+        str.gsub!(/\\([A-Z]+?)([\s\{\^\_\\])/io) do
+            "\\#{$1.downcase}#{$2}"
+        end
+        str
+    end
+
+end
+
+# ExaModes.cleanup_request()
diff --git a/scripts/context/ruby/base/file.rb b/scripts/context/ruby/base/file.rb
new file mode 100644
index 000000000..1aeac5fd6
--- /dev/null
+++ b/scripts/context/ruby/base/file.rb
@@ -0,0 +1,150 @@
+# module    : base/file
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2002-2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+require 'fileutils'
+# require 'ftools'
+
+class File
+
+    def File.suffixed(name,sufa,sufb=nil)
+        if sufb then
+            if sufa.empty? then
+                unsuffixed(name) + ".#{sufb}"
+            else
+                unsuffixed(name) + "-#{sufa}.#{sufb}"
+            end
+        else
+            unsuffixed(name) + ".#{sufa}"
+        end
+    end
+
+    def File.unsuffixed(name)
+        name.sub(/\.[^\.]*?$/o, '')
+    end
+
+    def File.suffix(name,default='')
+        if name =~ /\.([^\.]*?)$/o then
+            $1
+        else
+            default
+        end
+    end
+
+    def File.splitname(name,suffix='')
+        if name =~ /^(.*)\.([^\.]*?)$/o then
+            [$1, $2]
+        else
+            [name, suffix]
+        end
+    end
+
+end
+
+class File
+
+    def File.silentopen(name,method='r')
+        begin
+            f = File.open(name,method)
+        rescue
+            return nil
+        else
+            return f
+        end
+    end
+
+    def File.silentread(name)
+        begin
+            data = IO.read(name)
+        rescue
+            return nil
+        else
+            return data
+        end
+    end
+
+    def File.atleast?(name,n=0)
+        begin
+            size = FileTest.size(name)
+        rescue
+            return false
+        else
+            return size > n
+        end
+    end
+
+    def File.appended(name,str='')
+        if FileTest.file?(name) then
+            begin
+                if f = File.open(name,'a') then
+                    f << str
+                    f.close
+                    return true
+                end
+            rescue
+            end
+        end
+        return false
+    end
+
+    def File.written(name,str='')
+        begin
+            if f = File.open(name,'w') then
+                f << str
+                f.close
+                return true
+            end
+        rescue
+        end
+        return false
+    end
+
+    def File.silentdelete(filename)
+        File.delete(filename) rescue false
+    end
+
+    def File.silentcopy(oldname,newname)
+        return if File.expand_path(oldname) == File.expand_path(newname)
+        FileUtils.makedirs(File.dirname(newname)) rescue false
+        File.copy(oldname,newname) rescue false
+    end
+
+    def File.silentrename(oldname,newname)
+        # in case of troubles, we just copy the file; we
+        # maybe working over multiple file systems or
+        # apps may have mildly locked files (like gs does)
+        return if File.expand_path(oldname) == File.expand_path(newname)
+        File.delete(newname) rescue false
+        begin
+            File.rename(oldname,newname)
+        rescue
+            FileUtils.makedirs(File.dirname(newname)) rescue false
+            File.copy(oldname,newname) rescue false
+        end
+    end
+
+end
+
+class File
+
+    # handles "c:\tmp\test.tex" as well as "/${TEMP}/test.tex")
+
+    def File.unixfied(filename)
+        begin
+            str = filename.gsub(/\$\{*([a-z0-9\_]+)\}*/oi) do
+                if ENV.key?($1) then ENV[$1] else $1 end
+            end
+            str.gsub(/[\/\\]+/o, '/')
+        rescue
+            filename
+        end
+    end
+
+end
+
diff --git a/scripts/context/ruby/base/kpse.rb b/scripts/context/ruby/base/kpse.rb
new file mode 100644
index 000000000..0f9868784
--- /dev/null
+++ b/scripts/context/ruby/base/kpse.rb
@@ -0,0 +1,389 @@
+# module    : base/kpse
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2002-2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+# rename this one to environment
+#
+# todo: web2c vs miktex module and include in kpse
+
+require 'rbconfig'
+require 'fileutils'
+
+# beware $engine is lowercase in kpse
+#
+# miktex has mem|fmt|base paths
+
+class String
+
+    def split_path
+        if self =~ /\;/o || self =~ /^[a-z]\:/io then
+            self.split(";")
+        else
+            self.split(":")
+        end
+    end
+
+    def sane_path
+        self.gsub(/\\/,'/')
+    end
+
+end
+
+class Array
+
+    def join_path
+        self.join(File::PATH_SEPARATOR)
+    end
+
+    def non_empty
+        self.delete_if do |i|
+            (i == nil || i.empty?) rescue false
+        end
+    end
+
+end
+
+module Kpse
+
+    @@located       = Hash.new
+    @@paths         = Hash.new
+    @@scripts       = Hash.new
+    @@formats       = ['tex','texmfscripts','other text files']
+    @@progname      = 'context'
+    @@ownpath       = $0.sub(/[\\\/][a-z0-9\-]*?\.rb/i,'')
+    @@problems      = false
+    @@tracing       = false
+    @@distribution  = 'web2c'
+    @@crossover     = true
+    @@mswindows     = Config::CONFIG['host_os'] =~ /mswin/
+
+    # @@distribution  = 'miktex' if ENV['PATH'] =~ /miktex[\\\/]bin/o
+
+    # if ENV['PATH'] =~ /(.*?)miktex[\\\/]bin/i then
+        # @@distribution = 'miktex' unless $1 =~ /(texmf\-mswin[\/\\]bin|bin[\/\\]win32)/i
+    # end
+
+    if @@mswindows && (ENV['PATH'] =~ /(.*?)miktex[\\\/]bin/i) then
+         @@distribution = 'miktex' unless $1 =~ /(texmf\-mswin[\/\\]bin|bin[\/\\]win32)/i
+    end
+
+    @@re_true = /yes|on|true|1/i
+
+    if (ENV['KPSEFAST'] =~ @@re_true) || (ENV['CTXMINIMAL'] =~ @@re_true) then
+        @@usekpserunner = true
+        require 'base/kpsefast'
+        require 'base/kpserunner'
+    else
+        @@usekpserunner = false
+    end
+
+    if @@crossover then
+        ENV.keys.each do |k|
+            case k
+                when /\_CTX\_KPSE\_V\_(.*?)\_/io then @@located[$1] = ENV[k].dup
+                when /\_CTX\_KPSE\_P\_(.*?)\_/io then @@paths  [$1] = ENV[k].dup.split(';')
+                when /\_CTX\_KPSE\_S\_(.*?)\_/io then @@scripts[$1] = ENV[k].dup
+            end
+        end
+    end
+
+    def Kpse.distribution
+        @@distribution
+    end
+
+    def Kpse.miktex?
+        @@distribution == 'miktex'
+    end
+
+    def Kpse.web2c?
+        @@distribution == 'web2c'
+    end
+
+    def Kpse.inspect
+        @@located.keys.sort.each do |k| puts("located : #{k} -> #{@@located[k]}\n") end
+        @@paths  .keys.sort.each do |k| puts("paths   : #{k} -> #{@@paths  [k]}\n") end
+        @@scripts.keys.sort.each do |k| puts("scripts : #{k} -> #{@@scripts[k]}\n") end
+    end
+
+    def Kpse.used_path(varname)
+        begin
+            if @@mswindows then
+                path = run("--expand-path=\$#{varname}") rescue ''
+            else
+                path = run("--expand-path='$#{varname}'") rescue ''
+            end
+        rescue
+            path = ''
+        end
+        return path.sane_path
+    end
+
+    def Kpse.found(filename, progname=nil, format=nil)
+        begin
+            tag = Kpse.key(filename) # all
+            if @@located.key?(tag) then
+                return @@located[tag].sane_path
+            elsif FileTest.file?(filename) then
+                setvariable(tag,filename)
+                return filename
+            elsif FileTest.file?(File.join(@@ownpath,filename)) then
+                setvariable(tag,File.join(@@ownpath,filename))
+                return @@located[tag].sane_path
+            else
+                [progname,@@progname].flatten.compact.uniq.each do |prg|
+                    [format,@@formats].flatten.compact.uniq.each do |fmt|
+                        begin
+                            tag = Kpse.key(filename,prg,fmt)
+                            if @@located.key?(tag) then
+                                return @@located[tag].sane_path
+                            elsif p = Kpse.kpsewhich(filename,prg,fmt) then
+                                setvariable(tag,p.chomp)
+                                return @@located[tag].sane_path
+                            end
+                        rescue
+                        end
+                    end
+                end
+                setvariable(tag,filename)
+                return filename.sane_path
+            end
+        rescue
+            filename.sane_path
+        end
+    end
+
+    def Kpse.kpsewhich(filename,progname,format)
+        p = if progname && ! progname.empty? then "-progname=#{progname}" else '' end
+        f = if format   && ! format.empty?   then "-format=\"#{format}\"" else '' end
+        Kpse.run("#{p} #{f} #{filename}")
+    end
+
+    def Kpse.which
+        Kpse.kpsewhich
+    end
+
+    def Kpse.run(arguments)
+        puts arguments if @@tracing
+        begin
+            if @@problems then
+                results = ''
+            elsif @@usekpserunner then
+                results = KpseRunner.kpsewhich(arguments).chomp
+            else
+                results = `kpsewhich #{arguments}`.chomp
+            end
+        rescue
+            puts "unable to run kpsewhich" if @@tracing
+            @@problems, results = true, ''
+        end
+        puts results if @@tracing
+        return results
+    end
+
+
+    def Kpse.formatpaths
+        # maybe we should check for writeability
+        unless @@paths.key?('formatpaths') then
+            begin
+                setpath('formatpaths',run("--show-path=fmt").sane_path.split_path)
+            rescue
+                setpath('formatpaths',[])
+            end
+        end
+        return @@paths['formatpaths']
+    end
+
+    def Kpse.key(filename='',progname='all',format='all')
+        [progname,format,filename].join('-')
+    end
+
+    def Kpse.formatpath(engine='pdftex',enginepath=true)
+
+        # because engine support in distributions is not always
+        # as we expect, we need to check for it;
+
+        # todo: miktex
+
+        if miktex? then
+            return '.'
+        else
+            unless @@paths.key?(engine) then
+                # savedengine = ENV['engine']
+                if ENV['TEXFORMATS'] && ! ENV['TEXFORMATS'].empty? then
+                    # make sure that we have a lowercase entry
+                    ENV['TEXFORMATS'] = ENV['TEXFORMATS'].sub(/\$engine/io,"\$engine")
+                    # well, we will append anyway, so we could also strip it
+                    # ENV['TEXFORMATS'] = ENV['TEXFORMATS'].sub(/\$engine/io,"")
+                end
+                # use modern method
+                if enginepath then
+                    formatpath = run("--engine=#{engine} --show-path=fmt")
+                else
+                    # ENV['engine'] = engine if engine
+                    formatpath = run("--show-path=fmt")
+                end
+                # use ancient method
+                if formatpath.empty? then
+                    if enginepath then
+                        if @@mswindows then
+                            formatpath = run("--engine=#{engine} --expand-path=\$TEXFORMATS")
+                        else
+                            formatpath = run("--engine=#{engine} --expand-path=\\\$TEXFORMATS")
+                        end
+                    end
+                    # either no enginepath or failed run
+                    if formatpath.empty? then
+                        if @@mswindows then
+                            formatpath = run("--expand-path=\$TEXFORMATS")
+                        else
+                            formatpath = run("--expand-path=\\\$TEXFORMATS")
+                        end
+                    end
+                end
+                # locate writable path
+                if ! formatpath.empty? then
+                    formatpaths, done = formatpath.split_path, false
+                    formatpaths.collect! do |fp|
+                        fp.gsub!(/\\/o,'/')
+                        fp.gsub!(/\/\/$/o,'/')
+                        # remove funny patterns
+                        fp.sub!(/^!!/o,'')
+                        fp.sub!(/\/+$/o,'')
+                        fp.sub!(/(unsetengine|unset)/o,if enginepath then engine else '' end)
+                        fp
+                    end
+                    formatpaths.delete_if do |fp|
+                        fp.empty? || fp == '.'
+                    end
+                    # the engine path may not yet be present, find first writable
+                    formatpaths.each do |fp|
+                        # strip (possible engine) and test for writeability
+                        fpp = fp.sub(/#{engine}\/*$/o,'')
+                        if FileTest.directory?(fpp) && FileTest.writable?(fpp) then
+                            # use this path
+                            formatpath, done = fp.dup, true
+                            break
+                        end
+                    end
+                    unless done then
+                        formatpaths.each do |fp|
+                            fpp = fp.sub(/#{engine}\/*$/o,'')
+                            FileUtils.makedirs(fpp) rescue false # maybe we don't have an path yet
+                            if FileTest.directory?(fpp) && FileTest.writable?(fpp) then
+                                # use this path
+                                formatpath, done = fp.dup, true
+                                break
+                            end
+                        end
+                    end
+                    unless done then
+                        formatpath = '.'
+                    end
+                end
+                # needed !
+                FileUtils.makedirs(formatpath) rescue false
+                # fall back to current path
+                formatpath = '.' if formatpath.empty? || ! FileTest.writable?(formatpath)
+                # append engine but prevent duplicates
+                formatpath = File.join(formatpath.sub(/\/*#{engine}\/*$/,''), engine) if enginepath
+                FileUtils.makedirs(formatpath) rescue false
+                setpath(engine,formatpath)
+                # ENV['engine'] = savedengine
+            end
+            return @@paths[engine].first
+        end
+    end
+
+    def Kpse.update
+        system('initexmf -u') if Kpse.miktex?
+        system('mktexlsr')
+    end
+
+    # engine support is either broken of not implemented in some
+    # distributions, so we need to take care of it ourselves (without
+    # delays due to kpse calls); there can be many paths in the string
+    #
+    # in a year or so, i will drop this check
+
+    def Kpse.fixtexmfvars(engine=nil)
+        ENV['ENGINE'] = engine if engine
+        texformats = if ENV['TEXFORMATS'] then ENV['TEXFORMATS'].dup else '' end
+        if texformats.empty? then
+            if engine then
+                if @@mswindows then
+                    texformats = `kpsewhich --engine=#{engine} --expand-var=\$TEXFORMATS`.chomp
+                else
+                    texformats = `kpsewhich --engine=#{engine} --expand-var=\\\$TEXFORMATS`.chomp
+                end
+            else
+                if @@mswindows then
+                    texformats = `kpsewhich --expand-var=\$TEXFORMATS`.chomp
+                else
+                    texformats = `kpsewhich --expand-var=\\\$TEXFORMATS`.chomp
+                end
+            end
+        end
+        if engine then
+            texformats.sub!(/unsetengine/,engine)
+        else
+            texformats.sub!(/unsetengine/,"\$engine")
+        end
+        if engine && (texformats =~ /web2c[\/\\].*#{engine}/o) then
+            # ok, engine is seen
+            return false
+        elsif texformats =~ /web2c[\/\\].*\$engine/io then
+            # shouldn't happen
+            return false
+        else
+            ENV['TEXFORMATS'] = texformats.gsub(/(web2c\/\{)(,\})/o) do
+                "#{$1}\$engine#{$2}"
+            end
+            if texformats !~ /web2c[\/\\].*\$engine/io then
+                ENV['TEXFORMATS'] = texformats.gsub(/web2c\/*/, "web2c/{\$engine,}")
+            end
+            return true
+        end
+    end
+
+    def Kpse.runscript(name,filename=[],options=[])
+        setscript(name,`texmfstart --locate #{name}`) unless @@scripts.key?(name)
+        cmd = "#{@@scripts[name]} #{[options].flatten.join(' ')} #{[filename].flatten.join(' ')}"
+        system(cmd)
+    end
+
+    def Kpse.pipescript(name,filename=[],options=[])
+        setscript(name,`texmfstart --locate #{name}`) unless @@scripts.key?(name)
+        cmd = "#{@@scripts[name]} #{[options].flatten.join(' ')} #{[filename].flatten.join(' ')}"
+        `#{cmd}`
+    end
+
+    def Kpse.searchmethod
+        if @@usekpserunner then 'kpsefast' else 'kpsewhich' end
+    end
+
+    private
+
+    def Kpse.setvariable(key,value)
+        @@located[key] = value
+        ENV["_CTX_K_V_#{key}_"] = @@located[key] if @@crossover
+    end
+
+    def Kpse.setscript(key,value)
+        @@scripts[key] = value
+        ENV["_CTX_K_S_#{key}_"] = @@scripts[key] if @@crossover
+    end
+
+    def Kpse.setpath(key,value)
+        @@paths[key] = [value].flatten.uniq.collect do |p|
+            p.sub(/^!!/,'').sub(/\/*$/,'')
+        end
+        ENV["_CTX_K_P_#{key}_"] = @@paths[key].join(';') if @@crossover
+    end
+
+end
diff --git a/scripts/context/ruby/base/kpse/drb.rb b/scripts/context/ruby/base/kpse/drb.rb
new file mode 100644
index 000000000..db1ce0eec
--- /dev/null
+++ b/scripts/context/ruby/base/kpse/drb.rb
@@ -0,0 +1,57 @@
+require 'drb'
+require 'base/kpse/trees'
+
+class KpseServer
+
+    attr_accessor :port
+
+    def initialize(port=7000)
+        @port = port
+    end
+
+    def start
+        puts "starting drb service at port #{@port}"
+        DRb.start_service("druby://localhost:#{@port}", KpseTrees.new)
+        trap(:INT) do
+            DRb.stop_service
+        end
+        DRb.thread.join
+    end
+
+    def stop
+        # todo
+    end
+
+end
+
+class KpseClient
+
+    attr_accessor :port
+
+    def initialize(port=7000)
+        @port = port
+        @kpse = nil
+    end
+
+    def start
+        # only needed when callbacks are used / slow, due to Socket::getaddrinfo
+        # DRb.start_service
+    end
+
+    def object
+        @kpse = DRbObject.new(nil,"druby://localhost:#{@port}")
+    end
+
+end
+
+
+# SERVER_URI="druby://localhost:8787"
+#
+#   # Start a local DRbServer to handle callbacks.
+#   #
+#   # Not necessary for this small example, but will be required
+#   # as soon as we pass a non-marshallable object as an argument
+#   # to a dRuby call.
+#   DRb.start_service
+#
+#   timeserver = DRbObject.new_with_uri(SERVER_URI)
diff --git a/scripts/context/ruby/base/kpse/soap.rb b/scripts/context/ruby/base/kpse/soap.rb
new file mode 100644
index 000000000..c9ed75c44
--- /dev/null
+++ b/scripts/context/ruby/base/kpse/soap.rb
@@ -0,0 +1,79 @@
+require 'soap/rpc/standaloneServer'
+require 'soap/rpc/driver'
+
+require 'base/kpse/trees'
+
+class KpseService < SOAP::RPC::StandaloneServer
+
+    def on_init
+        kpse = KpseTrees.new
+        add_method(kpse, 'choose', 'files', 'environment')
+        add_method(kpse, 'load', 'files', 'environment')
+        add_method(kpse, 'expand_variables', 'tree')
+        add_method(kpse, 'expand_braces', 'tree', 'str')
+        add_method(kpse, 'expand_path', 'tree', 'str')
+        add_method(kpse, 'expand_var', 'tree', 'str')
+        add_method(kpse, 'show_path', 'tree', 'str')
+        add_method(kpse, 'var_value', 'tree', 'str')
+        add_method(kpse, 'find_file', 'tree', 'filename')
+        add_method(kpse, 'find_files', 'tree', 'filename', 'first')
+    end
+
+end
+
+class KpseServer
+
+    @@url = 'http://kpse.thismachine.org/KpseService'
+
+    attr_accessor :port
+
+    def initialize(port=7000)
+        @port = port
+        @server = nil
+    end
+
+    def start
+        puts "starting soap service at port #{@port}"
+        @server = KpseService.new('KpseServer', @@url, '0.0.0.0', @port.to_i)
+        trap(:INT) do
+            @server.shutdown
+        end
+        status = @server.start
+    end
+
+    def stop
+        @server.shutdown rescue false
+    end
+
+end
+
+class KpseClient
+
+    @@url = 'http://kpse.thismachine.org/KpseService'
+
+    attr_accessor :port
+
+    def initialize(port=7000)
+        @port = port
+        @kpse = nil
+    end
+
+    def start
+        @kpse = SOAP::RPC::Driver.new("http://localhost:#{port}/", @@url)
+        @kpse.add_method('choose','files', 'environment')
+        @kpse.add_method('load','files', 'environment')
+        @kpse.add_method('expand_variables', 'tree')
+        @kpse.add_method('expand_braces', 'tree', 'str')
+        @kpse.add_method('expand_path', 'tree', 'str')
+        @kpse.add_method('expand_var', 'tree', 'str')
+        @kpse.add_method('show_path', 'tree', 'str')
+        @kpse.add_method('var_value', 'tree', 'str')
+        @kpse.add_method('find_file', 'tree', 'filename')
+        @kpse.add_method('find_files', 'tree', 'filename', 'first')
+    end
+
+    def object
+        @kpse
+    end
+
+end
diff --git a/scripts/context/ruby/base/kpse/trees.rb b/scripts/context/ruby/base/kpse/trees.rb
new file mode 100644
index 000000000..9c872eb18
--- /dev/null
+++ b/scripts/context/ruby/base/kpse/trees.rb
@@ -0,0 +1,84 @@
+require 'monitor'
+require 'base/kpsefast'
+
+class KpseTrees < Monitor
+
+    def initialize
+        @trees = Hash.new
+    end
+
+    def pattern(filenames)
+        filenames.join('|').gsub(/\\+/o,'/').downcase
+    end
+
+    def choose(filenames,environment)
+        current = pattern(filenames)
+        load(filenames,environment) unless @trees[current]
+        puts "enabling tree #{current}"
+        current
+    end
+
+    def fetch(filenames,environment) # will send whole object !
+        current = pattern(filenames)
+        load(filenames,environment) unless @trees[current]
+        puts "fetching tree #{current}"
+        @trees[current]
+    end
+
+    def load(filenames,environment)
+        current = pattern(filenames)
+        puts "loading tree #{current}"
+        @trees[current] = KpseFast.new
+        @trees[current].push_environment(environment)
+        @trees[current].load_cnf(filenames)
+        @trees[current].expand_variables
+        @trees[current].load_lsr
+    end
+
+    def set(tree,key,value)
+        case key
+            when 'progname' then @trees[tree].progname = value
+            when 'engine'   then @trees[tree].engine   = value
+            when 'format'   then @trees[tree].format   = value
+        end
+    end
+    def get(tree,key)
+        case key
+            when 'progname' then @trees[tree].progname
+            when 'engine'   then @trees[tree].engine
+            when 'format'   then @trees[tree].format
+        end
+    end
+
+    def load_cnf(tree)
+        @trees[tree].load_cnf
+    end
+    def load_lsr(tree)
+        @trees[tree].load_lsr
+    end
+    def expand_variables(tree)
+        @trees[tree].expand_variables
+    end
+    def expand_braces(tree,str)
+        @trees[tree].expand_braces(str)
+    end
+    def expand_path(tree,str)
+        @trees[tree].expand_path(str)
+    end
+    def expand_var(tree,str)
+        @trees[tree].expand_var(str)
+    end
+    def show_path(tree,str)
+        @trees[tree].show_path(str)
+    end
+    def var_value(tree,str)
+        @trees[tree].var_value(str)
+    end
+    def find_file(tree,filename)
+        @trees[tree].find_file(filename)
+    end
+    def find_files(tree,filename,first)
+        @trees[tree].find_files(filename,first)
+    end
+
+end
diff --git a/scripts/context/ruby/base/kpsedirect.rb b/scripts/context/ruby/base/kpsedirect.rb
new file mode 100644
index 000000000..6fa8c8601
--- /dev/null
+++ b/scripts/context/ruby/base/kpsedirect.rb
@@ -0,0 +1,34 @@
+class KpseDirect
+
+    attr_accessor :progname, :format, :engine
+
+    def initialize
+        @progname, @format, @engine = '', '', ''
+    end
+
+    def expand_path(str)
+        clean_name(`kpsewhich -expand-path=#{str}`.chomp)
+    end
+
+    def expand_var(str)
+        clean_name(`kpsewhich -expand-var=#{str}`.chomp)
+    end
+
+    def find_file(str)
+        clean_name(`kpsewhich #{_progname_} #{_format_} #{str}`.chomp)
+    end
+
+    def _progname_
+        if @progname.empty? then '' else "-progname=#{@progname}" end
+    end
+    def _format_
+        if @format.empty?   then '' else "-format=\"#{@format}\"" end
+    end
+
+    private
+
+    def clean_name(str)
+        str.gsub(/\\/,'/')
+    end
+
+end
diff --git a/scripts/context/ruby/base/kpsefast.rb b/scripts/context/ruby/base/kpsefast.rb
new file mode 100644
index 000000000..8a9f89593
--- /dev/null
+++ b/scripts/context/ruby/base/kpsefast.rb
@@ -0,0 +1,934 @@
+# module    : base/kpsefast
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+# todo: multiple cnf files
+#
+# todo: cleanup, string or table store (as in lua variant)
+
+class String
+
+    def split_path
+        if self =~ /\;/o || self =~ /^[a-z]\:/io then
+            self.split(";")
+        else
+            self.split(":")
+        end
+    end
+
+end
+
+class Array
+
+    def join_path
+        self.join(File::PATH_SEPARATOR)
+    end
+
+end
+
+class File
+
+    def File.locate_file(path,name)
+        begin
+            files = Dir.entries(path)
+            if files.include?(name) then
+                fullname = File.join(path,name)
+                return fullname if FileTest.file?(fullname)
+            end
+            files.each do |p|
+                fullname = File.join(path,p)
+                if p != '.' and p != '..' and FileTest.directory?(fullname) and result = locate_file(fullname,name) then
+                    return result
+                end
+            end
+        rescue
+            # bad path
+        end
+        return nil
+    end
+
+    def File.glob_file(pattern)
+        return Dir.glob(pattern).first
+    end
+
+end
+
+module KpseUtil
+
+    # to be adapted, see loading cnf file
+
+    @@texmftrees = ['texmf-local','texmf.local','../..','texmf'] # '../..' is for gwtex
+    @@texmfcnf   = 'texmf.cnf'
+
+    def KpseUtil::identify
+        # we mainly need to identify the local tex stuff and wse assume that
+        # the texmfcnf variable is set; otherwise we need to expand the
+        # TEXMF variable and that takes time since it may involve more
+        ownpath = File.expand_path($0)
+        if ownpath.gsub!(/texmf.*?$/o, '') then
+            ENV['SELFAUTOPARENT'] = ownpath
+        else
+            ENV['SELFAUTOPARENT'] = '.' # fall back
+            # may be too tricky:
+            #
+            # (ENV['PATH'] ||'').split_path.each do |p|
+                # if p.gsub!(/texmf.*?$/o, '') then
+                    # ENV['SELFAUTOPARENT'] = p
+                    # break
+                # end
+            # end
+        end
+        filenames = Array.new
+        if ENV['TEXMFCNF'] && ! ENV['TEXMFCNF'].empty? then
+            ENV['TEXMFCNF'].to_s.split_path.each do |path|
+                filenames << File.join(path,@@texmfcnf)
+            end
+        elsif ENV['SELFAUTOPARENT'] == '.' then
+            filenames << File.join('.',@@texmfcnf)
+        else
+            @@texmftrees.each do |tree|
+                filenames << File.join(ENV['SELFAUTOPARENT'],tree,'web2c',@@texmfcnf)
+            end
+        end
+        loop do
+            busy = false
+            filenames.collect! do |f|
+                f.gsub(/\$([a-zA-Z0-9\_\-]+)/o) do
+                    if (! ENV[$1]) || (ENV[$1] == $1) then
+                        "$#{$1}"
+                    else
+                        busy = true
+                        ENV[$1]
+                    end
+                end
+            end
+            break unless busy
+        end
+        filenames.delete_if do |f|
+            ! FileTest.file?(f)
+        end
+        return filenames
+    end
+
+    def KpseUtil::environment
+        Hash.new.merge(ENV)
+    end
+
+end
+
+class KpseFast
+
+    # formats are an incredible inconsistent mess
+
+    @@suffixes  = Hash.new
+    @@formats   = Hash.new
+    @@suffixmap = Hash.new
+
+    @@texmfcnf  = 'texmf.cnf'
+
+    @@suffixes['gf']                       = ['.<resolution>gf'] # todo
+    @@suffixes['pk']                       = ['.<resolution>pk'] # todo
+    @@suffixes['tfm']                      = ['.tfm']
+    @@suffixes['afm']                      = ['.afm']
+    @@suffixes['base']                     = ['.base']
+    @@suffixes['bib']                      = ['.bib']
+    @@suffixes['bst']                      = ['.bst']
+    @@suffixes['cnf']                      = ['.cnf']
+    @@suffixes['ls-R']                     = ['ls-R', 'ls-r']
+    @@suffixes['fmt']                      = ['.fmt', '.efmt', '.efm', '.ofmt', '.ofm', '.oft', '.eofmt', '.eoft', '.eof', '.pfmt', '.pfm', '.epfmt', '.epf', '.xpfmt', '.xpf', '.afmt', '.afm']
+    @@suffixes['map']                      = ['.map']
+    @@suffixes['mem']                      = ['.mem']
+    @@suffixes['mf']                       = ['.mf']
+    @@suffixes['mfpool']                   = ['.pool']
+    @@suffixes['mft']                      = ['.mft']
+    @@suffixes['mp']                       = ['.mp']
+    @@suffixes['mppool']                   = ['.pool']
+    @@suffixes['ocp']                      = ['.ocp']
+    @@suffixes['ofm']                      = ['.ofm', '.tfm']
+    @@suffixes['opl']                      = ['.opl']
+    @@suffixes['otp']                      = ['.otp']
+    @@suffixes['ovf']                      = ['.ovf']
+    @@suffixes['ovp']                      = ['.ovp']
+    @@suffixes['graphic/figure']           = ['.eps', '.epsi']
+    @@suffixes['tex']                      = ['.tex']
+    @@suffixes['texpool']                  = ['.pool']
+    @@suffixes['PostScript header']        = ['.pro']
+    @@suffixes['type1 fonts']              = ['.pfa', '.pfb']
+    @@suffixes['vf']                       = ['.vf']
+    @@suffixes['ist']                      = ['.ist']
+    @@suffixes['truetype fonts']           = ['.ttf', '.ttc']
+    @@suffixes['web']                      = ['.web', '.ch']
+    @@suffixes['cweb']                     = ['.w', '.web', '.ch']
+    @@suffixes['enc files']                = ['.enc']
+    @@suffixes['cmap files']               = ['.cmap']
+    @@suffixes['subfont definition files'] = ['.sfd']
+    @@suffixes['lig files']                = ['.lig']
+    @@suffixes['bitmap font']              = []
+    @@suffixes['MetaPost support']         = []
+    @@suffixes['TeX system documentation'] = []
+    @@suffixes['TeX system sources']       = []
+    @@suffixes['Troff fonts']              = []
+    @@suffixes['dvips config']             = []
+    @@suffixes['type42 fonts']             = []
+    @@suffixes['web2c files']              = []
+    @@suffixes['other text files']         = []
+    @@suffixes['other binary files']       = []
+    @@suffixes['misc fonts']               = []
+    @@suffixes['opentype fonts']           = []
+    @@suffixes['pdftex config']            = []
+    @@suffixes['texmfscripts']             = []
+
+    # replacements
+
+    @@suffixes['fmt']                      = ['.fmt']
+    @@suffixes['type1 fonts']              = ['.pfa', '.pfb', '.pfm']
+    @@suffixes['tex']                      = ['.tex', '.xml']
+    @@suffixes['texmfscripts']             = ['rb','lua','py','pl']
+
+    @@suffixes.keys.each do |k| @@suffixes[k].each do |s| @@suffixmap[s] = k end end
+
+    # TTF2TFMINPUTS
+    # MISCFONTS
+    # TEXCONFIG
+    # DVIPDFMINPUTS
+    # OTFFONTS
+
+    @@formats['gf']                       = ''
+    @@formats['pk']                       = ''
+    @@formats['tfm']                      = 'TFMFONTS'
+    @@formats['afm']                      = 'AFMFONTS'
+    @@formats['base']                     = 'MFBASES'
+    @@formats['bib']                      = ''
+    @@formats['bst']                      = ''
+    @@formats['cnf']                      = ''
+    @@formats['ls-R']                     = ''
+    @@formats['fmt']                      = 'TEXFORMATS'
+    @@formats['map']                      = 'TEXFONTMAPS'
+    @@formats['mem']                      = 'MPMEMS'
+    @@formats['mf']                       = 'MFINPUTS'
+    @@formats['mfpool']                   = 'MFPOOL'
+    @@formats['mft']                      = ''
+    @@formats['mp']                       = 'MPINPUTS'
+    @@formats['mppool']                   = 'MPPOOL'
+    @@formats['ocp']                      = 'OCPINPUTS'
+    @@formats['ofm']                      = 'OFMFONTS'
+    @@formats['opl']                      = 'OPLFONTS'
+    @@formats['otp']                      = 'OTPINPUTS'
+    @@formats['ovf']                      = 'OVFFONTS'
+    @@formats['ovp']                      = 'OVPFONTS'
+    @@formats['graphic/figure']           = ''
+    @@formats['tex']                      = 'TEXINPUTS'
+    @@formats['texpool']                  = 'TEXPOOL'
+    @@formats['PostScript header']        = 'TEXPSHEADERS'
+    @@formats['type1 fonts']              = 'T1FONTS'
+    @@formats['vf']                       = 'VFFONTS'
+    @@formats['ist']                      = ''
+    @@formats['truetype fonts']           = 'TTFONTS'
+    @@formats['web']                      = ''
+    @@formats['cweb']                     = ''
+    @@formats['enc files']                = 'ENCFONTS'
+    @@formats['cmap files']               = 'CMAPFONTS'
+    @@formats['subfont definition files'] = 'SFDFONTS'
+    @@formats['lig files']                = 'LIGFONTS'
+    @@formats['bitmap font']              = ''
+    @@formats['MetaPost support']         = ''
+    @@formats['TeX system documentation'] = ''
+    @@formats['TeX system sources']       = ''
+    @@formats['Troff fonts']              = ''
+    @@formats['dvips config']             = ''
+    @@formats['type42 fonts']             = 'T42FONTS'
+    @@formats['web2c files']              = 'WEB2C'
+    @@formats['other text files']         = ''
+    @@formats['other binary files']       = ''
+    @@formats['misc fonts']               = ''
+    @@formats['opentype fonts']           = 'OPENTYPEFONTS'
+    @@formats['pdftex config']            = 'PDFTEXCONFIG'
+    @@formats['texmfscripts']             = 'TEXMFSCRIPTS'
+
+    attr_accessor :progname, :engine, :format, :rootpath, :treepath,
+        :verbose, :remember, :scandisk, :diskcache, :renewcache
+
+    @@cacheversion = '1'
+
+    def initialize
+        @rootpath    = ''
+        @treepath    = ''
+        @progname    = 'kpsewhich'
+        @engine      = 'pdftex'
+        @variables   = Hash.new
+        @expansions  = Hash.new
+        @files       = Hash.new
+        @found       = Hash.new
+        @kpsevars    = Hash.new
+        @lsrfiles    = Array.new
+        @cnffiles    = Array.new
+        @verbose     = true
+        @remember    = true
+        @scandisk    = true
+        @diskcache   = true
+        @renewcache  = false
+        @isolate     = false
+
+        @diskcache   = false
+        @cachepath   = nil
+        @cachefile   = 'tmftools.log'
+
+        @environment = ENV
+    end
+
+    def set(key,value)
+        case key
+            when 'progname' then @progname = value
+            when 'engine'   then @engine   = value
+            when 'format'   then @format   = value
+        end
+    end
+
+    def push_environment(env)
+        @environment = env
+    end
+
+    # {$SELFAUTOLOC,$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,}/web2c}
+    #
+    # $SELFAUTOLOC    : /usr/tex/bin/platform
+    # $SELFAUTODIR    : /usr/tex/bin
+    # $SELFAUTOPARENT : /usr/tex
+    #
+    # since we live in scriptpath we need a slightly different method
+
+    def load_cnf(filenames=nil)
+        unless filenames then
+            ownpath = File.expand_path($0)
+            if ownpath.gsub!(/texmf.*?$/o, '') then
+                @environment['SELFAUTOPARENT'] = ownpath
+            else
+                @environment['SELFAUTOPARENT'] = '.'
+            end
+            unless @treepath.empty? then
+                unless @rootpath.empty? then
+                    @treepath = @treepath.split(',').collect do |p| File.join(@rootpath,p) end.join(',')
+                end
+                @environment['TEXMF'] = @treepath
+                # only the first one
+                @environment['TEXMFCNF'] = File.join(@treepath.split(',').first,'texmf/web2c')
+            end
+            unless @rootpath.empty? then
+                @environment['TEXMFCNF'] = File.join(@rootpath,'texmf/web2c')
+                @environment['SELFAUTOPARENT'] = @rootpath
+                @isolate = true
+            end
+            filenames = Array.new
+            if @environment['TEXMFCNF'] and not @environment['TEXMFCNF'].empty? then
+                @environment['TEXMFCNF'].to_s.split_path.each do |path|
+                    filenames << File.join(path,@@texmfcnf)
+                end
+            elsif @environment['SELFAUTOPARENT'] == '.' then
+                filenames << File.join('.',@@texmfcnf)
+            else
+                ['texmf-local','texmf'].each do |tree|
+                    filenames << File.join(@environment['SELFAUTOPARENT'],tree,'web2c',@@texmfcnf)
+                end
+            end
+        end
+        # <root>/texmf/web2c/texmf.cnf
+        filenames = _expanded_path_(filenames)
+        @rootpath = filenames.first
+        3.times do
+            @rootpath = File.dirname(@rootpath)
+        end
+        filenames.collect! do |f|
+            f.gsub("\\", '/')
+        end
+        filenames.each do |fname|
+            if FileTest.file?(fname) and f = File.open(fname) then
+                @cnffiles << fname
+                while line = f.gets do
+                    loop do
+                        # concatenate lines ending with \
+                        break unless line.sub!(/\\\s*$/o) do
+                            f.gets || ''
+                        end
+                    end
+                    case line
+                        when /^[\%\#]/o then
+                            # comment
+                        when /^\s*(.*?)\s*\=\s*(.*?)\s*$/o then
+                            key, value = $1, $2
+                            unless @variables.key?(key) then
+                                value.sub!(/\%.*$/,'')
+                                value.sub!(/\~/, "$HOME")
+                                @variables[key] = value
+                            end
+                            @kpsevars[key] = true
+                    end
+                end
+                f.close
+            end
+        end
+    end
+
+    def load_lsr
+        @lsrfiles = []
+        simplified_list(expansion('TEXMF')).each do |p|
+            ['ls-R','ls-r'].each do |f|
+                filename = File.join(p,f)
+                if FileTest.file?(filename) then
+                    @lsrfiles << [filename,File.size(filename)]
+                    break
+                end
+            end
+        end
+        @files = Hash.new
+        if @diskcache then
+            ['HOME','TEMP','TMP','TMPDIR'].each do |key|
+                if @environment[key] then
+                    if FileTest.directory?(@environment[key]) then
+                        @cachepath = @environment[key]
+                        @cachefile = [@rootpath.gsub(/[^A-Z0-9]/io, '-').gsub(/\-+/,'-'),File.basename(@cachefile)].join('-')
+                        break
+                    end
+                end
+            end
+            if @cachepath and not @renewcache and FileTest.file?(File.join(@cachepath,@cachefile)) then
+                begin
+                    if f = File.open(File.join(@cachepath,@cachefile)) then
+                        cacheversion = Marshal.load(f)
+                        if cacheversion == @@cacheversion then
+                            lsrfiles = Marshal.load(f)
+                            if lsrfiles == @lsrfiles then
+                                @files = Marshal.load(f)
+                            end
+                        end
+                        f.close
+                    end
+                rescue
+                   @files = Hash.new
+               end
+            end
+        end
+        return if @files.size > 0
+        @lsrfiles.each do |filedata|
+            filename, filesize = filedata
+            filepath = File.dirname(filename)
+            begin
+                path = '.'
+                data = IO.readlines(filename)
+                if data[0].chomp =~ /% ls\-R \-\- filename database for kpathsea\; do not change this line\./io then
+                    data.each do |line|
+                        case line
+                            when /^[a-zA-Z0-9]/o then
+                                line.chomp!
+                                if @files[line] then
+                                    @files[line] << path
+                                else
+                                    @files[line] = [path]
+                                end
+                            when /^\.\/(.*?)\:$/o then
+                                path = File.join(filepath,$1)
+                        end
+                    end
+                end
+            rescue
+                # sorry
+            end
+        end
+        if @diskcache and @cachepath and f = File.open(File.join(@cachepath,@cachefile),'wb') then
+            f << Marshal.dump(@@cacheversion)
+            f << Marshal.dump(@lsrfiles)
+            f << Marshal.dump(@files)
+            f.close
+        end
+    end
+
+    def expand_variables
+        @expansions = Hash.new
+        if @isolate then
+            @variables['TEXMFCNF'] = @environment['TEXMFCNF'].dup
+            @variables['SELFAUTOPARENT'] = @environment['SELFAUTOPARENT'].dup
+        else
+            @environment.keys.each do |e|
+                if e =~ /^([a-zA-Z]+)\_(.*)\s*$/o then
+                    @expansions["#{$1}.#{$2}"] = (@environment[e] ||'').dup
+                else
+                    @expansions[e] = (@environment[e] ||'').dup
+                end
+            end
+        end
+        @variables.keys.each do |k|
+            @expansions[k] = @variables[k].dup unless @expansions[k]
+        end
+        loop do
+            busy = false
+            @expansions.keys.each do |k|
+                @expansions[k].gsub!(/\$([a-zA-Z0-9\_\-]*)/o) do
+                    busy = true
+                    @expansions[$1] || ''
+                end
+                @expansions[k].gsub!(/\$\{([a-zA-Z0-9\_\-]*)\}/o) do
+                    busy = true
+                    @expansions[$1] || ''
+                end
+            end
+            break unless busy
+        end
+        @expansions.keys.each do |k|
+            @expansions[k] = @expansions[k].gsub("\\", '/')
+        end
+    end
+
+    def variable(name='')
+        (name and not name.empty? and @variables[name.sub('$','')]) or  ''
+    end
+
+    def expansion(name='')
+        (name and not name.empty? and @expansions[name.sub('$','')]) or ''
+    end
+
+    def variable?(name='')
+        name and not name.empty? and @variables.key?(name.sub('$',''))
+    end
+
+    def expansion?(name='')
+        name and not name.empty? and @expansions.key?(name.sub('$',''))
+    end
+
+    def simplified_list(str)
+        lst = str.gsub(/^\{/o,'').gsub(/\}$/o,'').split(",")
+        lst.collect do |l|
+            l.sub(/^[\!]*/,'').sub(/[\/\\]*$/o,'')
+        end
+    end
+
+    def original_variable(variable)
+        if variable?("#{@progname}.#{variable}") then
+            variable("#{@progname}.#{variable}")
+        elsif variable?(variable) then
+            variable(variable)
+        else
+            ''
+        end
+    end
+
+    def expanded_variable(variable)
+        if expansion?("#{variable}.#{@progname}") then
+            expansion("#{variable}.#{@progname}")
+        elsif expansion?(variable) then
+            expansion(variable)
+        else
+            ''
+        end
+    end
+
+    def original_path(filename='')
+        _expanded_path_(original_variable(var_of_format_or_suffix(filename)).split(";"))
+    end
+
+    def expanded_path(filename='')
+        _expanded_path_(expanded_variable(var_of_format_or_suffix(filename)).split(";"))
+    end
+
+    def _expanded_path_(pathlist)
+        i, n = 0, 0
+        pathlist.collect! do |mainpath|
+            mainpath.gsub(/([\{\}])/o) do
+                if $1 == "{" then
+                    i += 1 ; n = i if i > n ; "<#{i}>"
+                else
+                    i -= 1 ; "</#{i+1}>"
+                end
+            end
+        end
+        n.times do |i|
+            loop do
+                more = false
+                newlist = []
+                pathlist.each do |path|
+                    unless path.sub!(/^(.*?)<(#{n-i})>(.*?)<\/\2>(.*?)$/) do
+                        pre, mid, post = $1, $3, $4
+                        mid.gsub!(/\,$/,',.')
+                        mid.split(',').each do |m|
+                            more = true
+                            if m == '.' then
+                                newlist << "#{pre}#{post}"
+                            else
+                                newlist << "#{pre}#{m}#{post}"
+                            end
+                        end
+                    end then
+                        newlist << path
+                    end
+                end
+                if more then
+                    pathlist = [newlist].flatten # copy -)
+                else
+                    break
+                end
+            end
+        end
+        pathlist = pathlist.uniq.collect do |path|
+            p = path
+            # p.gsub(/^\/+/o) do '' end
+            # p.gsub!(/(.)\/\/(.)/o) do "#{$1}/#{$2}" end
+            # p.gsub!(/\/\/+$/o) do '//' end
+            p.gsub!(/\/\/+/o) do '//' end
+            p
+        end
+        pathlist
+    end
+
+    # todo: ignore case
+
+    def var_of_format(str)
+        @@formats[str] || ''
+    end
+
+    def var_of_suffix(str) # includes .
+        if @@suffixmap.key?(str) then @@formats[@@suffixmap[str]] else '' end
+    end
+
+    def var_of_format_or_suffix(str)
+        if @@formats.key?(str) then
+            @@formats[str]
+        elsif @@suffixmap.key?(File.extname(str)) then # extname includes .
+            @@formats[@@suffixmap[File.extname(str)]]  # extname includes .
+        else
+            ''
+        end
+    end
+
+end
+
+class KpseFast
+
+    # test things
+
+    def list_variables(kpseonly=true)
+        @variables.keys.sort.each do |k|
+            if kpseonly then
+                puts("#{k} = #{@variables[k]}") if @kpsevars[k]
+            else
+                puts("#{if @kpsevars[k] then 'K' else 'E' end} #{k} = #{@variables[k]}")
+            end
+        end
+    end
+
+    def list_expansions(kpseonly=true)
+        @expansions.keys.sort.each do |k|
+            if kpseonly then
+                puts("#{k} = #{@expansions[k]}") if @kpsevars[k]
+            else
+                puts("#{if @kpsevars[k] then 'K' else 'E' end} #{k} = #{@expansions[k]}")
+            end
+        end
+    end
+
+    def list_lsr
+        puts("files = #{@files.size}")
+    end
+
+    def set_test_patterns
+        @variables["KPSE_TEST_PATTERN_A"] = "foo/{1,2}/bar//"
+        @variables["KPSE_TEST_PATTERN_B"] = "!!x{A,B{1,2}}y"
+        @variables["KPSE_TEST_PATTERN_C"] = "x{A,B//{1,2}}y"
+        @variables["KPSE_TEST_PATTERN_D"] = "x{A,B//{1,2,}}//y"
+    end
+
+    def show_test_patterns
+        ['A','B','D'].each do |i|
+            puts ""
+            puts @variables ["KPSE_TEST_PATTERN_#{i}"]
+            puts ""
+            puts expand_path("KPSE_TEST_PATTERN_#{i}").split_path
+            puts ""
+        end
+    end
+
+end
+
+class KpseFast
+
+    # kpse stuff
+
+    def expand_braces(str) # output variable and brace expansion of STRING.
+        _expanded_path_(original_variable(str).split_path).join_path
+    end
+
+    def expand_path(str)   # output complete path expansion of STRING.
+        _expanded_path_(expanded_variable(str).split_path).join_path
+    end
+
+    def expand_var(str)    # output variable expansion of STRING.
+        expanded_variable(str)
+    end
+
+    def show_path(str)     # output search path for file type NAME
+        expanded_path(str).join_path
+    end
+
+    def var_value(str)     # output the value of variable $STRING.
+        original_variable(str)
+    end
+
+end
+
+class KpseFast
+
+    def _is_cnf_?(filename)
+        filename == File.basename((@cnffiles.first rescue @@texmfcnf) || @@texmfcnf)
+    end
+
+    def find_file(filename)
+        if _is_cnf_?(filename) then
+            @cnffiles.first rescue ''
+        else
+            [find_files(filename,true)].flatten.first || ''
+        end
+    end
+
+    def find_files(filename,first=false)
+        if _is_cnf_?(filename) then
+            result = @cnffiles.dup
+        else
+            if @remember then
+                # stamp = "#{filename}--#{@format}--#{@engine}--#{@progname}"
+                stamp = "#{filename}--#{@engine}--#{@progname}"
+                return @found[stamp] if @found.key?(stamp)
+            end
+            pathlist = expanded_path(filename)
+            result = []
+            filelist = if @files.key?(filename) then @files[filename].uniq else nil end
+            done = false
+            if pathlist.size == 0 then
+                if FileTest.file?(filename) then
+                    done = true
+                    result << '.'
+                end
+            else
+                pathlist.each do |path|
+                    doscan = if path =~ /^\!\!/o then false else true end
+                    recurse = if path =~ /\/\/$/o then true else false end
+                    pathname = path.dup
+                    pathname.gsub!(/^\!+/o, '')
+                    done = false
+                    if not done and filelist then
+                        # checking for exact match
+                        if filelist.include?(pathname) then
+                            result << pathname
+                            done = true
+                        end
+                        if not done and recurse then
+                            # checking for fuzzy //
+                            pathname.gsub!(/\/+$/o, '/.*')
+                            # pathname.gsub!(/\/\//o,'/[\/]*/')
+                            pathname.gsub!(/\/\//o,'/.*?/')
+                            re = /^#{pathname}/
+                            filelist.each do |f|
+                                if re =~ f then
+                                    result << f # duplicates will be filtered later
+                                    done = true
+                                end
+                                break if done
+                            end
+                        end
+                    end
+                    if not done and doscan then
+                        # checking for path itself
+                        pname = pathname.sub(/\.\*$/,'')
+                        if not pname =~ /\*/o and FileTest.file?(File.join(pname,filename)) then
+                            result << pname
+                            done = true
+                        end
+                    end
+                    break if done and first
+                end
+            end
+            if not done and @scandisk then
+                pathlist.each do |path|
+                    pathname = path.dup
+                    unless pathname.gsub!(/^\!+/o, '') then # !! prevents scan
+                        recurse = pathname.gsub!(/\/+$/o, '')
+                        complex = pathname.gsub!(/\/\//o,'/*/')
+                        if recurse then
+                            if complex then
+                                if ok = File.glob_file("#{pathname}/**/#{filename}") then
+                                    result << File.dirname(ok)
+                                    done = true
+                                end
+                            elsif ok = File.locate_file(pathname,filename) then
+                                result << File.dirname(ok)
+                                done = true
+                            end
+                        elsif complex then
+                            if ok = File.glob_file("#{pathname}/#{filename}") then
+                                result << File.dirname(ok)
+                                done = true
+                            end
+                        elsif FileTest.file?(File.join(pathname,filename)) then
+                            result << pathname
+                            done = true
+                        end
+                        break if done and first
+                    end
+                end
+            end
+            result = result.uniq.collect do |pathname|
+                File.join(pathname,filename)
+            end
+            @found[stamp] = result if @remember
+        end
+        return result # redundant
+    end
+
+end
+
+class KpseFast
+
+    class FileData
+        attr_accessor :tag, :name, :size, :date
+        def initialize(tag=0,name=nil,size=nil,date=nil)
+            @tag, @name, @size, @date = tag, name, size, date
+        end
+        def FileData.sizes(a)
+            a.collect do |aa|
+                aa.size
+            end
+        end
+        def report
+            case @tag
+                when 1 then "deleted  | #{@size.to_s.rjust(8)} | #{@date.strftime('%m/%d/%Y %I:%M')} | #{@name}"
+                when 2 then "present  | #{@size.to_s.rjust(8)} | #{@date.strftime('%m/%d/%Y %I:%M')} | #{@name}"
+                when 3 then "obsolete | #{' '*8} | #{' '*16} | #{@name}"
+            end
+        end
+    end
+
+    def analyze_files(filter='',strict=false,sort='',delete=false)
+        puts("command line     = #{ARGV.join(' ')}")
+        puts("number of files  = #{@files.size}")
+        puts("filter pattern   = #{filter}")
+        puts("loaded cnf files = #{@cnffiles.join(' ')}")
+        puts('')
+        if filter.gsub!(/^not:/,'') then
+            def the_same(filter,filename)
+                not filter or filter.empty? or /#{filter}/ !~ filename
+            end
+        else
+            def the_same(filter,filename)
+                not filter or filter.empty? or /#{filter}/ =~ filename
+            end
+        end
+        @files.keys.each do |name|
+            if @files[name].size > 1 then
+                data = Array.new
+                @files[name].each do |path|
+                    filename = File.join(path,name)
+                    # if not filter or filter.empty? or /#{filter}/ =~ filename then
+                    if the_same(filter,filename) then
+                        if FileTest.file?(filename) then
+                            if delete then
+                                data << FileData.new(1,filename,File.size(filename),File.mtime(filename))
+                                begin
+                                    File.delete(filename) if delete
+                                rescue
+                                end
+                            else
+                                data << FileData.new(2,filename,File.size(filename),File.mtime(filename))
+                            end
+                        else
+                            # data << FileData.new(3,filename)
+                        end
+                    end
+                end
+                if data.length > 1 then
+                    if strict then
+                        # if data.collect do |d| d.size end.uniq! then
+                            # data.sort! do |a,b| b.size <=> a.size end
+                            # data.each do |d| puts d.report end
+                            # puts ''
+                        # end
+                        data.sort! do |a,b|
+                            if a.size and b.size then
+                                b.size <=> a.size
+                            else
+                                0
+                            end
+                        end
+                        bunch = Array.new
+                        done = false
+                        data.each do |d|
+                            if bunch.size == 0 then
+                                bunch << d
+                            elsif bunch[0].size == d.size then
+                                bunch << d
+                            else
+                                if bunch.size > 1 then
+                                    bunch.each do |b|
+                                        puts b.report
+                                    end
+                                    done = true
+                                end
+                                bunch = [d]
+                            end
+                        end
+                        puts '' if done
+                    else
+                        case sort
+                            when 'size'    then data.sort! do |a,b| a.size <=> b.size end
+                            when 'revsize' then data.sort! do |a,b| b.size <=> a.size end
+                            when 'date'    then data.sort! do |a,b| a.date <=> b.date end
+                            when 'revdate' then data.sort! do |a,b| b.date <=> a.date end
+                        end
+                        data.each do |d| puts d.report end
+                        puts ''
+                    end
+                end
+            end
+        end
+    end
+
+end
+
+# if false then
+
+    # k = KpseFast.new # (root)
+    # k.set_test_patterns
+    # k.load_cnf
+    # k.expand_variables
+    # k.load_lsr
+
+    # k.show_test_patterns
+
+    # puts k.list_variables
+    # puts k.list_expansions
+    # k.list_lsr
+    # puts k.expansion("$TEXMF")
+    # puts k.expanded_path("TEXINPUTS","context")
+
+    # k.progname, k.engine, k.format = 'context', 'pdftex', 'tfm'
+    # k.scandisk = false # == must_exist
+    # k.expand_variables
+
+    # 10.times do |i| puts k.find_file('texnansi-lmr10.tfm') end
+
+    # puts "expand braces $TEXMF"
+    # puts k.expand_braces("$TEXMF")
+    # puts "expand path $TEXMF"
+    # puts k.expand_path("$TEXMF")
+    # puts "expand var $TEXMF"
+    # puts k.expand_var("$TEXMF")
+    # puts "expand path $TEXMF"
+    # puts k.show_path('tfm')
+    # puts "expand value $TEXINPUTS"
+    # puts k.var_value("$TEXINPUTS")
+    # puts "expand value $TEXINPUTS.context"
+    # puts k.var_value("$TEXINPUTS.context")
+
+    # exit
+
+# end
diff --git a/scripts/context/ruby/base/kpseremote.rb b/scripts/context/ruby/base/kpseremote.rb
new file mode 100644
index 000000000..9a73b88b0
--- /dev/null
+++ b/scripts/context/ruby/base/kpseremote.rb
@@ -0,0 +1,116 @@
+require 'base/kpsefast'
+
+case ENV['KPSEMETHOD']
+    when /soap/o then require 'base/kpse/soap'
+    when /drb/o  then require 'base/kpse/drb'
+    else              require 'base/kpse/drb'
+end
+
+class KpseRemote
+
+    @@port   = ENV['KPSEPORT']   || 7000
+    @@method = ENV['KPSEMETHOD'] || 'drb'
+
+    def KpseRemote::available?
+        @@method && @@port
+    end
+
+    def KpseRemote::start_server(port=nil)
+        kpse = KpseServer.new(port || @@port)
+        kpse.start
+    end
+
+    def KpseRemote::start_client(port=nil) # keeps object in server
+        kpseclient = KpseClient.new(port || @@port)
+        kpseclient.start
+        kpse = kpseclient.object
+        tree = kpse.choose(KpseUtil::identify, KpseUtil::environment)
+        [kpse, tree]
+    end
+
+    def KpseRemote::fetch(port=nil) # no need for defining methods but slower, send whole object
+        kpseclient = KpseClient.new(port || @@port)
+        kpseclient.start
+        kpseclient.object.fetch(KpseUtil::identify, KpseUtil::environment) rescue nil
+    end
+
+    def initialize(port=nil)
+        if KpseRemote::available? then
+            begin
+                @kpse, @tree = KpseRemote::start_client(port)
+            rescue
+                @kpse, @tree = nil, nil
+            end
+        else
+            @kpse, @tree = nil, nil
+        end
+    end
+
+    def progname=(value)
+        @kpse.set(@tree,'progname',value)
+    end
+    def format=(value)
+        @kpse.set(@tree,'format',value)
+    end
+    def engine=(value)
+        @kpse.set(@tree,'engine',value)
+    end
+
+    def progname
+        @kpse.get(@tree,'progname')
+    end
+    def format
+        @kpse.get(@tree,'format')
+    end
+    def engine
+        @kpse.get(@tree,'engine')
+    end
+
+    def load
+        @kpse.load(KpseUtil::identify, KpseUtil::environment)
+    end
+    def okay?
+        @kpse && @tree
+    end
+    def set(key,value)
+        @kpse.set(@tree,key,value)
+    end
+    def load_cnf
+        @kpse.load_cnf(@tree)
+    end
+    def load_lsr
+        @kpse.load_lsr(@tree)
+    end
+    def expand_variables
+        @kpse.expand_variables(@tree)
+    end
+    def expand_braces(str)
+        clean_name(@kpse.expand_braces(@tree,str))
+    end
+    def expand_path(str)
+        clean_name(@kpse.expand_path(@tree,str))
+    end
+    def expand_var(str)
+        clean_name(@kpse.expand_var(@tree,str))
+    end
+    def show_path(str)
+        clean_name(@kpse.show_path(@tree,str))
+    end
+    def var_value(str)
+        clean_name(@kpse.var_value(@tree,str))
+    end
+    def find_file(filename)
+        clean_name(@kpse.find_file(@tree,filename))
+    end
+    def find_files(filename,first=false)
+        # dodo: each filename
+        @kpse.find_files(@tree,filename,first)
+    end
+
+    private
+
+    def clean_name(str)
+        str.gsub(/\\/,'/')
+    end
+
+end
diff --git a/scripts/context/ruby/base/kpserunner.rb b/scripts/context/ruby/base/kpserunner.rb
new file mode 100644
index 000000000..cfc2ad4fb
--- /dev/null
+++ b/scripts/context/ruby/base/kpserunner.rb
@@ -0,0 +1,87 @@
+require 'base/kpsefast'
+
+module KpseRunner
+
+    @@kpse = nil
+
+    def KpseRunner.kpsewhich(arg='')
+        options, arguments = split_args(arg)
+        unless @@kpse then
+            if ENV['KPSEMETHOD'] && ENV['KPSEPORT'] then
+                require 'base/kpseremote'
+                @@kpse = KpseRemote.new
+            else
+                @@kpse = nil
+            end
+            if @@kpse && @@kpse.okay? then
+                @@kpse.progname = options['progname'] || ''
+                @@kpse.engine   = options['engine']   || ''
+                @@kpse.format   = options['format']   || ''
+            else
+                require 'base/kpsefast'
+                @@kpse = KpseFast.new
+                @@kpse.load_cnf
+                @@kpse.progname = options['progname'] || ''
+                @@kpse.engine   = options['engine']   || ''
+                @@kpse.format   = options['format']   || ''
+                @@kpse.expand_variables
+                @@kpse.load_lsr
+            end
+        else
+            @@kpse.progname = options['progname'] || ''
+            @@kpse.engine   = options['engine']   || ''
+            @@kpse.format   = options['format']   || ''
+            @@kpse.expand_variables
+        end
+        if    option = options['expand-braces'] and not option.empty? then
+            @@kpse.expand_braces(option)
+        elsif option = options['expand-path']   and not option.empty? then
+            @@kpse.expand_path(option)
+        elsif option = options['expand-var']    and not option.empty? then
+            @@kpse.expand_var(option)
+        elsif option = options['show-path']     and not option.empty? then
+            @@kpse.show_path(option)
+        elsif option = options['var-value']     and not option.empty? then
+            @@kpse.expand_var(option)
+        elsif arguments.size > 0 then
+            files = Array.new
+            arguments.each do |option|
+                if file = @@kpse.find_file(option) and not file.empty? then
+                    files << file
+                end
+            end
+            files.join("\n")
+        else
+            ''
+        end
+    end
+
+    def KpseRunner.kpsereset
+        @@kpse = nil
+    end
+
+    private
+
+    def KpseRunner.split_args(arg)
+        vars, args = Hash.new, Array.new
+        arg.gsub!(/([\"\'])(.*?)\1/o) do
+            $2.gsub(' ','<space/>')
+        end
+        arg = arg.split(/\s+/o)
+        arg.collect! do |a|
+            a.gsub('<space/>',' ')
+        end
+        arg.each do |a|
+            if a =~ /^(.*?)\=(.*?)$/o then
+                k, v = $1, $2
+                vars[k.sub(/^\-+/,'')] = v
+            else
+                args << a
+            end
+        end
+        # puts vars.inspect
+        # puts args.inspect
+        return vars, args
+    end
+
+end
diff --git a/scripts/context/ruby/base/logger.rb b/scripts/context/ruby/base/logger.rb
new file mode 100644
index 000000000..2526cdb0e
--- /dev/null
+++ b/scripts/context/ruby/base/logger.rb
@@ -0,0 +1,104 @@
+# module    : base/logger
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2002-2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+require 'thread'
+
+# The next calls are valid:
+
+# @log.report('a','b','c', 'd')
+# @log.report('a','b',"c #{d}")
+# @log.report("a b c #{d}")
+
+# Keep in mind that "whatever #{something}" is two times faster than
+# 'whatever ' + something or ['whatever',something].join and that
+# when verbosity is not needed the following is much faster too:
+
+# @log.report('a','b','c', 'd') if @log.verbose?
+# @log.report('a','b',"c #{d}") if @log.verbose?
+# @log.report("a b c #{d}")     if @log.verbose?
+
+# The last three cases are equally fast when verbosity is turned off.
+
+# Under consideration: verbose per instance
+
+class Logger
+
+    @@length  = 0
+    @@verbose = false
+
+    def initialize(tag=nil,length=0,verbose=false)
+        @tag = tag || ''
+        @@verbose = @@verbose || verbose
+        @@length = @tag.length if @tag.length > @@length
+        @@length =      length if      length > @@length
+    end
+
+    def report(*str)
+        begin
+            case str.length
+                when 0
+                    print("\n")
+                    return true
+                when 1
+                    message = str.first
+                else
+                    message = [str].flatten.collect{|s| s.to_s}.join(' ').chomp
+            end
+            if @tag.empty? then
+                print("#{message}\n")
+            else
+                # try to avoid too many adjustments
+                @tag = @tag.ljust(@@length) unless @tag.length == @@length
+                print("#{@tag} | #{message}\n")
+            end
+        rescue
+        end
+        return true
+    end
+
+    def reportlines(*str)
+        unless @tag.empty? then
+            @tag = @tag.ljust(@@length) unless @tag.length == @@length
+        end
+        report([str].flatten.collect{|s| s.gsub(/\n/,"\n#{@tag} | ")}.join(' '))
+    end
+
+    def debug(*str)
+        report(str) if @@verbose
+    end
+
+    def error(*str)
+        if ! $! || $!.to_s.empty? then
+            report(str)
+        else
+            report(str,$!)
+        end
+    end
+
+    def verbose
+        @@verbose = true
+    end
+
+    def silent
+        @@verbose = false
+    end
+
+    def verbose?
+        @@verbose
+    end
+
+    # attr_reader :tag
+
+    # alias fatal  error
+    # alias info   debug
+    # alias warn   debug
+    # alias debug? :verbose?
+
+end
diff --git a/scripts/context/ruby/base/merge.rb b/scripts/context/ruby/base/merge.rb
new file mode 100644
index 000000000..a66b97e91
--- /dev/null
+++ b/scripts/context/ruby/base/merge.rb
@@ -0,0 +1,139 @@
+# module    : base/merge
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2006
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+# --selfmerg ewill create stand alone script (--selfcleanup does the opposite)
+
+# this module will package all the used modules in the file itself
+# so that we can relocate the file at wish, usage:
+#
+# merge:
+#
+# unless SelfMerge::ok? && SelfMerge::merge then
+#     puts("merging should happen on the path were the base inserts reside")
+# end
+#
+# cleanup:
+#
+# unless SelfMerge::cleanup then
+#     puts("merging should happen on the path were the base inserts reside")
+# end
+
+module SelfMerge
+
+    @@kpsemergestart = "\# kpse_merge_start"
+    @@kpsemergestop  = "\# kpse_merge_stop"
+    @@kpsemergefile  = "\# kpse_merge_file: "
+    @@kpsemergedone  = "\# kpse_merge_done: "
+
+    @@filename = File.basename($0)
+    @@ownpath  = File.expand_path(File.dirname($0))
+    @@modroot  = '(base|graphics|rslb|www)' # needed in regex in order not to mess up SelfMerge
+    @@modules  = $".collect do |file| File.expand_path(file) end
+
+    @@modules.delete_if do |file|
+        file !~ /^#{@@ownpath}\/#{@@modroot}.*$/i
+    end
+
+    def SelfMerge::ok?
+        begin
+            @@modules.each do |file|
+                return false unless FileTest.file?(file)
+            end
+        rescue
+            return false
+        else
+            return true
+        end
+    end
+
+    def SelfMerge::merge
+        begin
+            if SelfMerge::ok? && rbfile = IO.read(@@filename) then
+                begin
+                    inserts = "#{@@kpsemergestart}\n\n"
+                    @@modules.each do |file|
+                        inserts << "#{@@kpsemergefile}'#{file}'\n\n"
+                        inserts << IO.read(file).gsub(/^#.*?\n$/,'')
+                        inserts << "\n\n"
+                    end
+                    inserts << "#{@@kpsemergestop}\n\n"
+                    # no gsub! else we end up in SelfMerge
+                    rbfile.sub!(/#{@@kpsemergestart}\s*#{@@kpsemergestop}/moi) do
+                        inserts
+                    end
+                    rbfile.gsub!(/^(.*)(require [\"\'].*?#{@@modroot}.*)$/) do
+                        pre, post = $1, $2
+                        if pre =~ /#{@@kpsemergedone}/ then
+                            "#{pre}#{post}"
+                        else
+                            "#{pre}#{@@kpsemergedone}#{post}"
+                        end
+                    end
+                rescue
+                    return false
+                else
+                    begin
+                        File.open(@@filename,'w') do |f|
+                            f << rbfile
+                        end
+                    rescue
+                        return false
+                    end
+                end
+            end
+        rescue
+            return false
+        else
+            return true
+        end
+    end
+
+    def SelfMerge::cleanup
+        begin
+            if rbfile = IO.read(@@filename) then
+                begin
+                    rbfile.sub!(/#{@@kpsemergestart}(.*)#{@@kpsemergestop}\s*/moi) do
+                        "#{@@kpsemergestart}\n\n#{@@kpsemergestop}\n\n"
+                    end
+                    rbfile.gsub!(/^(.*#{@@kpsemergedone}.*)$/) do
+                        str = $1
+                        if str =~ /require [\"\']/ then
+                            str.gsub(/#{@@kpsemergedone}/, '')
+                        else
+                            str
+                        end
+                    end
+                rescue
+                    return false
+                else
+                    begin
+                        File.open(@@filename,'w') do |f|
+                            f << rbfile
+                        end
+                    rescue
+                        return false
+                    end
+                end
+            end
+        rescue
+            return false
+        else
+            return true
+        end
+    end
+
+    def SelfMerge::replace
+        if SelfMerge::ok? then
+            SelfMerge::cleanup
+            SelfMerge::merge
+        end
+    end
+
+end
diff --git a/scripts/context/ruby/base/mp.rb b/scripts/context/ruby/base/mp.rb
new file mode 100644
index 000000000..d168bde1d
--- /dev/null
+++ b/scripts/context/ruby/base/mp.rb
@@ -0,0 +1,167 @@
+# module    : base/mp
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2005-2006
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+module MPTools
+
+    @@definitions, @@start, @@stop, @@before, @@after = Hash.new, Hash.new, Hash.new, Hash.new, Hash.new
+
+
+    @@definitions['plain'] = <<EOT
+\\gdef\\mpxshipout{\\shipout\\hbox\\bgroup
+  \\setbox0=\\hbox\\bgroup}
+
+\\gdef\\stopmpxshipout{\\egroup  \\dimen0=\\ht0 \\advance\\dimen0\\dp0
+  \\dimen1=\\ht0 \\dimen2=\\dp0
+  \\setbox0=\\hbox\\bgroup
+    \\box0
+    \\ifnum\\dimen0>0 \\vrule width1sp height\\dimen1 depth\\dimen2
+    \\else \\vrule width1sp height1sp depth0sp\\relax
+    \\fi\\egroup
+  \\ht0=0pt \\dp0=0pt \\box0 \\egroup}
+EOT
+
+    @@start ['plain'] = ""
+    @@before['plain'] = "\\mpxshipout"
+    @@after ['plain'] = "\\stopmpxshipout"
+    @@stop  ['plain'] = "\\end{document}"
+
+    @@definitions['context'] = <<EOT
+\\ifx\\startMPXpage\\undefined
+
+  \\ifx\\loadallfontmapfiles\\undefined \\let\\loadallfontmapfiles\\relax \\fi
+
+  \\gdef\\startMPXpage
+    {\\shipout\\hbox
+     \\bgroup
+     \\setbox0=\\hbox
+     \\bgroup}
+
+  \\gdef\\stopMPXpage
+    {\\egroup
+     \\dimen0=\\ht0
+     \\advance\\dimen0\\dp0
+     \\dimen1=\\ht0
+     \\dimen2=\\dp0
+     \\setbox0=\\hbox\\bgroup
+       \\box0
+       \\ifnum\\dimen0>0
+         \\vrule width 1sp height \\dimen1 depth \\dimen2
+       \\else
+         \\vrule width 1sp height 1sp depth 0sp \\relax
+       \\fi
+    \\egroup
+    \\ht0=0pt
+    \\dp0=0pt
+    \\loadallfontmapfiles
+    \\box0
+    \\egroup}
+
+\\fi
+
+\\ifx\\starttext\\undefined
+
+  \\let\\starttext\\relax
+  \\def\\stoptext{\\end{document}}
+
+\\fi
+EOT
+
+    @@start ['context'] = "\\starttext"
+    @@before['context'] = "\\startMPXpage"
+    @@after ['context'] = "\\stopMPXpage"
+    @@stop  ['context'] = "\\stoptext"
+
+    # todo: \usemodule[m-mpx ] and test fo defined
+
+    def MPTools::mptotex(from,to=nil,method='plain')
+        begin
+            if from && data = IO.read(from) then
+                f = if to then File.open(to,'w') else $stdout end
+                f.puts("% file: #{from}")
+                f.puts("")
+                f.puts(@@definitions[method])
+                unless @@start[method].empty? then
+                    f.puts("")
+                    f.puts(@@start[method])
+                end
+                data.gsub!(/([^\\])%.*?$/mo) do
+                    $1
+                end
+                data.scan(/(verbatim|b)tex\s*(.*?)\s*etex/mo) do
+                    tag, text = $1, $2
+                    f.puts("")
+                    if tag == 'b' then
+                        f.puts(@@before[method])
+                        f.puts("#{text}%")
+                        f.puts(@@after [method])
+                    else
+                        f.puts("#{text}")
+                    end
+                    f.puts("")
+                end
+                f.puts("")
+                f.puts(@@stop[method])
+                f.close
+            else
+                return false
+            end
+        rescue
+            File.delete(to) rescue false
+            return false
+        else
+            return true
+        end
+    end
+
+    @@splitMPlines = false
+
+    def MPTools::splitmplines(str)
+        if @@splitMPlines then
+            btex, verbatimtex, strings, result = Array.new, Array.new, Array.new, str.dup
+            # protect texts
+            result.gsub!(/btex\s*(.*?)\s*etex/) do
+                btex << $1
+                "btex(#{btex.length-1})"
+            end
+            result.gsub!(/verbatimtex\s*(.*?)\s*etex/) do
+                verbatimtex << $1
+                "verbatimtex(#{verbatimtex.length-1})"
+            end
+            result.gsub!(/\"(.*?)\"/) do
+                strings << $1
+                "\"#{strings.length-1}\""
+            end
+            result.gsub!(/\;/) do
+                ";\n"
+            end
+            result.gsub!(/(.{80,})(\-\-\-|\-\-|\.\.\.|\.\.)/) do
+                "#{$1}#{$2}\n"
+            end
+            result.gsub!(/\n[\s\n]+/moi) do
+                "\n"
+            end
+            result.gsub!(/btex\((\d+)\)/) do
+                "btex #{btex[$1.to_i]} etex"
+            end
+            result.gsub!(/verbatimtex\((\d+)\)/) do
+                "verbatimtex #{verbatimtex[$1.to_i]} etex"
+            end
+            result.gsub!(/\"(\d+)\"/) do
+                "\"#{strings[$1.to_i]}\""
+            end
+            # return result # let's catch xetex bug
+            return result.gsub(/\^\^(M|J)/o, "\n")
+        else
+            # return str # let's catch xetex bug
+            return str.gsub(/\^\^(M|J)/o, "\n")
+        end
+    end
+
+end
diff --git a/scripts/context/ruby/base/pdf.rb b/scripts/context/ruby/base/pdf.rb
new file mode 100644
index 000000000..5aec06fc5
--- /dev/null
+++ b/scripts/context/ruby/base/pdf.rb
@@ -0,0 +1,75 @@
+module PDFview
+
+    @files      = Hash.new
+    @opencalls  = Hash.new
+    @closecalls = Hash.new
+    @allcalls   = Hash.new
+
+    @method     = 'default' # 'xpdf'
+
+    @opencalls['default']  = "pdfopen  --file" # "pdfopen --back --file"
+    @opencalls['xpdf']     = "xpdfopen"
+
+    @closecalls['default'] = "pdfclose --file"
+    @closecalls['xpdf']    = nil
+
+    @allcalls['default']   = "pdfclose --all"
+    @allcalls['xpdf']      = nil
+
+    def PDFview.setmethod(method)
+        @method = method
+    end
+
+    def PDFview.open(*list)
+        begin
+            [*list].flatten.each do |file|
+                filename = fullname(file)
+                if FileTest.file?(filename) then
+                    if @opencalls[@method] then
+                        result = `#{@opencalls[@method]} #{filename} 2>&1`
+                        @files[filename] = true
+                    end
+                end
+            end
+        rescue
+        end
+    end
+
+    def PDFview.close(*list)
+        [*list].flatten.each do |file|
+            filename = fullname(file)
+            begin
+                if @files.key?(filename) then
+                    if @closecalls[@method] then
+                        result = `#{@closecalls[@method]} #{filename} 2>&1`
+                    end
+                else
+                    closeall
+                    return
+                end
+            rescue
+            end
+            @files.delete(filename)
+        end
+    end
+
+    def PDFview.closeall
+        begin
+            if @allcalls[@method] then
+                result = `#{@allcalls[@method]} 2>&1`
+            end
+        rescue
+        end
+        @files.clear
+    end
+
+    def PDFview.fullname(name)
+        name + if name =~ /\.pdf$/ then '' else '.pdf' end
+    end
+
+end
+
+# PDFview.open("t:/document/show-exa.pdf")
+# PDFview.open("t:/document/show-gra.pdf")
+# PDFview.close("t:/document/show-exa.pdf")
+# PDFview.close("t:/document/show-gra.pdf")
diff --git a/scripts/context/ruby/base/state.rb b/scripts/context/ruby/base/state.rb
new file mode 100644
index 000000000..76ef50b25
--- /dev/null
+++ b/scripts/context/ruby/base/state.rb
@@ -0,0 +1,75 @@
+require 'digest/md5'
+
+# todo: register omissions per file
+
+class FileState
+
+    def initialize
+        @states = Hash.new
+        @omiter = Hash.new
+    end
+
+    def reset
+        @states.clear
+        @omiter.clear
+    end
+
+    def register(filename,omit=nil)
+        unless @states.key?(filename) then
+            @states[filename] = Array.new
+            @omiter[filename] = omit
+        end
+        @states[filename] << checksum(filename,@omiter[filename])
+    end
+
+    def update(filename=nil)
+        [filename,@states.keys].flatten.compact.uniq.each do |fn|
+            register(fn)
+        end
+    end
+
+    def inspect(filename=nil)
+        result = ''
+        [filename,@states.keys].flatten.compact.uniq.sort.each do |fn|
+            if @states.key?(fn) then
+                result += "#{fn}: #{@states[fn].inspect}\n"
+            end
+        end
+        result
+    end
+
+    def changed?(filename)
+        if @states.key?(filename) then
+            n = @states[filename].length
+            if n>1 then
+                changed = @states[filename][n-1] != @states[filename][n-2]
+            else
+                changed = true
+            end
+        else
+            changed = true
+        end
+        return changed
+    end
+
+    def checksum(filename,omit=nil)
+        sum = ''
+        begin
+            if FileTest.file?(filename) && (data = IO.read(filename)) then
+                data.gsub!(/\n.*?(#{[omit].flatten.join('|')}).*?\n/) do "\n" end if omit
+                sum = Digest::MD5.hexdigest(data).upcase
+            end
+        rescue
+            sum = ''
+        end
+        return sum
+    end
+
+    def stable?
+        @states.keys.each do |s|
+            return false if changed?(s)
+        end
+        return true
+    end
+
+end
diff --git a/scripts/context/ruby/base/switch.rb b/scripts/context/ruby/base/switch.rb
new file mode 100644
index 000000000..19eced424
--- /dev/null
+++ b/scripts/context/ruby/base/switch.rb
@@ -0,0 +1,635 @@
+# module    : base/switch
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2002-2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+# we cannot use getoptlong because we want to be more
+# tolerant; also we want to be case insensitive (2002).
+
+# we could make each option a class itself, but this is
+# simpler; also we can put more in the array
+
+# beware: regexps/o in methods are optimized globally
+
+require "rbconfig"
+
+$mswindows = Config::CONFIG['host_os'] =~ /mswin/
+$separator = File::PATH_SEPARATOR
+
+class String
+
+    def has_suffix?(suffix)
+        self =~ /\.#{suffix}$/i
+    end
+
+end
+
+# may move to another module
+
+class File
+
+    @@update_eps = 1
+
+    def File.needsupdate(oldname,newname)
+        begin
+            oldtime = File.stat(oldname).mtime.to_i
+            newtime = File.stat(newname).mtime.to_i
+            if newtime >= oldtime then
+                return false
+            elsif oldtime-newtime < @@update_eps then
+                return false
+            else
+                return true
+            end
+        rescue
+            return true
+        end
+    end
+
+    def File.syncmtimes(oldname,newname)
+        return
+        begin
+            if $mswindows then
+                # does not work (yet) / gives future timestamp
+                # t = File.mtime(oldname) # i'm not sure if the time is frozen, so we do it here
+                # File.utime(0,t,oldname,newname)
+            else
+                t = File.mtime(oldname) # i'm not sure if the time is frozen, so we do it here
+                File.utime(0,t,oldname,newname)
+            end
+        rescue
+        end
+    end
+
+    def File.timestamp(name)
+        begin
+            "#{File.stat(name).mtime}"
+        rescue
+            return 'unknown'
+        end
+    end
+
+end
+
+# main thing
+
+module CommandBase
+
+    # this module can be used as a mixin in a command handler
+
+    $stdout.sync = true
+
+    def initialize(commandline,logger,banner)
+        @commandline, @logger, @banner = commandline, logger, banner
+        @forcenewline, @versiondone, @error = false, false, false
+        version if @commandline.option('version')
+    end
+
+    def reportlines(*str)
+        @logger.reportlines(str)
+    end
+
+    # only works in 1.8
+    #
+    # def report(*str)
+    #     @logger.report(str)
+    # end
+    #
+    # def version # just a bit of playing with defs
+    #    report(@banner.join(' - '))
+    #    def report(*str)
+    #        @logger.report
+    #        @logger.report(str)
+    #        def report(*str)
+    #            @logger.report(str)
+    #        end
+    #    end
+    #    def version
+    #    end
+    # end
+
+    def report(*str)
+        initlogger ; @logger.report(str)
+    end
+
+    def seterror
+        @error = true
+    end
+
+    def error?
+        return @error
+    end
+
+    def exit
+        if @error then Kernel.exit(1) else Kernel.exit(0) end
+    end
+
+    def execute(str=nil)
+        send(str || action || 'main')
+        exit
+    end
+
+    def debug(*str)
+        initlogger ; @logger.debug(str)
+    end
+
+    def error(*str)
+        initlogger ; @logger.error(str)
+    end
+
+    def initlogger
+        if @forcenewline then
+            @logger.report
+            @forcenewline = false
+        end
+    end
+
+    def logger
+        @logger
+    end
+
+    def version # just a bit of playing with defs
+        unless @versiondone then
+            report(@banner.join(' - '))
+            @forcenewline = true
+            @versiondone = true
+        end
+    end
+
+    def help
+        version # is nilled when already given
+        @commandline.helpkeys.each do |k|
+            if @commandline.help?(k) then
+                kstr = ('--'+k).ljust(@commandline.helplength+2)
+                message = @commandline.helptext(k)
+                message = '' if message == CommandLine::NOHELP
+                message = message.split(/\s*\n\s*/)
+                loop do
+                    report("#{kstr} #{message.shift}")
+                    kstr = ' '*kstr.length
+                    break if message.length == 0
+                end
+            end
+        end
+    end
+
+    def option(key)
+        @commandline.option(key)
+    end
+    def oneof(*key)
+        @commandline.oneof(*key)
+    end
+
+    def globfiles(pattern='*',suffix=nil)
+        @commandline.setarguments([pattern].flatten)
+        if files = findfiles(suffix) then
+            @commandline.setarguments(files)
+        else
+            @commandline.setarguments
+        end
+    end
+
+    private
+
+    def findfiles(suffix=nil)
+
+        if @commandline.arguments.length>1 then
+            return @commandline.arguments
+        else
+            pattern  = @commandline.argument('first')
+            pattern  = '*' if pattern.empty?
+            if suffix && ! pattern.match(/\..+$/o) then
+                suffix   = '.' + suffix
+                pattern += suffix unless pattern =~ /#{suffix}$/
+            end
+            # not {} safe
+            pattern = '**/' + pattern if @commandline.option('recurse')
+            files = Dir[pattern]
+            if files && files.length>0 then
+                return files
+            else
+                pattern = @commandline.argument('first')
+                if FileTest.file?(pattern) then
+                    return [pattern]
+                else
+                    report("no files match pattern #{pattern}")
+                    return nil
+                end
+            end
+        end
+
+    end
+
+    def globbed(pattern,recurse=false)
+
+        files = Array.new
+        pattern.split(' ').each do |p|
+            if recurse then
+                if p =~ /^(.*)(\/.*?)$/i then
+                    p = $1 + '/**' + $2
+                else
+                    p = '**/' + p
+                end
+                p.gsub!(/[\\\/]+/, '/')
+            end
+            files.push(Dir.glob(p))
+        end
+        files.flatten.sort do |a,b|
+            pathcompare(a,b)
+        end
+    end
+
+    def pathcompare(a,b)
+
+        aa, bb = a.split('/'), b.split('/')
+        if aa.length == bb.length then
+            aa.each_index do |i|
+                if aa[i]<bb[i] then
+                    return -1
+                elsif aa[i]>bb[i] then
+                    return +1
+                end
+            end
+            return 0
+        else
+            return aa.length <=> bb.length
+        end
+
+    end
+
+end
+
+class CommandLine
+
+    VALUE, FLAG = 1, 2
+    NOHELP = 'no arguments'
+
+    def initialize(prefix='-')
+
+        @registered = Array.new
+        @options    = Hash.new
+        @unchecked  = Hash.new
+        @arguments  = Array.new
+        @original   = ARGV.join(' ')
+        @helptext   = Hash.new
+        @mandated   = Hash.new
+        @provided   = Hash.new
+        @prefix     = prefix
+        @actions    = Array.new
+
+        # The quotes in --switch="some value" get lost in ARGV, so we need to do some trickery here.
+
+        @original = ''
+        ARGV.each do |a|
+            aa = a.strip.gsub(/^([#{@prefix}]+\w+\=)([^\"].*?\s+.*[^\"])$/) do
+                $1 + "\"" + $2 + "\""
+            end
+            @original += if @original.empty? then '' else ' ' end + aa
+        end
+
+    end
+
+    def setarguments(args=[])
+        @arguments = if args then args else [] end
+    end
+
+    def register(option,shortcut,kind,default=false,action=false,helptext='')
+        if kind == FLAG then
+            @options[option] = default
+        elsif not default then
+            @options[option] = ''
+        else
+            @options[option] = default
+        end
+        @registered.push([option,shortcut,kind])
+        @mandated[option] = false
+      # @provided[option] = false
+        @helptext[option] = helptext
+        @actions.push(option) if action
+    end
+
+    def registerflag(option,default=false,helptext='')
+        if default.class == String then
+            register(option,'',FLAG,false,false,default)
+        else
+            register(option,'',FLAG,false,false,helptext)
+        end
+    end
+
+    def registervalue(option,default='',helptext='')
+        register(option,'',VALUE,default,false,helptext)
+    end
+
+    def registeraction(option,helptext='')
+        register(option,'',FLAG,false,true,helptext)
+    end
+
+    def registermandate(*option)
+        [*option].each do |o|
+            [o].each do |oo|
+                @mandated[oo] = true
+            end
+        end
+    end
+
+    def actions
+        a = @actions.delete_if do |t|
+            ! option(t)
+        end
+        if a && a.length>0 then
+            return a
+        else
+            return nil
+        end
+    end
+
+    def action
+        @actions.each do |t|
+            return t if option(t)
+        end
+        return nil
+    end
+
+    def forgotten
+        @mandated.keys.sort - @provided.keys.sort
+    end
+
+    def registerhelp(option,text='')
+        @helptext['unknown'] = if text.empty? then option else text end
+    end
+
+    def helpkeys(option='.*')
+        @helptext.keys.sort.grep(/#{option}/)
+    end
+
+    def helptext(option)
+        @helptext.fetch(option,'')
+    end
+
+    def help?(option)
+        @helptext[option] && ! @helptext[option].empty?
+    end
+
+    def helplength
+        n = 0
+        @helptext.keys.each do |h|
+            n = h.length if h.length>n
+        end
+        return n
+    end
+
+    def expand
+
+        # todo : '' or false, depending on type
+        # @options.clear
+        # @arguments.clear
+
+        dirtyvalue(@original).split(' ').each do |arg|
+            case arg
+                when /^[#{@prefix}][#{@prefix}](.+?)\=(.*?)$/ then locatedouble($1,$2)
+                when /^[#{@prefix}][#{@prefix}](.+?)$/        then locatedouble($1,false)
+                when /^[#{@prefix}](.)\=(.)$/                 then locatesingle($1,$2)
+                when /^[#{@prefix}](.+?)$/                    then locateseries($1,false)
+                when /^[\+\-]+/o                              then # do nothing
+            else
+                arguments.push(arg)
+            end
+        end
+
+        @options or @unchecked or @arguments
+
+    end
+
+    def extend (str)
+        @original = @original + ' ' + str
+    end
+
+    def replace (str)
+        @original = str
+    end
+
+    def show
+      # print "-- options --\n"
+        @options.keys.sort.each do |key|
+            print "option: #{key} -> #{@options[key]}\n"
+        end
+      # print "-- arguments --\n"
+        @arguments.each_index do |key|
+            print "argument: #{key} -> #{@arguments[key]}\n"
+        end
+    end
+
+    def option(str,default=nil)
+        if @options.key?(str) then
+            @options[str]
+        elsif default then
+            default
+        else
+            @options[str]
+        end
+    end
+
+    def checkedoption(str,default='')
+        if @options.key?(str) then
+            if @options[str].empty? then default else @options[str] end
+        else
+            default
+        end
+    end
+
+    def foundoption(str,default='')
+        str = str.split(',') if str.class == String
+        str.each do |s|
+            return str if @options.key?(str)
+        end
+        return default
+    end
+
+    def oneof(*key)
+        [*key].flatten.compact.each do |k|
+           return true if @options.key?(k) && @options[k]
+        end
+        return false
+    end
+
+    def setoption(str,value)
+        @options[str] = value
+    end
+
+    def getoption(str,value='') # value ?
+        @options[str]
+    end
+
+    def argument(n=0)
+        if n.class == String then
+            case n
+                when 'first'  then argument(0)
+                when 'second' then argument(1)
+                when 'third'  then argument(2)
+            else
+                argument(0)
+            end
+        elsif @arguments[n] then
+            @arguments[n]
+        else
+            ''
+        end
+    end
+
+    # a few local methods, cannot be defined nested (yet)
+
+    private
+
+    def dirtyvalue(value)
+        if value then
+            value.gsub(/([\"\'])(.*?)\1/) do
+                $2.gsub(/\s+/o, "\xFF")
+            end
+        else
+            ''
+        end
+    end
+
+    def cleanvalue(value)
+        if value then
+            # value.sub(/^([\"\'])(.*?)\1$/) { $2.gsub(/\xFF/o, ' ') }
+            value.gsub(/\xFF/o, ' ')
+        else
+            ''
+        end
+    end
+
+    def locatedouble(key, value)
+
+        foundkey, foundkind = nil, nil
+
+        @registered.each do |option, shortcut, kind|
+            if option == key then
+                foundkey, foundkind = option, kind
+                break
+            end
+        end
+        unless foundkey then
+            @registered.each do |option, shortcut, kind|
+                n = 0
+                begin
+                    re = /^#{key}/i
+                rescue
+                    key = key.inspect.sub(/^\"(.*)\"$/) do $1 end
+                    re = /^#{key}/i
+                ensure
+                    if option =~ re then
+                        case n
+                            when 0
+                                foundkey, foundkind, n = option, kind, 1
+                            when 1
+                                # ambiguous matches, like --fix => --fixme --fixyou
+                                foundkey, foundkind = nil, nil
+                                break
+                        end
+                    end
+                end
+            end
+        end
+        if foundkey then
+            @provided[foundkey] = true
+            if foundkind == VALUE then
+                @options[foundkey] = cleanvalue(value)
+            else
+                @options[foundkey] = true
+            end
+        else
+            if value.class == FalseClass then
+                @unchecked[key] = true
+            else
+                @unchecked[key] = cleanvalue(value)
+            end
+        end
+
+    end
+
+    def locatesingle(key, value)
+
+        @registered.each do |option, shortcut, kind|
+            if shortcut == key then
+                @provided[option] = true
+                @options[option] = if kind == VALUE then '' else cleanvalue(value) end
+                break
+            end
+        end
+
+    end
+
+    def locateseries(series, value)
+
+        series.each do |key|
+            locatesingle(key,cleanvalue(value))
+        end
+
+    end
+
+    public
+
+    attr_reader :arguments, :options, :original, :unchecked
+
+end
+
+# options = CommandLine.new
+#
+# options.register("filename", "f", CommandLine::VALUE)
+# options.register("request" , "r", CommandLine::VALUE)
+# options.register("verbose" , "v", CommandLine::FLAG)
+#
+# options.expand
+# options.extend(str)
+# options.show
+#
+# c = CommandLine.new
+#
+# c.registervalue('aaaa')
+# c.registervalue('test')
+# c.registervalue('zzzz')
+#
+# c.registerhelp('aaaa','some aaaa to enter')
+# c.registerhelp('test','some text to enter')
+# c.registerhelp('zzzz','some zzzz to enter')
+#
+# c.registermandate('test')
+#
+# c.expand
+#
+# class CommandLine
+#
+# def showhelp (banner,*str)
+#   if helpkeys(*str).length>0
+#     print banner
+#     helpkeys(*str).each do |h|
+#       print helptext(h) + "\n"
+#     end
+#     true
+#   else
+#     false
+#   end
+# end
+#
+# def showmandate(banner)
+#   if forgotten.length>0
+#     print banner
+#     forgotten.each do |f|
+#       print helptext(f) + "\n"
+#     end
+#     true
+#   else
+#     false
+#   end
+# end
+#
+# end
+#
+# c.showhelp("you can provide:\n\n")
+# c.showmandate("you also need to provide:\n\n")
diff --git a/scripts/context/ruby/base/system.rb b/scripts/context/ruby/base/system.rb
new file mode 100644
index 000000000..c3fb08645
--- /dev/null
+++ b/scripts/context/ruby/base/system.rb
@@ -0,0 +1,121 @@
+# module    : base/system
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2002-2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+require "rbconfig"
+
+module System
+
+    @@mswindows   = Config::CONFIG['host_os'] =~ /mswin/
+    @@binpaths    = ENV['PATH'].split(File::PATH_SEPARATOR)
+    @@binsuffixes = if $mswindows then ['.exe','.com','.bat'] else ['','.sh','.csh'] end
+    @@located     = Hash.new
+    @@binnames    = Hash.new
+
+    if @@mswindows then
+        @@binnames['ghostscript'] = ['gswin32c.exe','gs.cmd','gs.bat']
+        @@binnames['imagemagick'] = ['imagemagick.exe','convert.exe']
+        @@binnames['inkscape']    = ['inkscape.exe']
+    else
+        @@binnames['ghostscript'] = ['gs']
+        @@binnames['imagemagick'] = ['convert']
+        @@binnames['inkscape']    = ['inkscape']
+    end
+
+
+    def System.null
+        if @@mswindows then 'nul' else '/dev/null' end
+    end
+
+    def System.unix?
+        not @@mswindows
+    end
+    def System.mswin?
+        @@mswindows
+    end
+
+    def System.binnames(str)
+        if @@binnames.key?(str) then
+            @@binnames[str]
+        else
+            [str]
+        end
+    end
+
+    def System.prependengine(str)
+        if str =~ /^\S+\.(pl|rb|lua|py)/io then
+            case $1
+                when 'pl'  then return "perl #{str}"
+                when 'rb'  then return "ruby #{str}"
+                when 'lua' then return "lua #{str}"
+                when 'py'  then return "python #{str}"
+            end
+        end
+        return str
+    end
+
+    def System.locatedprogram(program)
+        if @@located.key?(program) then
+            return @@located[program]
+        else
+            System.binnames(program).each do |binname|
+                if binname =~ /\..*$/io then
+                    @@binpaths.each do |path|
+                        if FileTest.file?(str = File.join(path,binname)) then
+                            return @@located[program] = System.prependengine(str)
+                        end
+                    end
+                end
+                binname.gsub!(/\..*$/io, '')
+                @@binpaths.each do |path|
+                    @@binsuffixes.each do |suffix|
+                        if FileTest.file?(str = File.join(path,"#{binname}#{suffix}")) then
+                            return @@located[program] = System.prependengine(str)
+                        end
+                    end
+                end
+            end
+        end
+        return @@located[program] = "texmfstart #{program}"
+    end
+
+    def System.command(program,arguments='')
+        if program =~ /^(.*?) (.*)$/ then
+            program = System.locatedprogram($1) + ' ' + $2
+        else
+            program = System.locatedprogram(program)
+        end
+        program = program + ' ' + arguments if ! arguments.empty?
+        program.gsub!(/\s+/io, ' ')
+        #program.gsub!(/(\/\.\/)+/io, '/')
+        program.gsub!(/\\/io, '/')
+        return program
+    end
+
+    def System.run(program,arguments='',pipe=false,collect=false)
+        if pipe then
+            if collect then
+                `#{System.command(program,arguments)} 2>&1`
+            else
+                `#{System.command(program,arguments)}`
+            end
+        else
+            system(System.command(program,arguments))
+        end
+    end
+
+    def System.pipe(program,arguments='',collect=false)
+        System.run(program,arguments,true)
+    end
+
+    def System.safepath(path)
+        if path.match(/ /o) then "\"#{path}\"" else path end
+    end
+
+end
diff --git a/scripts/context/ruby/base/tex.rb b/scripts/context/ruby/base/tex.rb
new file mode 100644
index 000000000..84025693b
--- /dev/null
+++ b/scripts/context/ruby/base/tex.rb
@@ -0,0 +1,2299 @@
+# module    : base/tex
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+# todo:
+#
+# - write systemcall for mpost to file so that it can be run faster
+# - use -8bit and -progname
+#
+
+# report ?
+
+require 'fileutils'
+
+require 'base/variables'
+require 'base/kpse'
+require 'base/system'
+require 'base/state'
+require 'base/pdf'
+require 'base/file'
+require 'base/ctx'
+require 'base/mp'
+
+class String
+
+    def standard?
+        begin
+            self == 'standard'
+        rescue
+            false
+        end
+    end
+
+end
+
+# class String
+    # def utf_bom?
+        # self.match(/^\357\273\277/o).length>0 rescue false
+    # end
+# end
+
+class Array
+
+    def standard?
+        begin
+            self.include?('standard')
+        rescue
+            false
+        end
+    end
+
+    def join_path
+        self.join(File::PATH_SEPARATOR)
+    end
+
+end
+
+class TEX
+
+    # The make-part of this class was made on a rainy day while listening
+    # to "10.000 clowns on a rainy day" by Jan Akkerman. Unfortunately the
+    # make method is not as swinging as this live cd.
+
+    include Variables
+
+    @@texengines      = Hash.new
+    @@mpsengines      = Hash.new
+    @@backends        = Hash.new
+    @@mappaths        = Hash.new
+    @@runoptions      = Hash.new
+    @@tcxflag         = Hash.new
+    @@draftoptions    = Hash.new
+    @@synctexcoptions = Hash.new
+    @@texformats      = Hash.new
+    @@mpsformats      = Hash.new
+    @@prognames       = Hash.new
+    @@texmakestr      = Hash.new
+    @@texprocstr      = Hash.new
+    @@mpsmakestr      = Hash.new
+    @@mpsprocstr      = Hash.new
+    @@texmethods      = Hash.new
+    @@mpsmethods      = Hash.new
+    @@pdftex          = 'pdftex' # new default, pdfetex is gone
+    @@luafiles        = "luafiles.tmp"
+    @@luatarget       = "lua/context"
+
+    @@platformslash = if System.unix? then "\\\\" else "\\" end
+
+    ['tex','etex','pdftex','pdfetex','standard']   .each do |e| @@texengines[e] = 'pdftex'    end
+    ['aleph','omega']                              .each do |e| @@texengines[e] = 'aleph'     end
+    ['xetex']                                      .each do |e| @@texengines[e] = 'xetex'     end
+    ['petex']                                      .each do |e| @@texengines[e] = 'petex'     end
+    ['luatex']                                     .each do |e| @@texengines[e] = 'luatex'    end
+
+    ['metapost','mpost', 'standard']               .each do |e| @@mpsengines[e] = 'mpost'     end
+
+    ['pdfetex','pdftex','pdf','pdftex','standard'] .each do |b| @@backends[b]   = 'pdftex'    end
+    ['dvipdfmx','dvipdfm','dpx','dpm']             .each do |b| @@backends[b]   = 'dvipdfmx'  end
+    ['xetex','xtx']                                .each do |b| @@backends[b]   = 'xetex'     end
+    ['petex']                                      .each do |b| @@backends[b]   = 'dvipdfmx'  end
+    ['aleph']                                      .each do |b| @@backends[b]   = 'dvipdfmx'  end
+    ['dvips','ps','dvi']                           .each do |b| @@backends[b]   = 'dvips'     end
+    ['dvipsone']                                   .each do |b| @@backends[b]   = 'dvipsone'  end
+    ['acrobat','adobe','distiller']                .each do |b| @@backends[b]   = 'acrobat'   end
+    ['xdv','xdv2pdf']                              .each do |b| @@backends[b]   = 'xdv2pdf'   end
+
+    ['tex','standard']                             .each do |b| @@mappaths[b]   = 'dvips'     end
+    ['pdftex','pdfetex']                           .each do |b| @@mappaths[b]   = 'pdftex'    end
+    ['aleph','omega','xetex','petex']              .each do |b| @@mappaths[b]   = 'dvipdfmx'  end
+    ['dvipdfm', 'dvipdfmx', 'xdvipdfmx']           .each do |b| @@mappaths[b]   = 'dvipdfmx'  end
+    ['xdv','xdv2pdf']                              .each do |b| @@mappaths[b]   = 'dvips'     end
+
+    # todo norwegian (no)
+
+    ['plain']                                      .each do |f| @@texformats[f] = 'plain'     end
+    ['cont-en','en','english','context','standard'].each do |f| @@texformats[f] = 'cont-en'   end
+    ['cont-nl','nl','dutch']                       .each do |f| @@texformats[f] = 'cont-nl'   end
+    ['cont-de','de','german']                      .each do |f| @@texformats[f] = 'cont-de'   end
+    ['cont-it','it','italian']                     .each do |f| @@texformats[f] = 'cont-it'   end
+    ['cont-fr','fr','french']                      .each do |f| @@texformats[f] = 'cont-fr'   end
+    ['cont-cs','cs','cont-cz','cz','czech']        .each do |f| @@texformats[f] = 'cont-cs'   end
+    ['cont-ro','ro','romanian']                    .each do |f| @@texformats[f] = 'cont-ro'   end
+    ['cont-gb','gb','cont-uk','uk','british']      .each do |f| @@texformats[f] = 'cont-gb'   end
+    ['cont-pe','pe','persian']                     .each do |f| @@texformats[f] = 'cont-pe'   end
+    ['cont-xp','xp','experimental']                .each do |f| @@texformats[f] = 'cont-xp'   end
+    ['mptopdf']                                    .each do |f| @@texformats[f] = 'mptopdf'   end
+
+    ['latex']                                      .each do |f| @@texformats[f] = 'latex.ltx' end
+
+    ['plain','mpost']                              .each do |f| @@mpsformats[f] = 'mpost'     end
+    ['metafun','context','standard']               .each do |f| @@mpsformats[f] = 'metafun'   end
+
+    ['pdftex','pdfetex','aleph','omega','petex',
+     'xetex','luatex']                             .each do |p| @@prognames[p]  = 'context'   end
+    ['mpost']                                      .each do |p| @@prognames[p]  = 'metafun'   end
+    ['latex','pdflatex']                           .each do |p| @@prognames[p]  = 'latex'     end
+
+    ['plain','default','standard','mptopdf']       .each do |f| @@texmethods[f] = 'plain'     end
+    ['cont-en','cont-nl','cont-de','cont-it',
+     'cont-fr','cont-cs','cont-ro','cont-gb',
+     'cont-pe','cont-xp']                          .each do |f| @@texmethods[f] = 'context'   end
+    ['latex','pdflatex']                           .each do |f| @@texmethods[f] = 'latex'     end
+
+    ['plain','default','standard']                 .each do |f| @@mpsmethods[f] = 'plain'     end
+    ['metafun']                                    .each do |f| @@mpsmethods[f] = 'metafun'   end
+
+    @@texmakestr['plain'] = @@platformslash + "dump"
+    @@mpsmakestr['plain'] = @@platformslash + "dump"
+
+    ['cont-en','cont-nl','cont-de','cont-it',
+     'cont-fr','cont-cs','cont-ro','cont-gb',
+     'cont-pe','cont-xp']                         .each do |f| @@texprocstr[f] = @@platformslash + "emergencyend"  end
+
+    @@runoptions['aleph']      = ['--8bit']
+    @@runoptions['luatex']     = ['--file-line-error']
+    @@runoptions['mpost']      = ['--8bit']
+    @@runoptions['pdfetex']    = ['--8bit']           # obsolete
+    @@runoptions['pdftex']     = ['--8bit']           # pdftex is now pdfetex
+  # @@runoptions['petex']      = []
+    @@runoptions['xetex']      = ['--8bit','-output-driver="xdvipdfmx -E -d 4 -V 5"']
+    @@draftoptions['pdftex']   = ['--draftmode']
+    @@synctexcoptions['pdftex'] = ['--synctex=1']
+    @@synctexcoptions['luatex'] = ['--synctex=1']
+    @@synctexcoptions['xetex']  = ['--synctex=1']
+
+    @@tcxflag['aleph']   = true
+    @@tcxflag['luatex']  = false
+    @@tcxflag['mpost']   = false
+    @@tcxflag['pdfetex'] = true
+    @@tcxflag['pdftex']  = true
+    @@tcxflag['petex']   = false
+    @@tcxflag['xetex']   = false
+
+    @@mainbooleanvars = [
+        'batchmode', 'nonstopmode', 'fast', 'final',
+        'paranoid', 'notparanoid', 'nobanner', 'once', 'allpatterns', 'draft',
+        'nompmode', 'nomprun', 'automprun', 'combine',
+        'nomapfiles', 'local',
+        'arrange', 'noarrange',
+        'forcexml', 'foxet',
+        'alpha', 'beta', 'luatex',
+        'mpyforce', 'forcempy',
+        'forcetexutil', 'texutil',
+        'globalfile', 'autopath',
+        'purge', 'purgeall', 'keep', 'autopdf', 'xpdf', 'simplerun', 'verbose',
+        'nooptionfile', 'nobackend', 'noctx', 'utfbom',
+        'mkii','mkiv',
+        'synctex',
+    ]
+    @@mainstringvars = [
+        'modefile', 'result', 'suffix', 'response', 'path',
+        'filters', 'usemodules', 'environments', 'separation', 'setuppath',
+        'arguments', 'input', 'output', 'randomseed', 'modes', 'mode', 'filename',
+        'ctxfile', 'printformat', 'paperformat', 'paperoffset',
+        'timeout', 'passon'
+    ]
+    @@mainstandardvars = [
+        'mainlanguage', 'bodyfont', 'language'
+    ]
+    @@mainknownvars = [
+        'engine', 'distribution', 'texformats', 'mpsformats', 'progname', 'interface',
+        'runs', 'backend'
+    ]
+
+    @@extrabooleanvars = []
+    @@extrastringvars = []
+
+    def booleanvars
+        [@@mainbooleanvars,@@extrabooleanvars].flatten.uniq
+    end
+    def stringvars
+        [@@mainstringvars,@@extrastringvars].flatten.uniq
+    end
+    def standardvars
+        [@@mainstandardvars].flatten.uniq
+    end
+    def knownvars
+        [@@mainknownvars].flatten.uniq
+    end
+    def allbooleanvars
+        [@@mainbooleanvars,@@extrabooleanvars].flatten.uniq
+    end
+    def allstringvars
+        [@@mainstringvars,@@extrastringvars,@@mainstandardvars,@@mainknownvars].flatten.uniq
+    end
+
+    def setextrastringvars(vars)
+        # @@extrastringvars << vars -- problems in 1.9
+        @@extrastringvars = [@@extrastringvars,vars].flatten
+    end
+    def setextrabooleanvars(vars)
+        # @@extrabooleanvars << vars -- problems in 1.9
+        @@extrabooleanvars = [@@extrabooleanvars,vars].flatten
+    end
+
+    # def jobvariables(names=nil)
+        # if [names ||[]].flatten.size == 0 then
+            # names = [allbooleanvars,allstringvars].flatten
+        # end
+        # data = Hash.new
+        # names.each do |name|
+            # if allbooleanvars.include?(name) then
+                # data[name] = if getvariable(name) then "yes" else "no" end
+            # else
+                # data[name] = getvariable(name)
+            # end
+        # end
+        # data
+    # end
+
+    # def setjobvariables(names=nil)
+        # assignments = Array.new
+        # jobvariables(names).each do |k,v|
+            # assignments << "#{k}=\{#{v}\}"
+        # end
+        # "\setvariables[exe][#{assignments.join(", ")}]"
+    # end
+
+    @@temprunfile = 'texexec'
+    @@temptexfile = 'texexec.tex'
+
+    def initialize(logger=nil)
+        if @logger = logger then
+            def report(str='')
+                @logger.report(str)
+            end
+        else
+            def report(str='')
+                puts(str)
+            end
+        end
+        @cleanups    = Array.new
+        @variables   = Hash.new
+        @startuptime = Time.now
+        # options
+        booleanvars.each do |k|
+            setvariable(k,false)
+        end
+        stringvars.each do |k|
+            setvariable(k,'')
+        end
+        standardvars.each do |k|
+            setvariable(k,'standard')
+        end
+        setvariable('distribution', Kpse.distribution)
+        setvariable('texformats',   defaulttexformats)
+        setvariable('mpsformats',   defaultmpsformats)
+        setvariable('progname',     'standard') # or ''
+        setvariable('interface',    'standard')
+        setvariable('engine',       'standard') # replaced by tex/mpsengine
+        setvariable('backend',      'pdftex')
+        setvariable('runs',         '8')
+        setvariable('randomseed',   rand(1440).to_s) # we want the same seed for one run
+        # files
+        setvariable('files',        [])
+        # defaults
+        setvariable('texengine',    'standard')
+        setvariable('mpsengine',    'standard')
+        setvariable('backend',      'standard')
+        setvariable('error',        '')
+    end
+
+    def error?
+        not getvariable('error').empty?
+    end
+
+    def runtime
+        Time.now - @startuptime
+    end
+
+    def reportruntime
+        report("runtime: #{runtime}")
+    end
+
+    def runcommand(something)
+        command = [something].flatten.join(' ')
+        report("running: #{command}") if getvariable('verbose')
+        system(command)
+    end
+
+    def inspect(name=nil)
+        if ! name || name.empty? then
+            name = [booleanvars,stringvars,standardvars,knownvars]
+        end
+        str = '' # allocate
+        [name].flatten.each do |n|
+            if str = getvariable(n) then
+                str = str.join(" ") if str.class == Array
+                unless (str.class == String) && str.empty? then
+                    report("option '#{n}' is set to '#{str}'")
+                end
+            end
+        end
+    end
+
+    def tempfilename(suffix='')
+        @@temprunfile + if suffix.empty? then '' else ".#{suffix}" end
+    end
+
+    def cleanup
+        @cleanups.each do |name|
+            begin
+                File.delete(name) if FileTest.file?(name)
+            rescue
+                report("unable to delete #{name}")
+            end
+        end
+    end
+
+    def cleanuptemprunfiles
+        begin
+            Dir.glob("#{@@temprunfile}*").each do |name|
+                if File.file?(name) && (File.splitname(name)[1] !~ /(pdf|dvi)/o) then
+                    File.delete(name) rescue false
+                end
+            end
+        rescue
+        end
+        ['mpgraph.mp'].each do |file|
+            (File.delete(file) if (FileTest.size?(file) rescue 10) < 10) rescue false
+        end
+    end
+
+    def backends() @@backends.keys.sort end
+
+    def texengines() @@texengines.keys.sort end
+    def mpsengines() @@mpsengines.keys.sort end
+    def texformats() @@texformats.keys.sort end
+    def mpsformats() @@mpsformats.keys.sort end
+
+    def defaulttexformats() ['en','nl','mptopdf'] end
+    def defaultmpsformats() ['metafun']           end
+
+    def texmakeextras(format) @@texmakestr[format] || '' end
+    def mpsmakeextras(format) @@mpsmakestr[format] || '' end
+    def texprocextras(format) @@texprocstr[format] || '' end
+    def mpsprocextras(format) @@mpsprocstr[format] || '' end
+
+    def texmethod(format) @@texmethods[str] || @@texmethods['standard'] end
+    def mpsmethod(format) @@mpsmethods[str] || @@mpsmethods['standard'] end
+
+    def runoptions(engine)
+        options = if getvariable('draft') then @@draftoptions[engine] else [] end
+        options = if getvariable('synctex') then @@synctexcoptions[engine] else [] end
+        begin
+            if str = getvariable('passon') then
+                options = [options,str.split(' ')].flatten
+            end
+        rescue
+        end
+        if @@runoptions.key?(engine) then
+            [options,@@runoptions[engine]].flatten.join(' ')
+        else
+            options.join(' ')
+        end
+    end
+
+    # private
+
+    def cleanuplater(name)
+        begin
+            @cleanups.push(File.expand_path(name))
+        rescue
+            @cleanups.push(name)
+        end
+    end
+
+    def openedfile(name)
+        begin
+            f = File.open(name,'w')
+        rescue
+            report("file '#{File.expand_path(name)}' cannot be opened for writing")
+            return nil
+        else
+            cleanuplater(name) if f
+            return f
+        end
+    end
+
+    def prefixed(format,engine)
+        # format
+        case engine
+           when /etex|pdftex|pdfetex|aleph|xetex|luatex/io then
+               "*#{format}"
+           else
+               format
+        end
+    end
+
+    def quoted(str)
+        if str =~ /^[^\"].* / then "\"#{str}\"" else str end
+    end
+
+    def getarrayvariable(str='')
+        str = getvariable(str)
+        if str.class == String then str.split(',') else str.flatten end
+    end
+
+    def validtexformat(str) validsomething(str,@@texformats,'tex')    end
+    def validmpsformat(str) validsomething(str,@@mpsformats,'mp' )    end
+    def validtexengine(str) validsomething(str,@@texengines,'pdftex') end
+    def validmpsengine(str) validsomething(str,@@mpsengines,'mpost' ) end
+
+    def validtexmethod(str) [validsomething(str,@@texmethods)].flatten.first end
+    def validmpsmethod(str) [validsomething(str,@@mpsmethods)].flatten.first end
+
+    def validsomething(str,something,type=nil)
+        if str then
+            list = [str].flatten.collect do |s|
+                if something[s] then
+                    something[s]
+                elsif type && s =~ /\.#{type}$/ then
+                    s
+                else
+                    nil
+                end
+            end .compact.uniq
+            if list.length>0 then
+                if str.class == String then list.first else list end
+            else
+                false
+            end
+        else
+            false
+        end
+    end
+
+    def validbackend(str)
+        if str && @@backends.key?(str) then
+            @@backends[str]
+        else
+            @@backends['standard']
+        end
+    end
+
+    def validprogname(str)
+        if str then
+            [str].flatten.each do |s|
+                s = s.sub(/\.\S*/,"")
+                return @@prognames[s] if @@prognames.key?(s)
+            end
+            return str[0].sub(/\.\S*/,"")
+        else
+            return nil
+        end
+    end
+
+    # we no longer support the & syntax
+
+    def formatflag(engine=nil,format=nil)
+        case getvariable('distribution')
+            when 'standard' then prefix = "--fmt"
+            when /web2c/io  then prefix = web2cformatflag(engine)
+            when /miktex/io then prefix = "--undump"
+                            else return ""
+        end
+        if format then
+            "#{prefix}=#{format.sub(/\.\S+$/,"")}"
+        else
+            prefix
+        end
+    end
+
+    def web2cformatflag(engine=nil)
+        # funny that we've standardized on the fmt suffix (at the cost of
+        # upward compatibility problems) but stuck to the bas/mem/fmt flags
+        if engine then
+            case validmpsengine(engine)
+                when /mpost/ then "-mem"
+                when /mfont/ then "-bas"
+                else              "-fmt"
+            end
+        else
+            "-fmt"
+        end
+    end
+
+    def prognameflag(progname=nil)
+        case getvariable('distribution')
+            when 'standard' then prefix = "-progname"
+            when /web2c/io  then prefix = "-progname"
+            when /miktex/io then prefix = "-alias"
+                            else return ""
+        end
+        if progname and not progname.empty? then
+            "#{prefix}=#{progname}"
+        else
+            prefix
+        end
+    end
+
+    def iniflag() # should go to kpse and kpse should become texenv
+        if Kpse.miktex? then
+            "-initialize"
+        else
+            "--ini"
+        end
+    end
+    def tcxflag(engine)
+        if @@tcxflag[engine] then
+            file = "natural.tcx"
+            if Kpse.miktex? then
+                "-tcx=#{file}"
+            else
+                "-translate-file=#{file}"
+            end
+        else
+            ""
+        end
+    end
+
+    def filestate(file)
+        File.mtime(file).strftime("%d/%m/%Y %H:%M:%S")
+    end
+
+    # will go to context/process context/listing etc
+
+    def contextversion # ook elders gebruiken
+        filename = Kpse.found('context.tex')
+        version = 'unknown'
+        begin
+            if FileTest.file?(filename) && IO.read(filename).match(/\\contextversion\{(\d+\.\d+\.\d+.*?)\}/) then
+                version = $1
+            end
+        rescue
+        end
+        return version
+    end
+
+    def cleanupluafiles
+        File.delete(@@luafiles) rescue false
+    end
+
+    def compileluafiles
+        begin
+            Dir.glob("lua/context/*.luc").each do |luc|
+                File.delete(luc) rescue false
+            end
+        rescue
+        end
+        if data = (IO.readlines(@@luafiles) rescue nil) then
+            report("compiling lua files (using #{File.expand_path(@@luafiles)})")
+            begin
+                FileUtils.makedirs(@@luatarget) rescue false
+                data.each do |line|
+                    luafile = line.chomp
+                    lucfile = File.basename(luafile).gsub(/\..*?$/,'') + ".luc"
+                    if runcommand(["luac","-s","-o",quoted(File.join(Dir.getwd,@@luatarget,lucfile)),quoted(luafile)]) then
+                        report("#{File.basename(luafile)} converted to #{File.basename(lucfile)}")
+                    else
+                        report("#{File.basename(luafile)} not converted to #{File.basename(lucfile)}")
+                    end
+                end
+            rescue
+                report("fatal error in compilation")
+            end
+        else
+            report("no lua compilations needed")
+        end
+        File.delete(@@luafiles) rescue false
+    end
+
+    # we need engine methods
+
+    def makeformats
+
+        checktestversion
+
+        report("using search method '#{Kpse.searchmethod}'")
+        if getvariable('fast') then
+            report('using existing database')
+        else
+            report('updating file database')
+            Kpse.update # obsolete here
+            if getvariable('luatex') then
+                begin
+                    runcommand(["luatools","--generate","--verbose"])
+                rescue
+                    report("run 'luatools --generate' manualy")
+                    exit
+                end
+            end
+        end
+        # goody
+        if getvariable('texformats') == 'standard' then
+            setvariable('texformats',[getvariable('interface')]) unless getvariable('interface').empty?
+        end
+        # prepare
+        texformats = validtexformat(getarrayvariable('texformats'))
+        mpsformats = validmpsformat(getarrayvariable('mpsformats'))
+        texengine  = validtexengine(getvariable('texengine'))
+        mpsengine  = validmpsengine(getvariable('mpsengine'))
+        # save current path
+        savedpath = Dir.getwd
+        # generate tex formats
+        unless texformats || mpsformats then
+            report('provide valid format (name.tex, name.mp, ...) or format id (metafun, en, nl, ...)')
+            setvariable('error','no format specified')
+        end
+        if texformats && texengine then
+            report("using tex engine #{texengine}")
+            texformatpath = if getvariable('local') then '.' else Kpse.formatpath(texengine,true) end
+            # can be empty, to do
+            report("using tex format path #{texformatpath}")
+            Dir.chdir(texformatpath) rescue false
+            if FileTest.writable?(texformatpath) then
+            # from now on we no longer support this; we load
+            # all patterns and if someone wants another
+            # interface language ... cook up a fmt or usr file
+            #
+            #   if texformats.length > 0 then
+            #       makeuserfile
+            #       makeresponsefile
+            #   end
+                if texengine == 'luatex' then
+                    cleanupluafiles
+                    texformats.each do |texformat|
+                        report("generating tex format #{texformat}")
+                        flags = ['--ini','--compile']
+                        flags << '--verbose' if getvariable('verbose')
+                        flags << '--mkii'    if getvariable('mkii')
+                        run_luatools("#{flags.join(" ")} #{texformat}")
+                    end
+                    compileluafiles
+                else
+                    texformats.each do |texformat|
+                        report("generating tex format #{texformat}")
+                        progname = validprogname([getvariable('progname'),texformat,texengine])
+                        runcommand([quoted(texengine),prognameflag(progname),iniflag,tcxflag(texengine),prefixed(texformat,texengine),texmakeextras(texformat)])
+                    end
+                end
+            else
+                report("unable to make format due to lack of permissions")
+                texformatpath = ''
+                setvariable('error','no permissions to write')
+            end
+            if not mpsformats then
+                # we want metafun to be in sync
+                setvariable('mpsformats',defaultmpsformats)
+                mpsformats = validmpsformat(getarrayvariable('mpsformats'))
+            end
+        else
+            texformatpath = ''
+        end
+        # generate mps formats
+        if mpsformats && mpsengine then
+            report("using mp engine #{mpsengine}")
+            mpsformatpath = if getvariable('local') then '.' else Kpse.formatpath(mpsengine,false) end
+            report("using mps format path #{mpsformatpath}")
+            Dir.chdir(mpsformatpath) rescue false
+            if FileTest.writable?(mpsformatpath) then
+                mpsformats.each do |mpsformat|
+                    report("generating mps format #{mpsformat}")
+                    progname = validprogname([getvariable('progname'),mpsformat,mpsengine])
+                #   if not runcommand([quoted(mpsengine),prognameflag(progname),iniflag,tcxflag(mpsengine),runoptions(mpsengine),mpsformat,mpsmakeextras(mpsformat)]) then
+                    if not runcommand([quoted(mpsengine),prognameflag(progname),iniflag,runoptions(mpsengine),mpsformat,mpsmakeextras(mpsformat)]) then
+                        setvariable('error','no format made')
+                    end
+                end
+            else
+                report("unable to make format due to lack of permissions")
+                mpsformatpath = ''
+                setvariable('error','file permission problem')
+            end
+        else
+            mpsformatpath = ''
+        end
+        # check for problems
+        report("")
+        report("tex engine path: #{texformatpath}") unless texformatpath.empty?
+        report("mps engine path: #{mpsformatpath}") unless mpsformatpath.empty?
+        report("")
+        [['fmt','tex'],['mem','mps']].each do |f|
+            [[texformatpath,'global'],[mpsformatpath,'global'],[savedpath,'current']].each do |p|
+                begin
+                    Dir.chdir(p[0])
+                rescue
+                else
+                    Dir.glob("*.#{f[0]}").each do |file|
+                        report("#{f[1]}: #{filestate(file)} > #{File.expand_path(file)} (#{File.size(file)})")
+                    end
+                end
+            end
+        end
+        begin
+            lucdir = File.join(texformatpath,@@luatarget)
+            Dir.chdir(lucdir)
+        rescue
+        else
+            Dir.glob("*.luc").each do |file|
+                report("luc: #{filestate(file)} > #{File.expand_path(file)} (#{File.size(file)})")
+            end
+        end
+        # to be sure, go back to current path
+        begin
+            Dir.chdir(savedpath)
+        rescue
+        end
+        # finalize
+        cleanup
+        report("")
+        reportruntime
+    end
+
+    def checkcontext
+
+        # todo : report texmf.cnf en problems
+
+        # basics
+        report("current distribution: #{Kpse.distribution}")
+        report("context source date: #{contextversion}")
+        formatpaths = Kpse.formatpaths
+        globpattern = "**/{#{formatpaths.join(',')}}/*/*.{fmt,efmt,ofmt,xfmt,mem}"
+        report("format path: #{formatpaths.join(' ')}")
+        # utilities
+        report('start of analysis')
+        results = Array.new
+        # ['texexec','texutil','ctxtools'].each do |program|
+        ['texexec'].each do |program|
+            result = `texmfstart #{program} --help`
+            result.sub!(/.*?(#{program}[^\n]+)\n.*/mi) do $1 end
+            results.push("#{result}")
+        end
+        # formats
+        cleanuptemprunfiles
+        if formats = Dir.glob(globpattern) then
+            formats.sort.each do |name|
+                cleanuptemprunfiles
+                if f = open(tempfilename('tex'),'w') then
+                    # kind of aleph-run-out-of-par safe
+                    f << "\\starttext\n"
+                    f << "  \\relax test \\relax\n"
+                    f << "\\stoptext\n"
+                    f << "\\endinput\n"
+                    f.close
+                    if FileTest.file?(tempfilename('tex')) then
+                        format = File.basename(name)
+                        engine = if name =~ /(pdftex|pdfetex|aleph|xetex|luatex)[\/\\]#{format}/ then $1 else '' end
+                        if engine.empty? then
+                            engineflag = ""
+                        else
+                            engineflag = "--engine=#{$1}"
+                        end
+                        case format
+                            when /cont\-([a-z]+)/ then
+                                interface = $1.sub(/cont\-/,'')
+                                results.push('')
+                                results.push("testing interface #{interface}")
+                                flags = ['--noctx','--process','--batch','--once',"--interface=#{interface}",engineflag]
+                                # result = Kpse.pipescript('texexec',tempfilename,flags)
+                                result = runtexexec([tempfilename], flags, 1)
+                                if FileTest.file?("#{@@temprunfile}.log") then
+                                    logdata = IO.read("#{@@temprunfile}.log")
+                                    if logdata =~ /^\s*This is (.*?)[\s\,]+(.*?)$/moi then
+                                        if validtexengine($1.downcase) then
+                                            results.push("#{$1} #{$2.gsub(/\(format.*$/,'')}".strip)
+                                        end
+                                    end
+                                    if logdata =~ /^\s*(ConTeXt)\s+(.*int:\s+[a-z]+.*?)\s*$/moi then
+                                        results.push("#{$1} #{$2}".gsub(/\s+/,' ').strip)
+                                    end
+                                else
+                                    results.push("format #{format} does not work")
+                                end
+                            when /metafun/ then
+                                # todo
+                            when /mptopdf/ then
+                                # todo
+                        end
+                    else
+                        results.push("error in creating #{tempfilename('tex')}")
+                    end
+                end
+                cleanuptemprunfiles
+            end
+        end
+        report('end of analysis')
+        report
+        results.each do |line|
+            report(line)
+        end
+        cleanuptemprunfiles
+
+    end
+
+    private
+
+    def makeuserfile # not used in luatex (yet)
+        language = getvariable('language')
+        mainlanguage = getvariable('mainlanguage')
+        bodyfont = getvariable('bodyfont')
+        if f = openedfile("cont-fmt.tex") then
+            f << "\\unprotect\n"
+            case language
+                when 'all' then
+                    f << "\\preloadallpatterns\n"
+                when '' then
+                    f << "% no language presets\n"
+                when 'standard'
+                    f << "% using defaults\n"
+                else
+                    languages = language.split(',')
+                    languages.each do |l|
+                        f << "\\installlanguage[\\s!#{l}][\\c!state=\\v!start]\n"
+                    end
+                    mainlanguage = languages.first
+            end
+            unless mainlanguage == 'standard' then
+                f << "\\setupcurrentlanguage[\\s!#{mainlanguage}]\n";
+            end
+            unless bodyfont == 'standard' then
+                # ~ will become obsolete when lmr is used
+                f << "\\definetypescriptsynonym[cmr][#{bodyfont}]"
+                # ~ is already obsolete for some years now
+                f << "\\definefilesynonym[font-cmr][font-#{bodyfont}]\n"
+            end
+            f << "\\protect\n"
+            f << "\\endinput\n"
+            f.close
+        end
+    end
+
+    def makeresponsefile
+        interface = getvariable('interface')
+        if f = openedfile("mult-def.tex") then
+            case interface
+                when 'standard' then
+                    f << "% using default response interface"
+                else
+                    f << "\\def\\currentresponses\{#{interface}\}\n"
+            end
+            f << "\\endinput\n"
+            f.close
+        end
+    end
+
+    private  # will become baee/context
+
+    @@preamblekeys = [
+        ['tex','texengine'],
+        ['engine','texengine'],
+        ['program','texengine'],
+        ['translate','tcxfilter'],
+        ['tcx','tcxfilter'],
+        ['output','backend'],
+        ['mode','mode'],
+        ['ctx','ctxfile'],
+        ['version','contextversion'],
+        ['format','texformats'],
+        ['interface','texformats'],
+    ]
+
+    @@re_utf_bom = /^\357\273\277/o
+
+    def scantexpreamble(filename)
+        begin
+            if FileTest.file?(filename) and tex = File.open(filename,'rb') then
+                bomdone = false
+                while str = tex.gets and str.chomp! do
+                    unless bomdone then
+                        if str.sub!(@@re_utf_bom, '')
+                            report("utf mode forced (bom found)")
+                            setvariable('utfbom',true)
+                        end
+                        bomdone = true
+                    end
+                    if str =~ /^\%\s*(.*)/o then
+                        # we only accept lines with key=value pairs
+                        vars, ok = Hash.new, true
+                        $1.split(/\s+/o).each do |s|
+                            k, v = s.split('=')
+                            if k && v then
+                                vars[k] = v
+                            else
+                                ok = false
+                                break
+                            end
+                        end
+                        if ok then
+                            # we have a valid line
+
+                            @@preamblekeys.each do |v|
+                                setvariable(v[1],vars[v[0]]) if vars.key?(v[0]) && vars[v[0]]
+                            end
+
+if getvariable('given.backend') == "standard" or getvariable('given.backend') == "" then
+    setvariable('backend',@@backends[getvariable('texengine')] || 'standard')
+end
+                            break
+                        end
+                    else
+                        break
+                    end
+                end
+                tex.close
+            end
+        rescue
+            # well, let's not worry too much
+        end
+    end
+
+    def scantexcontent(filename)
+        if FileTest.file?(filename) and tex = File.open(filename,'rb') then
+            while str = tex.gets do
+                case str.chomp
+                    when /^\%/o then
+                        # next
+                #   when /\\(starttekst|stoptekst|startonderdeel|startdocument|startoverzicht)/o then
+                    when /\\(starttekst|stoptekst|startonderdeel|startoverzicht)/o then
+                        setvariable('texformats','nl') ; break
+                    when /\\(stelle|verwende|umgebung|benutze)/o then
+                        setvariable('texformats','de') ; break
+                    when /\\(stel|gebruik|omgeving)/o then
+                        setvariable('texformats','nl') ; break
+                    when /\\(use|setup|environment)/o then
+                        setvariable('texformats','en') ; break
+                    when /\\(usa|imposta|ambiente)/o then
+                        setvariable('texformats','it') ; break
+                    when /(height|width|style)=/o then
+                        setvariable('texformats','en') ; break
+                    when /(hoehe|breite|schrift)=/o then
+                        setvariable('texformats','de') ; break
+                    when /(hoogte|breedte|letter)=/o then
+                        setvariable('texformats','nl') ; break
+                    when /(altezza|ampiezza|stile)=/o then
+                        setvariable('texformats','it') ; break
+                    when /externfiguur/o then
+                        setvariable('texformats','nl') ; break
+                    when /externalfigure/o then
+                        setvariable('texformats','en') ; break
+                    when /externeabbildung/o then
+                        setvariable('texformats','de') ; break
+                    when /figuraesterna/o then
+                        setvariable('texformats','it') ; break
+                end
+            end
+            tex.close
+        end
+
+    end
+
+    private # will become base/context
+
+    def pushresult(filename,resultname)
+        fname = File.unsuffixed(filename)
+        rname = File.unsuffixed(resultname)
+        if ! rname.empty? && (rname != fname) then
+            report("outputfile #{rname}")
+            ['tuo','tuc','log','dvi','pdf'].each do |s|
+                File.silentrename(File.suffixed(fname,s),File.suffixed('texexec',s))
+            end
+            ['tuo','tuc'].each do |s|
+                File.silentrename(File.suffixed(rname,s),File.suffixed(fname,s)) if FileTest.file?(File.suffixed(rname,s))
+            end
+        end
+    end
+
+    def popresult(filename,resultname)
+        fname = File.unsuffixed(filename)
+        rname = File.unsuffixed(resultname)
+        if ! rname.empty? && (rname != fname) then
+            report("renaming #{fname} to #{rname}")
+            ['tuo','tuc','log','dvi','pdf'].each do |s|
+                File.silentrename(File.suffixed(fname,s),File.suffixed(rname,s))
+            end
+            report("restoring #{fname}")
+            unless $fname == 'texexec' then
+                ['tuo','tuc','log','dvi','pdf'].each do |s|
+                    File.silentrename(File.suffixed('texexec',s),File.suffixed(fname,s))
+                end
+            end
+        end
+    end
+
+    def makestubfile(rawname,rawbase,forcexml=false)
+        if tmp = openedfile(File.suffixed(rawbase,'run')) then
+            tmp << "\\starttext\n"
+            if forcexml then
+                # tmp << checkxmlfile(rawname)
+                if getvariable('mkiv') then
+                    tmp << "\\xmlprocess{\\xmldocument}{#{rawname}}{}\n"
+                else
+                    tmp << "\\processXMLfilegrouped{#{rawname}}\n"
+                end
+            else
+                tmp << "\\processfile{#{rawname}}\n"
+            end
+            tmp << "\\stoptext\n"
+            tmp.close
+            return "run"
+        else
+            return File.splitname(rawname)[1]
+        end
+    end
+
+    # def checkxmlfile(rawname)
+        # tmp = ''
+        # if FileTest.file?(rawname) && (xml = File.open(rawname)) then
+            # xml.each do |line|
+                # case line
+                    # when /<\?context\-directive\s+(\S+)\s+(\S+)\s+(\S+)\s*(.*?)\s*\?>/o then
+                        # category, key, value, rest = $1, $2, $3, $4
+                        # case category
+                            # when 'job' then
+                                # case key
+                                    # when 'control' then
+                                        # setvariable(value,if rest.empty? then true else rest end)
+                                    # when 'mode', 'modes' then
+                                        # tmp << "\\enablemode[#{value}]\n"
+                                    # when 'stylefile', 'environment' then
+                                        # tmp << "\\environment #{value}\n"
+                                    # when 'module' then
+                                        # tmp << "\\usemodule[#{value}]\n"
+                                    # when 'interface' then
+                                        # contextinterface = value
+                                    # when 'ctxfile' then
+                                        # setvariable('ctxfile', value)
+                                        # report("using source driven ctxfile #{value}")
+                                # end
+                        # end
+                    # when /<[a-z]+/io then # beware of order, first pi test
+                        # break
+                # end
+            # end
+            # xml.close
+        # end
+        # return tmp
+    # end
+
+    def extendvariable(name,value)
+        set = getvariable(name).split(',')
+        set << value
+        str = set.uniq.join(',')
+        setvariable(name,str)
+    end
+
+    def checkxmlfile(rawname)
+        if FileTest.file?(rawname) && (xml = File.open(rawname,'rb')) then
+            xml.each do |line|
+                case line
+                    when /<\?context\-directive\s+(\S+)\s+(\S+)\s+(\S+)\s*(.*?)\s*\?>/o then
+                        category, key, value, rest = $1, $2, $3, $4
+                        case category
+                            when 'job' then
+                                case key
+                                    when 'control' then
+                                        setvariable(value,if rest.empty? then true else rest end)
+                                    when /^(mode)(s|)$/ then
+                                        extendvariable('modes',value)
+                                    when /^(stylefile|environment)(s|)$/ then
+                                        extendvariable('environments',value)
+                                    when /^(use|)(module)(s|)$/ then
+                                        extendvariable('usemodules',value)
+                                    when /^(filter)(s|)$/ then
+                                        extendvariable('filters',value)
+                                    when 'interface' then
+                                        contextinterface = value
+                                    when 'ctxfile' then
+                                        setvariable('ctxfile', value)
+                                        report("using source driven ctxfile #{value}")
+                                end
+                        end
+                    when /<[a-z]+/io then # beware of order, first pi test
+                        break
+                end
+            end
+            xml.close
+        end
+    end
+
+end
+
+class TEX
+
+    def timedrun(delay, &block)
+        delay = delay.to_i rescue 0
+        if delay > 0 then
+            begin
+                report("job started with timeout '#{delay}'")
+                timeout(delay) do
+                    yield block
+                end
+            rescue TimeoutError
+                report("job aborted due to timeout '#{delay}'")
+                setvariable('error','timeout')
+            rescue
+                report("job aborted due to error")
+                setvariable('error','fatal error')
+            else
+                report("job finished within timeout '#{delay}'")
+            end
+        else
+            yield block
+        end
+    end
+
+    def processtex # much to do: mp, xml, runs etc
+        setvariable('texformats',[getvariable('interface')]) unless getvariable('interface').empty?
+        getarrayvariable('files').each do |filename|
+            setvariable('filename',filename)
+            report("processing document '#{filename}'")
+            timedrun(getvariable('timeout')) do
+                processfile
+            end
+        end
+        reportruntime
+    end
+
+    def processmptex
+        getarrayvariable('files').each do |filename|
+            setvariable('filename',filename)
+            report("processing graphic '#{filename}'")
+            runtexmp(filename)
+        end
+        reportruntime
+    end
+
+    private
+
+    def load_map_files(filename) # tui basename
+        # c \usedmapfile{=}{lm-texnansi}
+        begin
+            str = ""
+            IO.read(filename).scan(/^c\s+\\usedmapfile\{(.*?)\}\{(.*?)\}\s*$/o) do
+                str << "\\loadmapfile[#{$2}.map]\n"
+            end
+        rescue
+            return ""
+        else
+            return str
+        end
+    end
+
+    public
+
+    # def run_luatools(args)
+        # dirty trick: we know that the lua path is relative to the ruby path; of course this
+        # will not work well when stubs are used
+        # [(ENV["_CTX_K_S_texexec_"] or ENV["_CTX_K_S_THREAD_"] or ENV["TEXMFSTART.THREAD"]), File.dirname($0)].each do |path|
+            # if path then
+                # script = "#{path}/../lua/luatools.lua"
+                # if FileTest.file?(script) then
+                    # return runcommand("luatex --luaonly #{script} #{args}")
+                # end
+            # end
+        # end
+        # return runcommand("texmfstart luatools #{args}")
+    # end
+
+    def run_luatools(args)
+        return runcommand("luatools #{args}")
+    end
+
+    def processmpgraphic
+        getarrayvariable('files').each do |filename|
+            setvariable('filename',filename)
+            report("processing graphic '#{filename}'")
+            runtexmp(filename,'',false) # no purge
+            mapspecs = load_map_files(File.suffixed(filename,'temp','tui'))
+            unless getvariable('keep') then
+                # not enough: purge_mpx_files(filename)
+                Dir.glob(File.suffixed(filename,'temp*','*')).each do |fname|
+                    File.delete(fname) unless File.basename(filename) == File.basename(fname)
+                end
+            end
+            begin
+                data = IO.read(File.suffixed(filename,'log'))
+                basename = filename.sub(/\.mp$/, '')
+                if data =~ /output files* written\:\s*(.*)$/moi then
+                    files, number, range, list = $1.split(/\s+/), 0, false, []
+                    files.each do |fname|
+                        if fname =~ /^.*\.(\d+)$/ then
+                            if range then
+                                (number+1 .. $1.to_i).each do |i|
+                                    list << i
+                                end
+                                range = false
+                            else
+                                number = $1.to_i
+                                list << number
+                            end
+                        elsif fname =~ /\.\./ then
+                            range = true
+                        else
+                            range = false
+                            next
+                        end
+                    end
+                    begin
+                        if getvariable('combine') then
+                            fullname = "#{basename}.#{number}"
+                            File.open("texexec.tex",'w') do |f|
+                                f << "\\setupoutput[pdftex]\n"
+                                f << "\\setupcolors[state=start]\n"
+                                f << mapspecs
+                                f << "\\starttext\n"
+                                list.each do |number|
+                                    f << "\\startTEXpage\n"
+                                    f << "\\convertMPtoPDF{#{fullname}}{1}{1}"
+                                    f << "\\stopTEXpage\n"
+                                end
+                                f << "\\stoptext\n"
+                            end
+                            report("converting graphic '#{fullname}'")
+                            runtex("texexec.tex")
+                            pdffile = File.suffixed(basename,'pdf')
+                            File.silentrename("texexec.pdf",pdffile)
+                            report ("#{basename}.* converted to #{pdffile}")
+                        else
+                            list.each do |number|
+                                begin
+                                    fullname = "#{basename}.#{number}"
+                                    File.open("texexec.tex",'w') do |f|
+                                        f << "\\setupoutput[pdftex]\n"
+                                        f << "\\setupcolors[state=start]\n"
+                                        f << mapspecs
+                                        f << "\\starttext\n"
+                                        f << "\\startTEXpage\n"
+                                        f << "\\convertMPtoPDF{#{fullname}}{1}{1}"
+                                        f << "\\stopTEXpage\n"
+                                        f << "\\stoptext\n"
+                                    end
+                                    report("converting graphic '#{fullname}'")
+                                    runtex("texexec.tex")
+                                    if files.length>1 then
+                                        pdffile = File.suffixed(basename,number.to_s,'pdf')
+                                    else
+                                        pdffile = File.suffixed(basename,'pdf')
+                                    end
+                                    File.silentrename("texexec.pdf",pdffile)
+                                    report ("#{fullname} converted to #{pdffile}")
+                                end
+                            end
+                        end
+                    rescue
+                        report ("error when converting #{fullname} (#{$!})")
+                    end
+                end
+            rescue
+                report("error in converting #{filename}")
+            end
+        end
+        reportruntime
+    end
+
+    def processmpstatic
+        if filename = getvariable('filename') then
+            filename += ".mp" unless filename =~ /\..+?$/
+            if FileTest.file?(filename) then
+                begin
+                    data = IO.read(filename)
+                    File.open("texexec.tex",'w') do |f|
+                        f << "\\setupoutput[pdftex]\n"
+                        f << "\\setupcolors[state=start]\n"
+                        data.sub!(/^%mpenvironment\:\s*(.*?)$/moi) do
+                            f << $1
+                            "\n"
+                        end
+                        f << "\\starttext\n"
+                        f << "\\startMPpage\n"
+                        f << data.gsub(/end\.*\s*$/m, '') # a bit of a hack
+                        f << "\\stopMPpage\n"
+                        f << "\\stoptext\n"
+                    end
+                    report("converting static '#{filename}'")
+                    runtex("texexec.tex")
+                    pdffile = File.suffixed(filename,'pdf')
+                    File.silentrename("texexec.pdf",pdffile)
+                    report ("#{filename} converted to #{pdffile}")
+                rescue
+                    report("error in converting #{filename} (#{$!}")
+                end
+            end
+        end
+        reportruntime
+    end
+
+    def processmpxtex
+        getarrayvariable('files').each do |filename|
+            setvariable('filename',filename)
+            report("processing text of graphic '#{filename}'")
+            processmpx(filename,false,true,true)
+        end
+        reportruntime
+    end
+
+    def deleteoptionfile(rawname)
+        ['top','top.keep'].each do |suffix|
+            begin
+                File.delete(File.suffixed(rawname,suffix))
+            rescue
+            end
+        end
+    end
+
+    def makeoptionfile(rawname, jobname, jobsuffix, finalrun, fastdisabled, kindofrun, currentrun=1)
+        begin
+            # jobsuffix = orisuffix
+            if topname = File.suffixed(rawname,'top') and opt = File.open(topname,'w') then
+                report("writing option file #{topname}")
+                # local handies
+                opt << "\% #{topname}\n"
+                opt << "\\unprotect\n"
+                #
+                # feedback and basic control
+                #
+                if getvariable('batchmode') then
+                    opt << "\\batchmode\n"
+                end
+                if getvariable('nonstopmode') then
+                    opt << "\\nonstopmode\n"
+                end
+                if getvariable('paranoid') then
+                    opt << "\\def\\maxreadlevel{1}\n"
+                end
+                if getvariable('nomapfiles') then
+                    opt << "\\disablemapfiles\n"
+                end
+                if getvariable('nompmode') || getvariable('nomprun') || getvariable('automprun') then
+                    opt << "\\runMPgraphicsfalse\n"
+                end
+                if getvariable('utfbom') then
+                    opt << "\\enableregime[utf]"
+                end
+                progname = validprogname(['metafun']) # [getvariable('progname'),mpsformat,mpsengine]
+                opt << "\\def\\MPOSTformatswitch\{#{prognameflag(progname)} #{formatflag('mpost')}=\}\n"
+                #
+                # process info
+                #
+                opt << "\\setupsystem[\\c!n=#{kindofrun},\\c!m=#{currentrun}]\n"
+                if (str = File.unixfied(getvariable('modefile'))) && ! str.empty? then
+                    opt << "\\readlocfile{#{str}}{}{}\n"
+                end
+                if (str = File.unixfied(getvariable('result'))) && ! str.empty? then
+                    opt << "\\setupsystem[file=#{str}]\n"
+                elsif (str = getvariable('suffix')) && ! str.empty? then
+                    opt << "\\setupsystem[file=#{jobname}.#{str}]\n"
+                end
+                opt << "\\setupsystem[\\c!method=2]\n" # 1=oldtexexec 2=newtexexec (obsolete)
+                opt << "\\setupsystem[\\c!type=#{Tool.ruby_platform()}]\n"
+                if (str = File.unixfied(getvariable('path'))) && ! str.empty? then
+                    opt << "\\usepath[#{str}]\n" unless str.empty?
+                end
+                if (str = getvariable('mainlanguage').downcase) && ! str.empty? && ! str.standard? then
+                    opt << "\\setuplanguage[#{str}]\n"
+                end
+                if (str = getvariable('arguments')) && ! str.empty? then
+                    opt << "\\setupenv[#{str}]\n"
+                end
+                if (str = getvariable('setuppath')) && ! str.empty? then
+                    opt << "\\setupsystem[\\c!directory=\{#{str}\}]\n"
+                end
+                if (str = getvariable('randomseed')) && ! str.empty? then
+                    report("using randomseed #{str}")
+                    opt << "\\setupsystem[\\c!random=#{str}]\n"
+                end
+                if (str = getvariable('input')) && ! str.empty? then
+                    opt << "\\setupsystem[inputfile=#{str}]\n"
+                else
+                    opt << "\\setupsystem[inputfile=#{rawname}]\n"
+                end
+                #
+                # modes
+                #
+                # we handle both "--mode" and "--modes", else "--mode" is mapped onto "--modefile"
+                if (str = getvariable('modes')) && ! str.empty? then
+                    opt << "\\enablemode[#{str}]\n"
+                end
+                if (str = getvariable('mode')) && ! str.empty? then
+                    opt << "\\enablemode[#{str}]\n"
+                end
+                #
+                # options
+                #
+                opt << "\\startsetups *runtime:options\n"
+                if str = validbackend(getvariable('backend')) then
+                    opt << "\\setupoutput[#{str}]\n"
+                elsif str = validbackend(getvariable('output')) then
+                    opt << "\\setupoutput[#{str}]\n"
+                end
+                if getvariable('color') then
+                    opt << "\\setupcolors[\\c!state=\\v!start]\n"
+                end
+                if (str = getvariable('separation')) && ! str.empty? then
+                    opt << "\\setupcolors[\\c!split=#{str}]\n"
+                end
+                if (str = getvariable('paperformat')) && ! str.empty? && ! str.standard? then
+                    if str =~ /^([a-z]+\d+)([a-z]+\d+)$/io then # A5A4 A4A3 A2A1 ...
+                        opt << "\\setuppapersize[#{$1.upcase}][#{$2.upcase}]\n"
+                    else # ...*...
+                        pf = str.upcase.split(/[x\*]/o)
+                        pf << pf[0] if pf.size == 1
+                        opt << "\\setuppapersize[#{pf[0]}][#{pf[1]}]\n"
+                    end
+                end
+                if (str = getvariable('background')) && ! str.empty? then
+                    opt << "\\defineoverlay[whatever][{\\externalfigure[#{str}][\\c!factor=\\v!max]}]\n"
+                    opt << "\\setupbackgrounds[\\v!page][\\c!background=whatever]\n"
+                end
+                if getvariable('centerpage') then
+                    opt << "\\setuplayout[\\c!location=\\v!middle,\\c!marking=\\v!on]\n"
+                end
+                if getvariable('noarrange') then
+                    opt << "\\setuparranging[\\v!disable]\n"
+                elsif getvariable('arrange') then
+                    arrangement = Array.new
+                    if finalrun then
+                        arrangement << "\\v!doublesided" unless getvariable('noduplex')
+                        case getvariable('printformat')
+                            when ''         then arrangement << "\\v!normal"
+                            when /.*up/oi   then arrangement << ["2UP","\\v!rotated"]
+                            when /.*down/oi then arrangement << ["2DOWN","\\v!rotated"]
+                            when /.*side/oi then arrangement << ["2SIDE","\\v!rotated"]
+                        end
+                    else
+                        arrangement << "\\v!disable"
+                    end
+                    opt << "\\setuparranging[#{arrangement.flatten.join(',')}]\n" if arrangement.size > 0
+                end
+                if (str = getvariable('pages')) && ! str.empty? then
+                    if str.downcase == 'odd' then
+                        opt << "\\chardef\\whichpagetoshipout=1\n"
+                    elsif str.downcase == 'even' then
+                        opt << "\\chardef\\whichpagetoshipout=2\n"
+                    else
+                        pagelist = Array.new
+                        str.split(/\,/).each do |page|
+                            pagerange = page.split(/\D+/o)
+                            if pagerange.size > 1 then
+                                pagerange.first.to_i.upto(pagerange.last.to_i) do |p|
+                                    pagelist << p.to_s
+                                end
+                            else
+                                pagelist << page
+                            end
+                        end
+                        opt << "\\def\\pagestoshipout\{#{pagelist.join(',')}\}\n";
+                    end
+                end
+                opt << "\\stopsetups\n"
+                #
+                # styles and modules
+                #
+                opt << "\\startsetups *runtime:modules\n"
+                begin getvariable('filters'     ).split(',').uniq.each do |f| opt << "\\useXMLfilter[#{f}]\n" end ; rescue ; end
+                begin getvariable('usemodules'  ).split(',').uniq.each do |m| opt << "\\usemodule   [#{m}]\n" end ; rescue ; end
+                begin getvariable('environments').split(',').uniq.each do |e| opt << "\\environment  #{e} \n" end ; rescue ; end
+                opt << "\\stopsetups\n"
+                #
+                opt << "\\protect \\endinput\n"
+                #
+                opt.close
+           else
+                report("unable to write option file #{topname}")
+            end
+        rescue
+            report("fatal error in writing option file #{topname} (#{$!})")
+        end
+    end
+
+    def takeprecautions
+        ENV['MPXCOMAND'] = '0' # else loop
+        if getvariable('paranoid') then
+            ENV['SHELL_ESCAPE'] = ENV['SHELL_ESCAPE'] || 'f'
+            ENV['OPENOUT_ANY']  = ENV['OPENOUT_ANY']  || 'p'
+            ENV['OPENIN_ANY']   = ENV['OPENIN_ANY']   || 'p'
+        elsif getvariable('notparanoid') then
+            ENV['SHELL_ESCAPE'] = ENV['SHELL_ESCAPE'] || 't'
+            ENV['OPENOUT_ANY']  = ENV['OPENOUT_ANY']  || 'a'
+            ENV['OPENIN_ANY']   = ENV['OPENIN_ANY']   || 'a'
+        end
+        if ENV['OPENIN_ANY'] && (ENV['OPENIN_ANY'] == 'p') then # first test redundant
+            setvariable('paranoid', true)
+        end
+        if ENV.key?('SHELL_ESCAPE') && (ENV['SHELL_ESCAPE'] == 'f') then
+            setvariable('automprun',true)
+        end
+        done = false
+        ['TXRESOURCES','MPRESOURCES','MFRESOURCES'].each do |res|
+            [getvariable('runpath'),getvariable('path')].each do |pat|
+                unless pat.empty? then
+                    if ENV.key?(res) then
+                        # ENV[res] = if ENV[res].empty? then pat else pat + ":" + ENV[res] end
+if ENV[res].empty? then
+    ENV[res] = pat
+elsif ENV[res] == pat || ENV[res] =~ /^#{pat}\:/ || ENV[res] =~ /\:#{pat}\:/ then
+    # skip
+else
+    ENV[res] = pat + ":" + ENV[res]
+end
+                    else
+                        ENV[res] = pat
+                    end
+                    report("setting #{res} to #{ENV[res]}") unless done
+                end
+            end
+            done = true
+        end
+    end
+
+    def checktestversion
+        #
+        # one can set TEXMFALPHA and TEXMFBETA for test versions
+        # but keep in mind that the format as well as the test files
+        # then need the --alpha or --beta flag
+        #
+        done, tree = false, ''
+        ['alpha', 'beta'].each do |what|
+            if getvariable(what) then
+                if ENV["TEXMF#{what.upcase}"] then
+                    done, tree = true, ENV["TEXMF#{what.upcase}"]
+                elsif ENV["TEXMFLOCAL"] then
+                    done, tree = true, File.join(File.dirname(ENV['TEXMFLOCAL']), "texmf-#{what}")
+                end
+            end
+            break if done
+        end
+        if done then
+            tree = tree.strip
+            ENV['TEXMFPROJECT'] = tree
+            report("using test tree '#{tree}'")
+            ['MP', 'MF', 'TX'].each do |ctx|
+                ENV['CTXDEV#{ctx}PATH'] = ''
+            end
+            unless (FileTest.file?(File.join(tree,'ls-r')) || FileTest.file?(File.join(tree,'ls-R'))) then
+                report("no ls-r/ls-R file for tree '#{tree}' (run: mktexlsr #{tree})")
+            end
+        end
+        # puts `kpsewhich --expand-path=$TEXMF`
+        # exit
+    end
+
+    def runtex(filename)
+        checktestversion
+        texengine = validtexengine(getvariable('texengine'))
+        texformat = validtexformat(getarrayvariable('texformats').first)
+        report("tex engine: #{texengine}")
+        report("tex format: #{texformat}")
+        if texengine && texformat then
+            fixbackendvars(@@mappaths[texengine])
+            if texengine == "luatex" then
+                # currently we use luatools to start luatex but some day we should
+                # find a clever way to directly call luatex (problem is that we need
+                # to feed the explicit location of the format and lua initialization
+                # file)
+                run_luatools("--fmt=#{texformat} #{filename}")
+            else
+                progname = validprogname([getvariable('progname'),texformat,texengine])
+                runcommand([quoted(texengine),prognameflag(progname),formatflag(texengine,texformat),tcxflag(texengine),runoptions(texengine),filename,texprocextras(texformat)])
+            end
+            # true
+        else
+            false
+        end
+    end
+
+    def runmp(mpname,mpx=false)
+        checktestversion
+        mpsengine = validmpsengine(getvariable('mpsengine'))
+        mpsformat = validmpsformat(getarrayvariable('mpsformats').first)
+        if mpsengine && mpsformat then
+            ENV["MPXCOMMAND"] = "0" unless mpx
+            progname = validprogname([getvariable('progname'),mpsformat,mpsengine])
+            mpname.gsub!(/\.mp$/,"") # temp bug in mp
+        #   runcommand([quoted(mpsengine),prognameflag(progname),formatflag(mpsengine,mpsformat),tcxflag(mpsengine),runoptions(mpsengine),mpname,mpsprocextras(mpsformat)])
+            runcommand([quoted(mpsengine),prognameflag(progname),formatflag(mpsengine,mpsformat),runoptions(mpsengine),mpname,mpsprocextras(mpsformat)])
+            true
+        else
+            false
+        end
+    end
+
+    def runtexmp(filename,filetype='',purge=true)
+        checktestversion
+        mpname = File.suffixed(filename,filetype,'mp')
+        if File.atleast?(mpname,10) then
+            # first run needed
+            File.silentdelete(File.suffixed(mpname,'mpt'))
+            doruntexmp(mpname,nil,true,purge)
+            mpgraphics = checkmpgraphics(mpname)
+            mplabels = checkmplabels(mpname)
+            if mpgraphics || mplabels then
+                # second run needed
+                doruntexmp(mpname,mplabels,true,purge)
+            else
+                # no labels
+            end
+        end
+    end
+
+    def runtexmpjob(filename,filetype='')
+        checktestversion
+        mpname = File.suffixed(filename,filetype,'mp')
+        if File.atleast?(mpname,25) && (data = File.silentread(mpname)) then
+            textranslation = if data =~ /^\%\s+translate.*?\=([\w\d\-]+)/io then $1 else '' end
+            mpjobname = if data =~ /collected graphics of job \"(.+?)\"/io then $1 else '' end
+            if ! mpjobname.empty? and File.unsuffixed(filename) =~ /#{mpjobname}/ then # don't optimize
+                options = Array.new
+                options.push("--mptex")
+                options.push("--nomp")
+                options.push("--mpyforce") if getvariable('forcempy') || getvariable('mpyforce')
+                options.push("--translate=#{textranslation}") unless textranslation.empty?
+                options.push("--batch") if getvariable('batchmode')
+                options.push("--nonstop") if getvariable('nonstopmode')
+                options.push("--output=ps") # options.push("--dvi")
+                options.push("--nobackend")
+                return runtexexec(mpname,options,2)
+            end
+        end
+        return false
+    end
+
+    def runtexutil(filename=[], options=['--ref','--ij','--high'], old=false)
+        [filename].flatten.each do |fname|
+            if old then
+                Kpse.runscript('texutil',fname,options)
+            else
+                begin
+                    logger = Logger.new('TeXUtil')
+                    if tu = TeXUtil::Converter.new(logger) and tu.loaded(fname) then
+                        ok = tu.processed && tu.saved && tu.finalized
+                    end
+                rescue
+                    Kpse.runscript('texutil',fname,options)
+                end
+            end
+        end
+    end
+
+    def runluacheck(jobname)
+        if false then
+            # test-pos.tex / 6 meg tua file: 18.6 runtime
+            old, new = File.suffixed(jobname,'tua'), File.suffixed(jobname,'tuc')
+            if FileTest.file?(old) then
+                report("converting #{old} into #{new}")
+                system("luac -s -o #{new} #{old}")
+            end
+        else
+            # test-pos.tex / 6 meg tua file: 17.5 runtime
+            old, new = File.suffixed(jobname,'tua'), File.suffixed(jobname,'tuc')
+            if FileTest.file?(old) then
+                report("renaming #{old} into #{new}")
+                File.rename(old,new) rescue false
+            end
+        end
+    end
+
+    # 1=tex 2=mptex 3=mpxtex 4=mpgraphic 5=mpstatic
+
+    def runtexexec(filename=[], options=[], mode=nil)
+        begin
+            if mode and job = TEX.new(@logger) then
+                options.each do |option|
+                    case option
+                        when /^\-*(.*?)\=(.*)$/o then
+                            job.setvariable($1,$2)
+                        when /^\-*(.*?)$/o then
+                            job.setvariable($1,true)
+                    end
+                end
+                job.setvariable("files",filename)
+                case mode
+                    when 1 then job.processtex
+                    when 2 then job.processmptex
+                    when 3 then job.processmpxtex
+                    when 4 then job.processmpgraphic
+                    when 5 then job.processmpstatic
+                end
+                job.inspect && Kpse.inspect if getvariable('verbose')
+                return true
+            else
+                Kpse.runscript('texexec',filename,options)
+            end
+        rescue
+            Kpse.runscript('texexec',filename,options)
+        end
+    end
+
+    def fixbackendvars(backend)
+        if backend then
+            ENV['backend']     = backend ;
+            ENV['progname']    = backend unless validtexengine(backend)
+            ENV['TEXFONTMAPS'] = ['.',"\$TEXMF/fonts/{data,map}/{#{backend},pdftex,dvips,}//",'./fonts//'].join_path
+            report("fixing backend map path for #{backend}: #{ENV['TEXFONTMAPS']}") if getvariable('verbose')
+        else
+            report("unable to fix backend map path") if getvariable('verbose')
+        end
+    end
+
+    def runbackend(rawname)
+        unless getvariable('nobackend') then
+            case validbackend(getvariable('backend'))
+                when 'dvipdfmx' then
+                    fixbackendvars('dvipdfm')
+                    runcommand("dvipdfmx -d 4 -V 5 #{File.unsuffixed(rawname)}")
+                when 'xetex'    then
+                    # xetex now runs its own backend
+                    xdvfile = File.suffixed(rawname,'xdv')
+                    if FileTest.file?(xdvfile) then
+                        fixbackendvars('dvipdfm')
+                        runcommand("xdvipdfmx -q -d 4 -V 5 -E #{xdvfile}")
+                    end
+                when 'xdv2pdf' then
+                    xdvfile = File.suffixed(rawname,'xdv')
+                    if FileTest.file?(xdvfile) then
+                        fixbackendvars('xdv2pdf')
+                        runcommand("xdv2pdf #{xdvfile}")
+                    end
+                when 'dvips' then
+                    fixbackendvars('dvips')
+                    mapfiles = ''
+                    begin
+                        if tuifile = File.suffixed(rawname,'tui') and FileTest.file?(tuifile) then
+                            IO.read(tuifile).scan(/^c \\usedmapfile\{.\}\{(.*?)\}\s*$/o) do
+                                mapfiles += "-u +#{$1} " ;
+                            end
+                        end
+                    rescue
+                        mapfiles = ''
+                    end
+                    runcommand("dvips #{mapfiles} #{File.unsuffixed(rawname)}")
+                when 'pdftex'   then
+                    # no need for postprocessing
+                else
+                    report("no postprocessing needed")
+            end
+        end
+    end
+
+    def processfile
+
+        takeprecautions
+        report("using search method '#{Kpse.searchmethod}'") if getvariable('verbose')
+
+        rawname    = getvariable('filename')
+        jobname    = getvariable('filename')
+
+        if getvariable('autopath') then
+            jobname = File.basename(jobname)
+            inppath = File.dirname(jobname)
+        else
+            inppath = ''
+        end
+
+        jobname, jobsuffix = File.splitname(jobname,'tex')
+
+        jobname = File.unixfied(jobname)
+        inppath = File.unixfied(inppath)
+
+        orisuffix = jobsuffix # still needed ?
+
+        if jobsuffix =~ /^(htm|html|xhtml|xml|fo|fox|rlg|exa)$/io then
+            setvariable('forcexml',true)
+        end
+
+        dummyfile = false
+
+        # fuzzy code snippet: (we kunnen kpse: prefix gebruiken)
+
+        unless FileTest.file?(File.suffixed(jobname,jobsuffix)) then
+            if FileTest.file?(rawname + '.tex') then
+                jobname = rawname.dup
+                jobsuffix  = 'tex'
+            end
+        end
+
+        # we can have funny names, like 2005.10.10 (given without suffix)
+
+        rawname = jobname + '.' + jobsuffix
+        rawpath = File.dirname(rawname)
+        rawbase = File.basename(rawname)
+
+        unless FileTest.file?(rawname) then
+            inppath.split(',').each do |ip|
+                break if dummyfile = FileTest.file?(File.join(ip,rawname))
+            end
+        end
+
+        forcexml   = getvariable('forcexml')
+
+        if dummyfile || forcexml then # after ctx?
+            jobsuffix = makestubfile(rawname,rawbase,forcexml)
+            checkxmlfile(rawname)
+        end
+
+        # preprocess files
+
+        unless getvariable('noctx') then
+            ctx = CtxRunner.new(rawname,@logger)
+            if pth = getvariable('path') then
+                pth.split(',').each do |p|
+                    ctx.register_path(p)
+                end
+            end
+            if getvariable('ctxfile').empty? then
+                if rawname == rawbase then
+                    ctx.manipulate(File.suffixed(rawname,'ctx'),'jobname.ctx')
+                else
+                    ctx.manipulate(File.suffixed(rawname,'ctx'),File.join(rawpath,'jobname.ctx'))
+                end
+            else
+                ctx.manipulate(File.suffixed(getvariable('ctxfile'),'ctx'))
+            end
+            ctx.savelog(File.suffixed(rawbase,'ctl'))
+
+            envs  = ctx.environments
+            mods  = ctx.modules
+            flags = ctx.flags
+            mdes  = ctx.modes
+
+            flags.each do |f|
+                f.sub!(/^\-+/,'')
+                if f =~ /^(.*?)=(.*)$/ then
+                    setvariable($1,$2)
+                else
+                    setvariable(f,true)
+                end
+            end
+
+            report("using flags #{flags.join(' ')}") if flags.size > 0
+
+            # merge environment and module specs
+
+            envs << getvariable('environments') unless getvariable('environments').empty?
+            mods << getvariable('usemodules')   unless getvariable('usemodules')  .empty?
+            mdes << getvariable('modes')        unless getvariable('modes')       .empty?
+
+            envs = envs.uniq.join(',')
+            mods = mods.uniq.join(',')
+            mdes = mdes.uniq.join(',')
+
+            report("using search method '#{Kpse.searchmethod}'") if getvariable('verbose')
+
+            report("using environments #{envs}") if envs.length > 0
+            report("using modules #{mods}")      if mods.length > 0
+            report("using modes #{mdes}")        if mdes.length > 0
+
+            setvariable('environments', envs)
+            setvariable('usemodules',   mods)
+            setvariable('modes',        mdes)
+        end
+
+        # end of preprocessing and merging
+
+        setvariable('nomprun',true) if orisuffix == 'mpx' # else cylic run
+        PDFview.setmethod('xpdf') if getvariable('xpdf')
+        PDFview.closeall if getvariable('autopdf')
+
+        runonce    = getvariable('once')
+        finalrun   = getvariable('final') || (getvariable('arrange') && ! getvariable('noarrange'))
+        suffix     = getvariable('suffix')
+        result     = getvariable('result')
+        globalfile = getvariable('globalfile')
+        forcexml   = getvariable('forcexml') # can be set in ctx file
+
+if dummyfile || forcexml then # after ctx?
+    jobsuffix = makestubfile(rawname,rawbase,forcexml)
+    checkxmlfile(rawname)
+end
+
+        result     = File.unixfied(result)
+
+        if globalfile || FileTest.file?(rawname) then
+
+            if not dummyfile and not globalfile and not forcexml then
+                scantexpreamble(rawname)
+                scantexcontent(rawname) if getvariable('texformats').standard?
+            end
+            result = File.suffixed(rawname,suffix) unless suffix.empty?
+
+            pushresult(rawbase,result)
+
+            method = validtexmethod(validtexformat(getvariable('texformats')))
+
+            report("tex processing method: #{method}")
+
+            case method
+
+                when 'context' then
+                    if getvariable('simplerun') || runonce then
+                        makeoptionfile(rawbase,jobname,orisuffix,true,true,3,1) unless getvariable('nooptionfile')
+                        ok = runtex(if dummyfile || forcexml then rawbase else rawname end)
+                        if ok then
+                            ok = runtexutil(rawbase) if getvariable('texutil') || getvariable('forcetexutil')
+                            runluacheck(rawbase)
+                            runbackend(rawbase)
+                            popresult(rawbase,result)
+                        end
+                        if getvariable('keep') then
+                            ['top','log','run'].each do |suffix|
+                                File.silentrename(File.suffixed(rawbase,suffix),File.suffixed(rawbase,suffix+'.keep'))
+                            end
+                        end
+                    else
+# goto tmp/jobname when present
+                        mprundone, ok, stoprunning = false, true, false
+                        texruns, nofruns = 0, getvariable('runs').to_i
+                        state = FileState.new
+                        ['tub','tuo','tuc'].each do |s|
+                            state.register(File.suffixed(rawbase,s))
+                        end
+                        if getvariable('automprun') then # check this
+                            ['mprun','mpgraph'].each do |s|
+                                state.register(File.suffixed(rawbase,s,'mp'),'randomseed')
+                            end
+                        end
+                        while ! stoprunning && (texruns < nofruns) && ok do
+                            texruns += 1
+                            report("TeX run #{texruns}")
+                            unless getvariable('nooptionfile') then
+                                if texruns == nofruns then
+                                    makeoptionfile(rawbase,jobname,orisuffix,false,false,4,texruns) # last
+                                elsif texruns == 1 then
+                                    makeoptionfile(rawbase,jobname,orisuffix,false,false,1,texruns) # first
+                                else
+                                    makeoptionfile(rawbase,jobname,orisuffix,false,false,2,texruns) # unknown
+                                end
+                            end
+# goto .
+
+                            ok = runtex(File.suffixed(if dummyfile || forcexml then rawbase else rawname end,jobsuffix))
+
+if getvariable('texengine') == "xetex" then
+    ok = true
+end
+
+############################
+
+# goto tmp/jobname when present
+                            if ok && (nofruns > 1) then
+                                unless getvariable('nompmode') then
+                                    mprundone = runtexmpjob(rawbase, "mpgraph")
+                                    mprundone = runtexmpjob(rawbase, "mprun")
+                                end
+                                ok = runtexutil(rawbase)
+                                runluacheck(rawbase)
+                                state.update
+                                stoprunning = state.stable?
+                            end
+                        end
+                        if not ok then
+                            setvariable('error','error in tex file')
+                        end
+                        if (nofruns == 1) && getvariable('texutil') then
+                            ok = runtexutil(rawbase)
+                            runluacheck(rawbase)
+                        end
+                        if ok && finalrun && (nofruns > 1) then
+                            makeoptionfile(rawbase,jobname,orisuffix,true,finalrun,4,texruns) unless getvariable('nooptionfile')
+                            report("final TeX run #{texruns}")
+# goto .
+                            ok = runtex(File.suffixed(if dummyfile || forcexml then rawbase else rawname end,jobsuffix))
+# goto tmp/jobname when present
+                        end
+                        if getvariable('keep') then
+                            ['top','log','run'].each do |suffix|
+                                File.silentrename(File.suffixed(rawbase,suffix),File.suffixed(rawbase,suffix+'.keep'))
+                            end
+                        else
+                            File.silentrename(File.suffixed(rawbase,'top'),File.suffixed(rawbase,'tmp'))
+                        end
+                        # ['tmp','top','log'].each do |s| # previous tuo file / runtime option file / log file
+                             # File.silentdelete(File.suffixed(rawbase,s))
+                        # end
+                        if ok then
+# goto .
+                            runbackend(rawbase)
+                            popresult(rawbase,result)
+# goto tmp/jobname when present
+# skip next
+                        end
+                        if true then # autopurge
+                            begin
+                                File.open(File.suffixed(rawbase, 'tuo'),'rb') do |f|
+                                    ok = 0
+                                    f.each do |line|
+                                        case ok
+                                            when 1 then
+                                                # next line is empty
+                                                ok = 2
+                                            when 2 then
+                                                if line =~ /^\%\s+\>\s+(.*?)\s+(\d+)/moi then
+                                                    filename, n = $1, $2
+                                                    done = File.delete(filename) rescue false
+                                                    if done && getvariable('verbose') then
+                                                        report("deleting #{filename} (#{n} times used)")
+                                                    end
+                                                else
+                                                    break
+                                                end
+                                            else
+                                                if line =~ /^\%\s+temporary files\:\s+(\d+)/moi then
+                                                    if $1.to_i == 0 then
+                                                        break
+                                                    else
+                                                        ok = 1
+                                                    end
+                                                end
+                                        end
+                                    end
+                                end
+                            rescue
+                                # report("fatal error #{$!}")
+                            end
+                        end
+                    end
+
+                    Kpse.runscript('ctxtools',rawbase,'--purge')       if getvariable('purge')
+                    Kpse.runscript('ctxtools',rawbase,'--purge --all') if getvariable('purgeall')
+
+                    # runcommand('mtxrun','--script','ctxtools',rawbase,'--purge')       if getvariable('purge')
+                    # runcommand('mtxrun','--script','ctxtools',rawbase,'--purge --all') if getvariable('purgeall')
+
+                when 'latex' then
+
+                    ok = runtex(rawname)
+
+                else
+
+                    ok = runtex(rawname)
+
+            end
+
+            if (dummyfile or forcexml) and FileTest.file?(rawbase) then
+                begin
+                    File.delete(File.suffixed(rawbase,'run'))
+                rescue
+                    report("unable to delete stub file")
+                end
+            end
+
+            if ok and getvariable('autopdf') then
+                PDFview.open(File.suffixed(if result.empty? then rawbase else result end,'pdf'))
+            end
+
+        else
+            report("nothing to process")
+        end
+
+    end
+
+    # The labels are collected in the mergebe hash. Here we merge the relevant labels
+    # into beginfig/endfig. We could as well do this in metafun itself. Maybe some
+    # day ... (it may cost a bit of string space but that is cheap nowadays).
+
+    def doruntexmp(mpname,mergebe=nil,context=true,purge=true)
+        texfound = false
+        mpname = File.suffixed(mpname,'mp')
+        mpcopy = File.suffixed(mpname,'mp.copy')
+        mpkeep = File.suffixed(mpname,'mp.keep')
+        setvariable('mp.file',mpname)
+        setvariable('mp.line','')
+        setvariable('mp.error','')
+        if mpdata = File.silentread(mpname) then
+        #   mpdata.gsub!(/^\%.*\n/o,'')
+            File.silentrename(mpname,mpcopy)
+            texfound = mergebe || (mpdata =~ /btex .*? etex/mo)
+            if mp = openedfile(mpname) then
+                if mergebe then
+                    mpdata.gsub!(/beginfig\s*\((\d+)\)\s*\;(.+?)endfig\s*\;/mo) do
+                        n, str = $1, $2
+                        if str =~ /^(.*?)(verbatimtex.*?etex)\s*\;(.*)$/mo then
+                            "beginfig(#{n})\;\n#{$1}#{$2}\;\n#{mergebe[n]}\n#{$3}\;endfig\;\n"
+                        else
+                            "beginfig(#{n})\;\n#{mergebe[n]}\n#{str}\;endfig\;\n"
+                        end
+                    end
+                    unless mpdata =~ /beginfig\s*\(\s*0\s*\)/o then
+                        mp << mergebe['0'] if mergebe.key?('0')
+                    end
+                end
+        #       mp << MPTools::splitmplines(mpdata)
+                mp << mpdata
+                mp << "\n"
+        #        mp << "end"
+        #        mp << "\n"
+                mp.close
+            end
+            processmpx(mpname,true,true,purge) if texfound
+            if getvariable('batchmode') then
+                options = ' --interaction=batch'
+            elsif getvariable('nonstopmode') then
+                options = ' --interaction=nonstop'
+            else
+                options = ''
+            end
+            # todo plain|mpost|metafun
+            begin
+                ok = runmp(mpname)
+            rescue
+            end
+            if f = File.silentopen(File.suffixed(mpname,'log')) then
+                while str = f.gets do
+                    if str =~ /^l\.(\d+)\s(.*?)\n/o then
+                        setvariable('mp.line',$1)
+                        setvariable('mp.error',$2)
+                        break
+                    end
+                end
+                f.close
+            end
+            File.silentrename(mpname, mpkeep)
+            File.silentrename(mpcopy, mpname)
+        end
+    end
+
+    # todo: use internal mptotext function and/or turn all btex/etex into textexts
+
+    def processmpx(mpname,force=false,context=true,purge=true)
+        unless force then
+            mpname = File.suffixed(mpname,'mp')
+            if File.atleast?(mpname,10) && (data = File.silentread(mpname)) then
+                if data =~ /(btex|etex|verbatimtex|textext)/o then
+                    force = true
+                end
+            end
+        end
+        if force then
+            begin
+                mptex = File.suffixed(mpname,'temp','tex')
+                mpdvi = File.suffixed(mpname,'temp','dvi')
+                mplog = File.suffixed(mpname,'temp','log')
+                mpmpx = File.suffixed(mpname,'mpx')
+                File.silentdelete(mptex)
+                if true then
+                    report("using internal mptotex converter")
+                    ok = MPTools::mptotex(mpname,mptex,'context')
+                else
+                    command = "mpto #{mpname} > #{mptex}"
+                    report(command) if getvariable('verbose')
+                    ok = system(command)
+                end
+                # not "ok && ..." because of potential problem with return code and redirect (>)
+                if FileTest.file?(mptex) && File.appended(mptex, "\\end\n") then
+                    # to be replaced by runtexexec([filenames],options,1)
+                    if localjob = TEX.new(@logger) then
+                        localjob.setvariable('files',mptex)
+                        localjob.setvariable('backend','dvips')
+                        localjob.setvariable('engine',getvariable('engine')) unless getvariable('engine').empty?
+                        localjob.setvariable('once',true)
+                        localjob.setvariable('nobackend',true)
+                        if context then
+                            localjob.setvariable('texformats',[getvariable('interface')]) unless getvariable('interface').empty?
+                        elsif getvariable('interface').empty? then
+                            localjob.setvariable('texformats',['plain'])
+                        else
+                            localjob.setvariable('texformats',[getvariable('interface')])
+                        end
+                        localjob.processtex
+                        ok = true # todo
+                    else
+                        ok = false
+                    end
+                    # so far
+                    command = "dvitomp #{mpdvi} #{mpmpx}"
+                    report(command) if getvariable('verbose')
+                    ok = ok && FileTest.file?(mpdvi) && system(command)
+                    purge_mpx_files(mpname) if purge
+                end
+            rescue
+                # error in processing mpx file
+            end
+        end
+    end
+
+    def purge_mpx_files(mpname)
+        unless getvariable('keep') then
+            ['tex', 'log', 'tui', 'tuo', 'tuc', 'top'].each do |suffix|
+                File.silentdelete(File.suffixed(mpname,'temp',suffix))
+            end
+        end
+    end
+
+    def checkmpgraphics(mpname)
+        # in practice the checksums will differ because of multiple instances
+        # ok, we could save the mpy/mpo files by number, but not now
+        mpoptions = ''
+        if getvariable('makempy') then
+            mpoptions += " --makempy "
+        end
+        mponame = File.suffixed(mpname,'mpo')
+        mpyname = File.suffixed(mpname,'mpy')
+        pdfname = File.suffixed(mpname,'pdf')
+        tmpname = File.suffixed(mpname,'tmp')
+        if getvariable('mpyforce') || getvariable('forcempy') then
+            mpoptions += " --force "
+        else
+            return false unless File.atleast?(mponame,32)
+            mpochecksum = FileState.new.checksum(mponame)
+            return false if mpochecksum.empty?
+            # where does the checksum get into the file?
+            # maybe let texexec do it?
+            # solution: add one if not present or update when different
+            begin
+                mpydata = IO.read(mpyname)
+                if mpydata then
+                    if mpydata =~ /^\%\s*mpochecksum\s*\:\s*([A-Z0-9]+)$/mo then
+                        checksum = $1
+                        if mpochecksum == checksum then
+                            return false
+                        end
+                    end
+                end
+            rescue
+                # no file
+            end
+        end
+        # return Kpse.runscript('makempy',mpname)
+        # only pdftex
+        flags = ['--noctx','--process','--batch','--once']
+        result = runtexexec([mponame], flags, 1)
+        runcommand(["pstoedit","-ssp -dt -f mpost", pdfname,tmpname])
+        tmpdata = IO.read(tmpname)
+        if tmpdata then
+            if mpy = openedfile(mpyname) then
+                mpy << "% mpochecksum: #{mpochecksum}\n"
+                tmpdata.scan(/beginfig(.*?)endfig/mo) do |s|
+                    mpy << "begingraphictextfig#{s}endgraphictextfig\n"
+                end
+                mpy.close()
+            end
+        end
+        File.silentdelete(tmpname)
+        File.silentdelete(pdfname)
+        return true
+    end
+
+    def checkmplabels(mpname)
+        mpname = File.suffixed(mpname,'mpt')
+        if File.atleast?(mpname,10) && (mp = File.silentopen(mpname)) then
+            labels = Hash.new
+            while str = mp.gets do
+                t = if str =~ /^%\s*setup\s*:\s*(.*)$/o then $1 else '' end
+                if str =~ /^%\s*figure\s*(\d+)\s*:\s*(.*)$/o then
+                    labels[$1] = labels[$1] || ''
+                    unless t.empty? then
+                        labels[$1] += "#{t}\n"
+                        t = ''
+                    end
+                    labels[$1] += "#{$2}\n"
+                end
+            end
+            mp.close
+            if labels.size>0 then
+                return labels
+            else
+                return nil
+            end
+        end
+        return nil
+    end
+
+end
diff --git a/scripts/context/ruby/base/texutil.rb b/scripts/context/ruby/base/texutil.rb
new file mode 100644
index 000000000..868e3ca16
--- /dev/null
+++ b/scripts/context/ruby/base/texutil.rb
@@ -0,0 +1,1097 @@
+require "base/file"
+require "base/logger"
+
+class String
+
+    # real dirty, but inspect does a pretty good escaping but
+    # unfortunately puts quotes around the string so we need
+    # to strip these
+
+    # def escaped
+    #     self.inspect[1,self.inspect.size-2]
+    # end
+
+    def escaped
+        str = self.inspect ; str[1,str.size-2]
+    end
+
+    def splitdata
+        if self =~ /^\s*(.*?)\s*\{(.*)\}\s*$/o then
+            first, second = $1, $2
+            if first.empty? then
+                [second.split(/\} \{/o)].flatten
+            else
+                [first.split(/\s+/o)] + [second.split(/\} \{/o)]
+            end
+        else
+            []
+        end
+    end
+
+end
+
+class Logger
+    def banner(str)
+        report(str)
+        return "%\n% #{str}\n%\n"
+    end
+end
+
+class TeXUtil
+
+    class Plugin
+
+        # we need to reset module data for each run; persistent data is
+        # possible, just don't reinitialize the data structures that need
+        # to be persistent; we reset afterwards becausethen we know what
+        # plugins are defined
+
+        def initialize(logger)
+            @plugins = Array.new
+            @logger = logger
+        end
+
+        def report(str)
+            @logger.report("fatal error in plugin (#{str}): #{$!}")
+            puts("\n")
+            $@.each do |line|
+                puts("  #{line}")
+            end
+            puts("\n")
+        end
+
+        def reset(name)
+            if @plugins.include?(name) then
+                begin
+                    eval("#{name}").reset(@logger)
+                rescue Exception
+                    report("resetting")
+                end
+            else
+               @logger.report("no plugin #{name}")
+            end
+        end
+
+        def resets
+            @plugins.each do |p|
+                reset(p)
+            end
+        end
+
+        def register(name, file=nil) # maybe also priority
+            if file then
+                begin
+                    require("#{file.downcase.sub(/\.rb$/,'')}.rb")
+                rescue Exception
+                    @logger.report("no plugin file #{file} for #{name}")
+                else
+                    @plugins.push(name)
+                end
+            else
+                @plugins.push(name)
+            end
+            return self
+        end
+
+        def reader(name, data=[])
+            if @plugins.include?(name) then
+                begin
+                    eval("#{name}").reader(@logger,data.flatten)
+                rescue Exception
+                    report("reading")
+                end
+            else
+               @logger.report("no plugin #{name}")
+            end
+        end
+
+        def readers(data=[])
+            @plugins.each do |p|
+                reader(p,data.flatten)
+            end
+        end
+
+        def writers(handle)
+            @plugins.each do |p|
+                begin
+                    eval("#{p}").writer(@logger,handle)
+                rescue Exception
+                    report("writing")
+                end
+            end
+        end
+
+        def processors
+            @plugins.each do |p|
+                begin
+                    eval("#{p}").processor(@logger)
+                rescue Exception
+                    report("processing")
+                end
+            end
+        end
+
+        def finalizers
+            @plugins.each do |p|
+                begin
+                    eval("#{p}").finalizer(@logger)
+                rescue Exception
+                    report("finalizing")
+                end
+            end
+        end
+
+    end
+
+    class Sorter
+
+        @@downcase = true
+
+        def initialize(max=12)
+            @rep, @map, @exp, @div = Hash.new, Hash.new, Hash.new, Hash.new
+            @max = max
+            @rexa, @rexb = nil, nil
+        end
+
+        def replacer(from,to='') # and expand
+            @max = [@max,to.length+1].max if to
+            @rep[from.escaped] = to || ''
+        end
+
+        # sorter.reducer('ch', 'c')
+        # sorter.reducer('ij', 'y')
+
+        def reducer(from,to='')
+            @max = [@max,to.length+1].max if to
+            @map[from] = to || ''
+        end
+
+        # sorter.expander('aeligature', 'ae')
+        # sorter.expander('ijligature', 'y')
+
+        def expander(from,to=nil)
+            to = converted(to) # not from !!!
+            @max = [@max,to.length+1].max if to
+            @exp[from] = to || from || ''
+        end
+
+        def division(from,to=nil)
+            from, to = converted(from), converted(to)
+            @max = [@max,to.length+1].max if to
+            @div[from] = to || from || ''
+        end
+
+        # shortcut("\\ab\\cd\\e\\f", 'iacute')
+        # shortcut("\\\'\\i", 'iacute')
+        # shortcut("\\\'i", 'iacute')
+        # shortcut("\\\"e", 'ediaeresis')
+        # shortcut("\\\'o", 'oacute')
+
+        def hextoutf(str)
+            str.gsub(/^(0x[A-F\d]+)$/) do
+                [$1.hex()].pack("U")
+            end
+        end
+
+        def shortcut(from,to)
+            from = hextoutf(from)
+            replacer(from,to)
+            expander(to)
+        end
+
+        def prepare
+            if @rep.size > 0 then
+                @rexa = /(#{@rep.keys.join('|')})/ # o
+            else
+                @rexa = nil
+            end
+            if @map.size > 0 then
+                # watch out, order of match matters
+                if @@downcase then
+                    @rexb = /(\\[a-zA-Z]+|#{@map.keys.join('|')}|.)\s*/i # o
+                else
+                    @rexb = /(\\[a-zA-Z]+|#{@map.keys.join('|')}|.)\s*/ # o
+                end
+            else
+                if @@downcase then
+                    @rexb = /(\\[a-zA-Z]+|.)\s*/io
+                else
+                    @rexb = /(\\[a-zA-Z]+|.)\s*/o
+                end
+            end
+            if false then
+                @exp.keys.each do |e|
+                    @exp[e].downcase!
+                end
+            end
+        end
+
+        def replace(str)
+            if @rexa then
+                str.gsub(@rexa) do
+                    @rep[$1.escaped]
+                end
+            else
+                str
+            end
+        end
+
+        def normalize(str)
+            # replace(str).gsub(/ +/,' ')
+            replace(str).gsub(/\s\s+/," \\space")
+        end
+
+        def tokenize(str)
+            if str then
+                str.gsub(/\\strchr\{(.*?)\}/o) do "\\#{$1}" end
+            else
+                ""
+            end
+        end
+
+        def remap(str)
+            s = str.dup
+            if true then # numbers are treated special
+                s.gsub!(/(\d+)/o) do
+                    $1.rjust(10,'a') # rest is b .. k
+                end
+            end
+            if @rexa then
+                s.gsub!(@rexa) do
+                    @rep[$1.escaped]
+                end
+            end
+            if @rexb then
+                s.gsub!(@rexb) do
+                    token = $1.sub(/\\/o, '')
+                    if @@downcase then
+                        token.downcase!
+                    end
+                    if @exp.key?(token) then
+                        @exp[token].ljust(@max,' ')
+                    elsif @map.key?(token) then
+                        @map[token].ljust(@max,' ')
+                    else
+                        ''
+                    end
+                end
+            end
+            s
+        end
+
+        def preset(shortcuts=[],expansions=[],reductions=[],divisions=[],language='')
+            'a'.upto('z') do |c| expander(c) ; division(c) end
+            'A'.upto('Z') do |c| expander(c) ; division(c) end
+            expander('1','b') ; expander('2','c') ; expander('3','e') ; expander('4','f')
+            expander('5','g') ; expander('6','h') ; expander('7','i') ; expander('8','i')
+            expander('9','j') ; expander('0','a') ; expander('-','-') ;
+            shortcuts.each  do |s| shortcut(s[1],s[2]) if s[0] == '' || s[0] == language end
+            expansions.each do |e| expander(e[1],e[2]) if e[0] == '' || e[0] == language end
+            reductions.each do |r|  reducer(r[1],r[2]) if r[0] == '' || r[0] == language end
+            divisions.each  do |d| division(d[1],d[2]) if d[0] == '' || d[0] == language end
+        end
+
+        def simplify(str)
+            s = str.dup
+            # ^^
+            # s.gsub!(/\^\^([a-f0-9][a-f0-9])/o, $1.hex.chr)
+            # \- ||
+            s.gsub!(/(\\\-|\|\|)/o) do '-' end
+            # {}
+            s.gsub!(/\{\}/o) do '' end
+            # <*..> (internal xml entity)
+            s.gsub!(/<\*(.*?)>/o) do $1 end
+            # entities
+            s.gsub!(/\\getXMLentity\s*\{(.*?)\}/o) do $1 end
+            # elements
+            s.gsub!(/\<.*?>/o) do '' end
+            # what to do with xml and utf-8
+            # \"e etc
+            # unknown \cs
+            s.gsub!(/\\[a-zA-Z][a-zA-Z]+\s*\{(.*?)\}/o) do $1 end
+            return s
+        end
+
+        def getdivision(str)
+            @div[str] || str
+        end
+
+        def division?(str)
+            @div.key?(str)
+        end
+
+        private
+
+        def converted(str)
+            if str then
+                # puts str
+                str.gsub(/([\+\-]*\d+)/o) do
+                    n = $1.to_i
+                    if n > 0 then
+                        'z'*n
+                    elsif n < 0 then
+                        '-'*(-n) # '-' precedes 'a'
+                    else
+                        ''
+                    end
+                end
+            else
+                nil
+            end
+        end
+
+    end
+
+    class Plugin
+
+        module MyFiles
+
+            @@files, @@temps = Hash.new, Hash.new
+
+            def MyFiles::reset(logger)
+                @@files, @@temps = Hash.new, Hash.new
+            end
+
+            def MyFiles::reader(logger,data)
+                case data[0]
+                    when 'b', 'e' then
+                        @@files[data[1]] = (@@files[data[1]] ||0) + 1
+                    when 't' then # temporary file
+                        @@temps[data[1]] = (@@temps[data[1]] ||0) + 1
+                end
+            end
+
+            def MyFiles::writer(logger,handle)
+                handle << logger.banner("loaded files: #{@@files.size}")
+                @@files.keys.sort.each do |k|
+                    handle << "% > #{k} #{@@files[k]/2}\n"
+                end
+                handle << logger.banner("temporary files: #{@@temps.size}")
+                @@temps.keys.sort.each do |k|
+                    handle << "% > #{k} #{@@temps[k]}\n"
+                end
+            end
+
+            def MyFiles::processor(logger)
+                @@files.keys.sort.each do |k|
+                    unless (@@files[k] % 2) == 0 then
+                        logger.report("check loading of file '#{k}', begin/end problem")
+                    end
+                end
+                @@temps.keys.sort.each do |k|
+                    # logger.report("temporary file '#{k}' can be deleted")
+                end
+            end
+
+            def MyFiles::finalizer(logger)
+            end
+
+        end
+
+    end
+
+    class Plugin
+
+        module MyCommands
+
+            @@commands = []
+
+            def MyCommands::reset(logger)
+                @@commands = []
+            end
+
+            def MyCommands::reader(logger,data)
+                @@commands.push(data.shift+data.collect do |d| "\{#{d}\}" end.join)
+            end
+
+            def MyCommands::writer(logger,handle)
+                handle << logger.banner("commands: #{@@commands.size}")
+                @@commands.each do |c|
+                    handle << "#{c}%\n"
+                end
+            end
+
+            def MyCommands::processor(logger)
+            end
+
+            def MyCommands::finalizer(logger)
+            end
+
+        end
+
+    end
+
+    class Plugin
+
+        module MyExtras
+
+            @@programs = []
+
+            def MyExtras::reset(logger)
+                @@programs = []
+            end
+
+            def MyExtras::reader(logger,data)
+                case data[0]
+                    when 'p' then
+                        @@programs.push(data[1]) if data[0]
+                end
+            end
+
+            def MyExtras::writer(logger,handle)
+                handle << logger.banner("programs: #{@@programs.size}")
+                @@programs.each_with_index do |cmd, p|
+                    handle << "% #{p+1} (#{cmd})\n"
+                end
+            end
+
+            def MyExtras::processor(logger)
+                @@programs.each do |p|
+                    # cmd = @@programs[p.to_i]
+                    # logger.report("running #{cmd}")
+                    # system(cmd)
+                end
+            end
+
+            def MyExtras::finalizer(logger)
+                unless (ENV["CTX.TEXUTIL.EXTRAS"] =~ /^(no|off|false|0)$/io) || (ENV["CTX_TEXUTIL_EXTRAS"] =~ /^(no|off|false|0)$/io) then
+                    @@programs.each do |cmd|
+                        logger.report("running #{cmd}")
+                        system(cmd)
+                    end
+                end
+            end
+
+        end
+
+    end
+
+    class Plugin
+
+        module MySynonyms
+
+            class Synonym
+
+                @@debug = false
+
+                def initialize(t, c, k, d)
+                    @type, @command, @key, @sortkey, @data = t, c, k, c, d
+                end
+
+                attr_reader :type, :command, :key, :data
+                attr_reader :sortkey
+                attr_writer :sortkey
+
+                # def build(sorter)
+                    # if @key then
+                        # @sortkey = sorter.normalize(sorter.tokenize(@sortkey))
+                        # @sortkey = sorter.remap(sorter.simplify(@key.downcase)) # ??
+                        # if @sortkey.empty? then
+                            # @sortkey = sorter.remap(@command.downcase)
+                        # end
+                    # else
+                        # @key = ""
+                        # @sortkey = ""
+                    # end
+                # end
+
+                def build(sorter)
+                    if @sortkey and not @sortkey.empty? then
+                        @sortkey = sorter.normalize(sorter.tokenize(@sortkey))
+                        @sortkey = sorter.remap(sorter.simplify(@sortkey.downcase)) # ??
+                    end
+                    if not @sortkey or @sortkey.empty? then
+                        @sortkey = sorter.normalize(sorter.tokenize(@key))
+                        @sortkey = sorter.remap(sorter.simplify(@sortkey.downcase)) # ??
+                    end
+                    if not @sortkey or @sortkey.empty? then
+                        @sortkey = @key.dup
+                    end
+                end
+
+                def <=> (other)
+                    @sortkey <=> other.sortkey
+                end
+
+                def Synonym.flush(list,handle)
+                    if @@debug then
+                        list.each do |entry|
+                            handle << "% [#{entry.sortkey}]\n"
+                        end
+                    end
+                    list.each do |entry|
+                        handle << "\\synonymentry{#{entry.type}}{#{entry.command}}{#{entry.key}}{#{entry.data}}%\n"
+                    end
+                end
+
+            end
+
+            @@synonyms  = Hash.new
+            @@sorter    = Hash.new
+            @@languages = Hash.new
+
+            def MySynonyms::reset(logger)
+                @@synonyms  = Hash.new
+                @@sorter    = Hash.new
+                @@languages = Hash.new
+            end
+
+            def MySynonyms::reader(logger,data)
+                case data[0]
+                    when 'e' then
+                        @@synonyms[data[1]] = Array.new unless @@synonyms.key?(data[1])
+                        @@synonyms[data[1]].push(Synonym.new(data[1],data[2],data[3],data[4]))
+                    when 'l' then
+                        @@languages[data[1]] = data[2] || ''
+                end
+            end
+
+            def MySynonyms::writer(logger,handle)
+                if @@synonyms.size > 0 then
+                    @@synonyms.keys.sort.each do |s|
+                        handle << logger.banner("synonyms: #{s} #{@@synonyms[s].size}")
+                        Synonym.flush(@@synonyms[s],handle)
+                    end
+                end
+            end
+
+            def MySynonyms::processor(logger)
+                @@synonyms.keys.each do |s|
+                    @@sorter[s] = Sorter.new
+                    @@sorter[s].preset(
+                        eval("MyKeys").shortcuts,
+                        eval("MyKeys").expansions,
+                        eval("MyKeys").reductions,
+                        eval("MyKeys").divisions,
+                        @@languages[s] || '')
+                    @@sorter[s].prepare
+                    @@synonyms[s].each_index do |i|
+                        @@synonyms[s][i].build(@@sorter[s])
+                    end
+                    @@synonyms[s] = @@synonyms[s].sort
+                end
+            end
+
+            def MySynonyms::finalizer(logger)
+            end
+
+        end
+
+    end
+
+    class Plugin
+
+        module MyRegisters
+
+            class Register
+
+                @@specialsymbol = "\000"
+                @@specialbanner = "" # \\relax"
+
+                @@debug = false
+
+                @@howto = /^(.*?)\:\:(.*)$/o
+                @@split = ' && '
+
+                def initialize(state, t, l, k, e, s, p, r)
+                    @state, @type, @location, @key, @entry, @seetoo, @page, @realpage = state, t, l, k, e, s, p, r
+                    if @key   =~ @@howto then @pagehowto, @key   = $1, $2 else @pagehowto = '' end
+                    if @entry =~ @@howto then @texthowto, @entry = $1, $2 else @texthowto = '' end
+                    @key = @entry.dup if @key.empty?
+                    @sortkey = @key.dup
+                    @nofentries, @nofpages = 0, 0
+                    @normalizeentry = false
+                end
+
+                attr_reader :state, :type, :location, :key, :entry, :seetoo, :page, :realpage, :texthowto, :pagehowto
+                attr_reader :sortkey
+                attr_writer :sortkey
+
+                def build(sorter)
+                    # @entry, @key = sorter.normalize(@entry), sorter.normalize(sorter.tokenize(@key))
+                    @entry = sorter.normalize(sorter.tokenize(@entry)) if @normalizeentry
+                    @key   = sorter.normalize(sorter.tokenize(@key))
+                    if false then
+                        @entry, @key = [@entry, @key].collect do |target|
+                            # +a+b+c &a&b&c a+b+c a&b&c
+                            case target[0,1]
+                                when '&' then target = target.sub(/^./o,'').gsub(/([^\\])\&/o)     do "#{$1}#{@@split}" end
+                                when '+' then target = target.sub(/^./o,'').gsub(/([^\\])\+/o)     do "#{$1}#{@@split}" end
+                                else          target = target              .gsub(/([^\\])[\&\+]/o) do "#{$1}#{@@split}" end
+                            end
+                            # {a}{b}{c}
+                            # if target =~ /^\{(.*)\}$/o then
+                                # $1.split(/\} \{/o).join(@@split) # space between } { is mandate
+                            # else
+                                target
+                            # end
+                        end
+                    else
+                        # @entry, @key = cleanupsplit(@entry), cleanupsplit(@key)
+                        @entry, @key = cleanupsplit(@entry), xcleanupsplit(@key)
+                    end
+                    @sortkey = sorter.simplify(@key)
+                    # special = @sortkey =~ /^([^a-zA-Z\\])/o
+                    special = @sortkey =~ /^([\`\~\!\@\#\$\%\^\&\*\(\)\_\-\+\=\{\}\[\]\:\;\"\'\|\<\,\>\.\?\/\d])/o
+                    @sortkey = @sortkey.split(@@split).collect do |c| sorter.remap(c) end.join(@@split)
+                    if special then
+                        @sortkey = "#{@@specialsymbol}#{@sortkey}"
+                    end
+                    if @realpage == 0 then
+                        @realpage = 999999
+                    end
+                    @sortkey = [
+                        @sortkey.downcase,
+                        @sortkey,
+                        @entry,
+                        @texthowto.ljust(10,' '),
+                        # @state, # no, messes up things
+                        (@realpage.to_s || '').rjust(6,' ').gsub(/0/,' '),
+                        # (@realpage ||'').rjust(6,' '),
+                        @pagehowto
+                    ].join(@@split)
+                end
+
+                def cleanupsplit(target)
+                    # +a+b+c &a&b&c a+b+c a&b&c
+                    case target[0,1]
+                        when '&' then target.sub(/^./o,'').gsub(/([^\\])\&/o)     do "#{$1}#{@@split}" end
+                        when '+' then target.sub(/^./o,'').gsub(/([^\\])\+/o)     do "#{$1}#{@@split}" end
+                        else          target              .gsub(/([^\\])[\&\+]/o) do "#{$1}#{@@split}" end
+                    end
+                end
+
+                def xcleanupsplit(target) # +a+b+c &a&b&c a+b+c a&b&c
+                    t = Array.new
+                    case target[0,1]
+                        when '&' then
+                            t = target.sub(/^./o,'').split(/([^\\])\&/o)
+                        when '+' then
+                            t = target.sub(/^./o,'').split(/([^\\])\+/o)
+                        else
+                            # t = target.split(/([^\\])[\&\+]/o)
+                            # t = target.split(/[\&\+]/o)
+                            t = target.split(/(?!\\)[\&\+]/o) # lookahead
+                    end
+                    if not t[1] then t[1] = " " end # we need some entry else we get subentries first
+                    if not t[2] then t[2] = " " end # we need some entry else we get subentries first
+                    if not t[3] then t[3] = " " end # we need some entry else we get subentries first
+                    return t.join(@@split)
+                end
+                def <=> (other)
+                    @sortkey <=> other.sortkey
+                end
+
+                # more module like
+
+                @@savedhowto, @@savedfrom, @@savedto, @@savedentry = '', '', '', '', ''
+                @@collapse = false
+
+                def Register.flushsavedline(handle)
+                    if @@collapse && ! @@savedfrom.empty? then
+                        if ! @@savedto.empty? then
+                            handle << "\\registerfrom#{@@savedfrom}%"
+                            handle << "\\registerto#{@@savedto}%"
+                        else
+                            handle << "\\registerpage#{@@savedfrom}%"
+                        end
+                    end
+                    @@savedhowto, @@savedfrom, @@savedto, @@savedentry = '', '', '', ''
+                end
+
+                def Register.flush(list,handle,sorter)
+                    # a bit messy, quite old mechanism, maybe some day ...
+                    # alphaclass can go, now flushed per class
+                    if list.size > 0 then
+                        @nofentries, @nofpages = 0, 0
+                        current, previous, howto  = Array.new, Array.new, Array.new
+                        lastpage, lastrealpage = '', ''
+                        alphaclass, alpha = '', ''
+                        @@savedhowto, @@savedfrom, @@savedto, @@savedentry = '', '', '', ''
+                        if @@debug then
+                            list.each do |entry|
+                                handle << "% [#{entry.sortkey.gsub(/#{@@split}/o,'] [')}]\n"
+                            end
+                        end
+                        list.each do |entry|
+# puts(entry.sortkey.gsub(/\s+/,""))
+                            if entry.sortkey =~ /^(\S+)/o then
+                                if sorter.division?($1) then
+                                    testalpha = sorter.getdivision($1)
+                                else
+                                    testalpha = entry.sortkey[0,1].downcase
+                                end
+                            else
+                                testalpha = entry.sortkey[0,1].downcase
+                            end
+                            if (testalpha != alpha.downcase) || (alphaclass != entry.class) then
+                                alpha = testalpha
+                                alphaclass = entry.class
+                                if alpha != ' ' then
+                                    flushsavedline(handle)
+                                    if alpha =~ /^[a-zA-Z]$/o then
+                                        character = alpha.dup
+                                    elsif alpha == @@specialsymbol then
+                                        character = @@specialbanner
+                                    elsif alpha.length > 1 then
+                                        # character = "\\getvalue\{#{alpha}\}"
+                                        character = "\\#{alpha}"
+                                    else
+                                        character = "\\unknown"
+                                    end
+                                    handle << "\\registerentry{#{entry.type}}{#{character}}%\n"
+                                end
+                            end
+                            current = [entry.entry.split(@@split),'','','',''].flatten
+                            howto = current.collect do |e|
+                                e + '::' + entry.texthowto
+                            end
+                            if howto[0] == previous[0] then
+                                current[0] = ''
+                            else
+                                previous[0] = howto[0].dup
+                                previous[1] = ''
+                                previous[2] = ''
+                                previous[3] = ''
+                            end
+                            if howto[1] == previous[1] then
+                                current[1] = ''
+                            else
+                                previous[1] = howto[1].dup
+                                previous[2] = ''
+                                previous[3] = ''
+                            end
+                            if howto[2] == previous[2] then
+                                current[2] = ''
+                            else
+                                previous[2] = howto[2].dup
+                                previous[3] = ''
+                            end
+                            if howto[3] == previous[3] then
+                                current[3] = ''
+                            else
+                                previous[3] = howto[3].dup
+                            end
+                            copied = false
+                            unless current[0].empty? then
+                                Register.flushsavedline(handle)
+                                handle << "\\registerentrya{#{entry.type}}{#{current[0]}}%\n"
+                                copied = true
+                            end
+                            unless current[1].empty? then
+                                Register.flushsavedline(handle)
+                                handle << "\\registerentryb{#{entry.type}}{#{current[1]}}%\n"
+                                copied = true
+                            end
+                            unless current[2].empty? then
+                                Register.flushsavedline(handle)
+                                handle << "\\registerentryc{#{entry.type}}{#{current[2]}}%\n"
+                                copied = true
+                            end
+                            unless current[3].empty? then
+                                Register.flushsavedline(handle)
+                                handle << "\\registerentryd{#{entry.type}}{#{current[3]}}%\n"
+                                copied = true
+                            end
+                            @nofentries += 1 if copied
+                            # if entry.realpage.to_i == 0 then
+                            if entry.realpage.to_i == 999999 then
+                                Register.flushsavedline(handle)
+                                handle << "\\registersee{#{entry.type}}{#{entry.pagehowto},#{entry.texthowto}}{#{entry.seetoo}}{#{entry.page}}%\n" ;
+                                lastpage, lastrealpage = entry.page, entry.realpage
+                                copied = false # no page !
+                            elsif @@savedhowto != entry.pagehowto and ! entry.pagehowto.empty? then
+                                @@savedhowto = entry.pagehowto
+                            end
+                            # beware, we keep multiple page entries per realpage because of possible prefix usage
+                            if copied || ! ((lastpage == entry.page) && (lastrealpage == entry.realpage)) then
+                                nextentry = "{#{entry.type}}{#{previous[0]}}{#{previous[1]}}{#{previous[2]}}{#{previous[3]}}{#{entry.pagehowto},#{entry.texthowto}}"
+                                savedline = "{#{entry.type}}{#{@@savedhowto},#{entry.texthowto}}{#{entry.location}}{#{entry.page}}{#{entry.realpage}}"
+                                if entry.state == 1 then # from
+                                    Register.flushsavedline(handle)
+                                    handle << "\\registerfrom#{savedline}%\n"
+                                elsif entry.state == 3 then # to
+                                    Register.flushsavedline(handle)
+                                    handle << "\\registerto#{savedline}%\n"
+                                    @@savedhowto = '' # test
+                                elsif @@collapse then
+                                    if savedentry != nextentry then
+                                        savedFrom = savedline
+                                    else
+                                        savedTo, savedentry = savedline, nextentry
+                                    end
+                                else
+                                    handle << "\\registerpage#{savedline}%\n"
+                                    @@savedhowto = '' # test
+                                end
+                                @nofpages += 1
+                                lastpage, lastrealpage = entry.page, entry.realpage
+                            end
+                        end
+                        Register.flushsavedline(handle)
+                    end
+                end
+
+            end
+
+            @@registers = Hash.new
+            @@sorter    = Hash.new
+            @@languages = Hash.new
+
+            def MyRegisters::reset(logger)
+                @@registers = Hash.new
+                @@sorter    = Hash.new
+                @@languages = Hash.new
+            end
+
+            def MyRegisters::reader(logger,data)
+                case data[0]
+                    when 'f' then
+                        @@registers[data[1]] = Array.new unless @@registers.key?(data[1])
+                        @@registers[data[1]].push(Register.new(1,data[1],data[2],data[3],data[4],nil,data[5],data[6]))
+                    when 'e' then
+                        @@registers[data[1]] = Array.new unless @@registers.key?(data[1])
+                        @@registers[data[1]].push(Register.new(2,data[1],data[2],data[3],data[4],nil,data[5],data[6]))
+                    when 't' then
+                        @@registers[data[1]] = Array.new unless @@registers.key?(data[1])
+                        @@registers[data[1]].push(Register.new(3,data[1],data[2],data[3],data[4],nil,data[5],data[6]))
+                    when 's' then
+                        @@registers[data[1]] = Array.new unless @@registers.key?(data[1])
+                        # was this but wrong sort order       (4,data[1],data[2],data[3],data[4],data[5],data[6],nil))
+                        @@registers[data[1]].push(Register.new(4,data[1],data[2],data[3],data[4],data[5],data[6],0))
+                    when 'l' then
+                        @@languages[data[1]] = data[2] || ''
+                end
+            end
+
+            def MyRegisters::writer(logger,handle)
+                if @@registers.size > 0 then
+                    @@registers.keys.sort.each do |s|
+                        handle << logger.banner("registers: #{s} #{@@registers[s].size}")
+                        Register.flush(@@registers[s],handle,@@sorter[s])
+                        # report("register #{@@registers[s].class}: #{@@registers[s].@nofentries} entries and #{@@registers[s].@nofpages} pages")
+                    end
+                end
+            end
+
+            def MyRegisters::processor(logger)
+                @@registers.keys.each do |s|
+                    @@sorter[s] = Sorter.new
+                    @@sorter[s].preset(
+                        eval("MyKeys").shortcuts,
+                        eval("MyKeys").expansions,
+                        eval("MyKeys").reductions,
+                        eval("MyKeys").divisions,
+                        @@languages[s] || '')
+                    @@sorter[s].prepare
+                    @@registers[s].each_index do |i|
+                        @@registers[s][i].build(@@sorter[s])
+                    end
+                    # @@registers[s].uniq!
+                    @@registers[s] = @@registers[s].sort
+                end
+            end
+
+            def MyRegisters::finalizer(logger)
+            end
+
+        end
+
+    end
+
+    class Plugin
+
+        module MyPlugins
+
+            @@plugins = nil
+
+            def MyPlugins::reset(logger)
+                @@plugins = nil
+            end
+
+            def MyPlugins::reader(logger,data)
+                @@plugins = Plugin.new(logger) unless @@plugins
+                case data[0]
+                    when 'r' then
+                        logger.report("registering plugin #{data[1]}")
+                        @@plugins.register(data[1],data[2])
+                    when 'd' then
+                        begin
+                            @@plugins.reader(data[1],data[2,data.length-1])
+                        rescue
+                            @@plugins.reader(data[1],['error'])
+                        end
+                end
+            end
+
+            def MyPlugins::writer(logger,handle)
+                @@plugins.writers(handle) if @@plugins
+            end
+
+            def MyPlugins::processor(logger)
+                @@plugins.processors if @@plugins
+            end
+
+            def MyPlugins::finalizer(logger)
+                @@plugins.finalizers if @@plugins
+            end
+
+        end
+
+    end
+
+    class Plugin
+
+        module MyKeys
+
+            @@shortcuts  = Array.new
+            @@expansions = Array.new
+            @@reductions = Array.new
+            @@divisions  = Array.new
+
+            def MyKeys::shortcuts
+                @@shortcuts
+            end
+            def MyKeys::expansions
+                @@expansions
+            end
+            def MyKeys::reductions
+                @@reductions
+            end
+            def MyKeys::divisions
+                @@divisions
+            end
+
+            def MyKeys::reset(logger)
+                @@shortcuts  = Array.new
+                @@expansions = Array.new
+                @@reductions = Array.new
+            end
+
+            def MyKeys::reader(logger,data)
+                key = data.shift
+                # grp = data.shift # language code, todo
+                case key
+                    when 's' then @@shortcuts.push(data)
+                    when 'e' then @@expansions.push(data)
+                    when 'r' then @@reductions.push(data)
+                    when 'd' then @@divisions.push(data)
+                end
+            end
+
+            def MyKeys::writer(logger,handle)
+            end
+
+            def MyKeys::processor(logger)
+                logger.report("shortcuts : #{@@shortcuts.size}")  # logger.report(@@shortcuts.inspect)
+                logger.report("expansions: #{@@expansions.size}") # logger.report(@@expansions.inspect)
+                logger.report("reductions: #{@@reductions.size}") # logger.report(@@reductions.inspect)
+                logger.report("divisions : #{@@divisions.size}")  # logger.report(@@divisions.inspect)
+            end
+
+            def MyKeys::finalizer(logger)
+            end
+
+        end
+
+    end
+
+    class Converter
+
+        def initialize(logger=nil)
+            if @logger = logger then
+                def report(str)
+                    @logger.report(str)
+                end
+                def banner(str)
+                    @logger.banner(str)
+                end
+            else
+                @logger = self
+                def report(str)
+                    puts(str)
+                end
+                def banner(str)
+                    puts(str)
+                end
+            end
+            @filename = 'texutil'
+            @fatalerror = false
+            @plugins = Plugin.new(@logger)
+            ['MyFiles', 'MyCommands', 'MySynonyms', 'MyRegisters', 'MyExtras', 'MyPlugins', 'MyKeys'].each do |p|
+                @plugins.register(p)
+            end
+        end
+
+        def loaded(filename)
+            begin
+                tuifile = File.suffixed(filename,'tui')
+                if FileTest.file?(tuifile) then
+                    report("parsing file #{tuifile}")
+                    if f = File.open(tuifile,'rb') then
+                        f.each do |line|
+                            case line.chomp
+                                when /^f (.*)$/o then @plugins.reader('MyFiles',    $1.splitdata)
+                                when /^c (.*)$/o then @plugins.reader('MyCommands', [$1])
+                                when /^e (.*)$/o then @plugins.reader('MyExtras',   $1.splitdata)
+                                when /^s (.*)$/o then @plugins.reader('MySynonyms', $1.splitdata)
+                                when /^r (.*)$/o then @plugins.reader('MyRegisters',$1.splitdata)
+                                when /^p (.*)$/o then @plugins.reader('MyPlugins',  $1.splitdata)
+                                when /^x (.*)$/o then @plugins.reader('MyKeys',     $1.splitdata)
+                                when /^r (.*)$/o then # nothing, not handled here
+                            else
+                                # report("unknown entry #{line[0,1]} in line #{line.chomp}")
+                            end
+                        end
+                        f.close
+                    end
+                else
+                    report("unable to locate #{tuifile}")
+                end
+            rescue
+                report("fatal error in parsing #{tuifile}")
+                @filename = 'texutil'
+            else
+                @filename = filename
+            end
+        end
+
+        def processed
+            @plugins.processors
+            return true # for the moment
+        end
+
+        def saved(filename=@filename)
+            if @fatalerror then
+                report("fatal error, no tuo file saved")
+                return false
+            else
+                begin
+                    if f = File.open(File.suffixed(filename,'tuo'),'w') then
+                        @plugins.writers(f)
+                        f << "\\endinput\n"
+                        f.close
+                    end
+                rescue
+                    report("fatal error when saving file (#{$!})")
+                    return false
+                else
+                    report("tuo file saved")
+                    return true
+                end
+            end
+        end
+
+        def finalized
+            @plugins.finalizers
+            @plugins.resets
+            return true # for the moment
+        end
+
+        def reset
+            @plugins.resets
+        end
+
+    end
+
+end
diff --git a/scripts/context/ruby/base/tool.rb b/scripts/context/ruby/base/tool.rb
new file mode 100644
index 000000000..abf0d5ed0
--- /dev/null
+++ b/scripts/context/ruby/base/tool.rb
@@ -0,0 +1,291 @@
+# module    : base/tool
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2002-2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+require 'timeout'
+require 'socket'
+require 'rbconfig'
+
+module Tool
+
+    $constructedtempdir = ''
+
+    def Tool.constructtempdir(create,mainpath='',fallback='')
+        begin
+            mainpath += '/' unless mainpath.empty?
+            timeout(5) do
+                begin
+                    t = Time.now
+                    u = t.usec.to_s % [1..2] [0..3]
+                    pth = t.strftime("#{mainpath}%Y%m%d-%H%M%S-#{u}-#{Process.pid}")
+                #
+                #    problems with 1.9
+                #
+                #    if pth == $constructedtempdir
+                #        # sleep(0.01)
+                #        retry
+                #    end
+                    pth == $constructedtempdir
+                #
+                    Dir.mkdir(pth) if create
+                    $constructedtempdir = pth
+                    return pth
+                rescue
+                    # sleep(0.01)
+                    retry
+                end
+            end
+        rescue TimeoutError
+            # ok
+        rescue
+            # ok
+        end
+        unless fallback.empty?
+            begin
+                pth = "#{mainpath}#{fallback}"
+                mkdir(pth) if create
+                $constructedtempdir = path
+                return pth
+            rescue
+                return '.'
+            end
+        else
+            return '.'
+        end
+
+    end
+
+    def Tool.findtempdir(*vars)
+        constructtempdir(false,*vars)
+    end
+
+    def Tool.maketempdir(*vars)
+        constructtempdir(true,*vars)
+    end
+
+    # print maketempdir + "\n"
+    # print maketempdir + "\n"
+    # print maketempdir + "\n"
+    # print maketempdir + "\n"
+    # print maketempdir + "\n"
+
+
+    def Tool.ruby_platform
+        case RUBY_PLATFORM
+            when /(mswin|bccwin|mingw|cygwin)/i then 'mswin'
+            when /(linux)/i                     then 'linux'
+            when /(netbsd|unix)/i               then 'unix'
+            when /(darwin|rhapsody|nextstep)/i  then 'macosx'
+            else                                     'unix'
+        end
+    end
+
+    $defaultlineseparator = $/ # $RS in require 'English'
+
+    def Tool.file_platform(filename)
+
+        begin
+            if f = open(filename,'rb') then
+                str = f.read(4000)
+                str.gsub!(/(.*?)\%\!PS/mo, "%!PS") # don't look into preamble crap
+                f.close
+                nn = str.count("\n")
+                nr = str.count("\r")
+                if nn>nr then
+                    return 2
+                elsif nn<nr then
+                    return 3
+                else
+                    return 1
+                end
+            else
+                return 0
+            end
+        rescue
+            return 0
+        end
+
+    end
+
+    def Tool.path_separator
+        return File::PATH_SEPARATOR
+    end
+
+    def Tool.line_separator(filename)
+
+        case file_platform(filename)
+            when 1 then return $defaultlineseparator
+            when 2 then return "\n"
+            when 3 then return "\r"
+            else        return $defaultlineseparator
+        end
+
+    end
+
+    def Tool.default_line_separator
+        $defaultlineseparator
+    end
+
+    def Tool.simplefilename(old)
+
+        return old # too fragile
+
+        return old if not FileTest.file?(old)
+
+        new = old.downcase
+        new.gsub!(/[^A-Za-z0-9\_\-\.\\\/]/o) do # funny chars
+            '-'
+        end
+        if old =~ /[a-zA-Z]\:/o
+            # seems like we have a dos/windows drive prefix, so roll back
+            new.sub!(/^(.)\-/) do
+                $1 + ':'
+            end
+        end
+        # fragile for a.b.c.d.bla-bla.e.eps
+        # new.gsub!(/(.+?)\.(.+?)(\..+)$/o)  do # duplicate .
+            # $1 + '-' + $2 + $3
+        # end
+        new.gsub!(/\-+/o)                  do # duplicate -
+            '-'
+        end
+        new
+
+    end
+
+    if Config::CONFIG['host_os'] =~ /mswin/ then
+
+        require 'Win32API'
+
+        GetShortPathName = Win32API.new('kernel32', 'GetShortPathName', ['P','P','N'], 'N')
+        GetLongPathName = Win32API.new('kernel32', 'GetLongPathName', ['P','P','N'], 'N')
+
+        def Tool.dowith_pathname (filename,filemethod)
+            filename.gsub!(/\\/o,'/')
+            case filename
+                when /\;/o then
+                    # could be a path spec
+                    return filename
+                when /\s+/o then
+                    # danger lurking
+                    buffer = ' ' * 260
+                    length = filemethod.call(filename,buffer,buffer.size)
+                    if length>0 then
+                        return buffer.slice(0..length-1)
+                    else
+                        # when the path or file does not exist, nothing is returned
+                        # so we try to handle the path separately from the basename
+                        basename = File.basename(filename)
+                        pathname = File.dirname(filename)
+                        length = filemethod.call(pathname,buffer,260)
+                        if length>0 then
+                            return buffer.slice(0..length-1) + '/' + basename
+                        else
+                            return filename
+                        end
+                    end
+                else
+                    # no danger
+                    return filename
+            end
+        end
+
+        def Tool.shortpathname(filename)
+            dowith_pathname(filename,GetShortPathName)
+        end
+
+        def Tool.longpathname(filename)
+            dowith_pathname(filename,GetLongPathName)
+        end
+
+    else
+
+        def Tool.shortpathname(filename)
+            filename
+        end
+
+        def Tool.longpathname(filename)
+            filename
+        end
+
+    end
+
+    # print shortpathname("C:/Program Files/ABBYY FineReader 6.0/matrix.str")+ "!\n"
+    # print shortpathname("C:/Program Files/ABBYY FineReader 6.0/matrix.strx")+ "!\n"
+
+    def Tool.checksuffix(old)
+
+        return old unless FileTest.file?(old)
+
+        new = old
+
+        unless new =~ /\./io # no suffix
+            f = open(filename,'rb')
+            if str = f.gets
+                case str
+                    when /^\%\!PS/io
+                        # logging.report(filename, 'analyzed as EPS')
+                        new = new + '.eps'
+                    when /^\%PDF/io
+                        # logging.report(filename, 'analyzed as PDF')
+                        new = new + '.pdf'
+                    else
+                        # logging.report(filename, 'fallback as TIF')
+                        new = new + '.tif'
+                end
+            end
+            f.close
+        end
+
+        new.sub!(/\.jpeg$/io) do
+            '.jpg'
+        end
+        new.sub!(/\.tiff$/io) do
+            '.tif'
+        end
+        new.sub!(/\.ai$/io) do
+            '.eps'
+        end
+        new.sub!(/\.ai([a-z0-9]*)$/io) do
+            '-' + $1 + '.eps'
+        end
+        new
+
+    end
+
+    def Tool.cleanfilename(old,logging=nil)
+
+        return old if not FileTest.file?(old)
+
+        new = checksuffix(simplefilename(old))
+        unless new == old
+            begin # bugged, should only be name, not path
+                File.rename(old,new)
+                logging.report("renaming fuzzy name #{old} to #{new}") unless logging
+                return old
+            rescue
+                logging.report("unable to rename fuzzy name #{old} to #{new}") unless logging
+            end
+        end
+        return new
+
+    end
+
+    def Tool.servername
+        host = Socket::gethostname
+        begin
+            Socket::gethostbyname(host)[0]
+        rescue
+            host
+        end
+    end
+
+    # print file_platform(ARGV[0])
+
+end
diff --git a/scripts/context/ruby/base/variables.rb b/scripts/context/ruby/base/variables.rb
new file mode 100644
index 000000000..403b57716
--- /dev/null
+++ b/scripts/context/ruby/base/variables.rb
@@ -0,0 +1,132 @@
+# module    : base/variables
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2002-2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+# ['base/tool','tool'].each do |r| begin require r ; rescue Exception ; else break ; end ; end
+
+require 'base/tool'
+
+class Hash
+
+    def nothing?(id)
+        ! self[id] || self[id].empty?
+    end
+
+    def subset(pattern)
+        h = Hash.new
+        p = pattern.gsub(/([\.\:\-])/) do "\\#{$1}" end
+        r = /^#{p}/
+        self.keys.each do |k|
+            h[k] = self[k].dup if k =~ r
+        end
+        return h
+    end
+
+end
+
+class ExtendedHash < Hash
+
+    @@re_var_a = /\%(.*?)\%/
+    @@re_var_b = /\$\((.*?)\)/
+
+    def set(key,value='',resolve=true)
+        if value then
+            self[key] = if resolve then resolved(value.to_s) else value.to_s end
+        else
+            self[key] = ''
+        end
+    end
+
+    def replace(key,value='')
+        self[key] = value if self?(key)
+    end
+
+    def get(key,default='')
+        if self.key?(key) then self[key] else default end
+    end
+
+    def true?(key)
+        self[key] =~ /^(yes|on|true|enable|enabled|y|start)$/io rescue false
+    end
+
+    def resolved(str)
+        begin
+            str.to_s.gsub(@@re_var_a) do
+                self[$1] || ''
+            end.gsub(@@re_var_b) do
+                self[$1] || ''
+            end
+        rescue
+            str.to_s rescue ''
+        end
+    end
+
+    def check(key,default='')
+        if self.key?(key) then
+            if self[key].empty? then self[key] = (default || '') end
+        else
+            self[key] = (default || '')
+        end
+    end
+
+    def checked(key,default='')
+        if self.key?(key) then
+            if self[key].empty? then default else self[key] end
+        else
+            default
+        end
+    end
+
+    def empty?(key)
+        self[key].empty?
+    end
+
+    # def downcase(key)
+        # self[key].downcase!
+    # end
+
+end
+
+# the next one is obsolete so we need to replace things
+
+module Variables
+
+    def setvariable(key,value='')
+        @variables[key] = value
+    end
+
+    def replacevariable(key,value='')
+        @variables[key] = value if @variables.key?(key)
+    end
+
+    def getvariable(key,default='')
+        if @variables.key?(key) then @variables[key] else default end
+    end
+
+    def truevariable(key)
+        @variables[key] =~ /^(yes|on|true)$/io rescue false
+    end
+
+    def checkedvariable(str,default='')
+        if @variables.key?(key) then
+            if @variables[key].empty? then default else @variables[key] end
+        else
+            default
+        end
+    end
+
+    def report(*str)
+        @logger.report(*str)
+    end
+
+    def debug(*str)
+        @logger.debug(str)
+    end
+
+end
diff --git a/scripts/context/ruby/concheck.rb b/scripts/context/ruby/concheck.rb
new file mode 100644
index 000000000..3db3e5148
--- /dev/null
+++ b/scripts/context/ruby/concheck.rb
@@ -0,0 +1,471 @@
+# Program   : concheck (tex & context syntax checker)
+# Copyright : PRAGMA ADE / Hasselt NL / www.pragma-ade.com
+# Author    : Hans Hagen
+# Version   : 1.1 / 2003.08.18
+
+# remarks:
+#
+# - the error messages are formatted like tex's messages so that scite can see them
+# - begin and end tags are only tested on a per line basis because we assume clean sources
+# - maybe i'll add begin{something} ... end{something} checking
+
+# # example validation file
+#
+# begin interface en
+#
+# 1 text
+# 4 Question
+# 0 endinput
+# 0 setupsomething
+# 0 chapter
+#
+# end interface en
+
+# nicer
+
+# class Interface
+
+    # def initialize (language = 'unknown')
+        # @valid = Array.new
+        # @language = language
+    # end
+
+    # def register (left, right)
+        # @valid.push([left,right])
+    # end
+
+# end
+
+# $interfaces = Hash.new
+
+# $interfaces['en'] = Interface.new('english')
+# $interfaces['nl'] = Interface.new('dutch')
+
+# $interfaces['en'].add('\\\\start','\\\\stop')
+# $interfaces['en'].add('\\\\begin','\\\\end')
+# $interfaces['en'].add('\\\\Start','\\\\Stop')
+# $interfaces['en'].add('\\\\Begin','\\\\End')
+
+# $interfaces['nl'].add('\\\\start','\\\\stop')
+# $interfaces['nl'].add('\\\\beginvan','\\\\eindvan')
+# $interfaces['nl'].add('\\\\Start','\\\\Stop')
+# $interfaces['nl'].add('\\\\BeginVan','\\\\Eindvan')
+
+# rest todo
+
+$valid = Hash.new
+
+$valid['en'] = Array.new
+$valid['nl'] = Array.new
+
+#$valid['en'].push(['',''])
+$valid['en'].push(['\\\\start','\\\\stop'])
+$valid['en'].push(['\\\\begin','\\\\end'])
+$valid['en'].push(['\\\\Start','\\\\Stop'])
+$valid['en'].push(['\\\\Begin','\\\\End'])
+
+#$valid['nl'].push(['',''])
+$valid['nl'].push(['\\\\start','\\\\stop'])
+$valid['nl'].push(['\\\\beginvan','\\\\eindvan'])
+$valid['nl'].push(['\\\\Start','\\\\Stop'])
+$valid['nl'].push(['\\\\BeginVan','\\\\Eindvan'])
+
+$valid_tex = "\\\\end\(input|insert|csname|linechar|graf|buffer|strut\)"
+$valid_mp  = "(enddef||end||endinput)"
+
+$start_verbatim = Hash.new
+$stop_verbatim  = Hash.new
+
+$start_verbatim['en'] = '\\\\starttyping'
+$start_verbatim['nl'] = '\\\\starttypen'
+
+$stop_verbatim['en'] = '\\\\stoptyping'
+$stop_verbatim['nl'] = '\\\\stoptypen'
+
+def message(str, filename=nil, line=nil, column=nil)
+    if filename then
+        if line then
+            if column then
+                puts("error in file #{filename} at line #{line} in column #{column}: #{str}\n")
+            else
+                puts("error in file #{filename} at line #{line}: #{str}\n")
+            end
+        else
+            puts("file #{filename}: #{str}\n")
+        end
+    else
+        puts(str+"\n")
+    end
+end
+
+def load_file (filename='')
+    begin
+        data = IO.readlines(filename)
+        data.collect! do |d|
+            if d =~ /^\s*%/o then
+                ''
+            elsif d =~ /(.*?[^\\])%.*$/o then
+                $1
+            else
+                d
+            end
+        end
+    rescue
+        message("provide proper filename")
+        return nil
+    end
+    # print data.to_s + "\n"
+    return data
+end
+
+def guess_interface(data)
+    if data.first =~ /^%.*interface\=(.*)\s*/ then
+        return $1
+    else
+        data.each do |line|
+            case line
+                when /\\(starttekst|stoptekst|startonderdeel|startdocument|startoverzicht)/o then return 'nl'
+                when /\\(stelle|verwende|umgebung|benutze)/o                                 then return 'de'
+                when /\\(stel|gebruik|omgeving)/                                             then return 'nl'
+                when /\\(use|setup|environment)/                                             then return 'en'
+                when /\\(usa|imposta|ambiente)/                                              then return 'it'
+                when /(height|width|style)=/                                                 then return 'en'
+                when /(hoehe|breite|schrift)=/                                               then return 'de'
+                when /(hoogte|breedte|letter)=/                                              then return 'nl'
+                when /(altezza|ampiezza|stile)=/                                             then return 'it'
+                when /externfiguur/                                                          then return 'nl'
+                when /externalfigure/                                                        then return 'en'
+                when /externeabbildung/                                                      then return 'de'
+                when /figuraesterna/                                                         then return 'it'
+            end
+        end
+        return 'en'
+    end
+end
+
+def cleanup_data(data, interface='en')
+    verbatim = 0
+    re_start = /^\s*#{$start_verbatim[interface]}/
+    re_stop = /^\s*#{$stop_verbatim[interface]}/
+    data.collect! do |d|
+        if d =~ re_start then
+            verbatim += 1
+            if verbatim>1 then
+                ''
+            else
+                d
+            end
+        elsif d =~ re_stop then
+            verbatim -= 1
+            if verbatim>0 then
+                ''
+            else
+                d
+            end
+        elsif verbatim > 0 then
+            ''
+        else
+            d
+        end
+    end
+    return data
+end
+
+def load_valid(data, interface=nil)
+    if data && (data.first =~ /^%.*valid\=(.*)\s*/)
+        filename = $1
+        filename = '../' + filename unless test(?f,filename)
+        filename = '../' + filename unless test(?f,filename)
+        if test(?f,filename) then
+            interface = guess_interface(data) unless interface
+            if $valid.has_key?(interface) then
+                interface = $valid[interface]
+            else
+                interface = $valid['en']
+            end
+            begin
+                message("loading validation file",filename)
+                validkeys = Hash.new
+                line = 1
+                IO.readlines(filename).each do |l|
+                    if l =~ /\s+[\#\%]/io then
+                        # ignore line
+                    elsif l =~ /^\s*(begin|end)\s+interface\s+([a-z][a-z])/o then
+                        # not yet supported
+                    elsif l =~ /^\s*(\d+)\s+([a-zA-Z]*)$/o then
+                        type, key = $1.to_i, $2.strip
+                        if interface[type] then
+                            validkeys[interface[type].first+key] = true
+                            validkeys[interface[type].last+key]  = true
+                        else
+                            error_message(filename,line,nil,'wrong definition')
+                        end
+                    end
+                    line += 1
+                end
+                if validkeys then
+                    message("#{validkeys.length} validation keys loaded",filename)
+                end
+                return validkeys
+            rescue
+                message("invalid validation file",filename)
+            end
+        else
+            message("unknown validation file", filename)
+        end
+    else
+        message("no extra validation file specified")
+    end
+    return nil
+end
+
+def some_chr_error(data, filename, left, right)
+    levels = Array.new
+    for line in 0..data.length-1 do
+         str = data[line]
+         # str = data[line].gsub(/\\[\#{left}\#{right}]/,'')
+         column = 0
+         while column<str.length do
+            case str[column].chr
+                when "\%" then
+                    break
+                when "\\" then
+                    column += 2
+                when left then
+                    levels.push([line,column])
+                    column += 1
+                when right then
+                    if levels.pop
+                        column += 1
+                    else
+                        message("missing #{left} for #{right}",filename,line+1,column+1)
+                        return true
+                    end
+                else
+                    column += 1
+            end
+        end
+    end
+    if levels && levels.length>0 then
+        levels.each do |l|
+            column = l.pop
+            line = l.pop
+            message("missing #{right} for #{left}",filename,line+1,column+1)
+        end
+        return true
+    else
+        return false
+    end
+end
+
+def some_wrd_error(data, filename, start, stop, ignore)
+    levels = Array.new
+    len = 0
+    re_start = /[^\%]*(#{start})([a-zA-Z]*)/
+    re_stop = /[^\%]*(#{stop})([a-zA-Z]*)/
+    re_ignore = /#{ignore}.*/
+    str_start = start.gsub(/\\+/,'\\')
+    str_stop = stop.gsub(/\\+/,'\\')
+    line = 0
+    while line<data.length do
+        dataline = data[line].split(/[^\\A-Za-z]/)
+        if dataline.length>0 then
+            # todo: more on one line
+            dataline.each do |dataword|
+                case dataword
+                    when re_ignore then
+                        # just go on
+                    when re_start then
+                        levels.push([line,$2])
+                        # print ' '*levels.length + '>' + $2 + "\n"
+                    when re_stop then
+                        # print ' '*levels.length + '<' + $2 + "\n"
+                        if levels && levels.last && (levels.last[1] == $2) then
+                            levels.pop
+                        elsif levels && levels.last then
+                            message("#{str_stop}#{levels.last[1]} expected instead of #{str_stop}#{$2}",filename,line+1)
+                            return true
+                        else
+                            message("missing #{str_start}#{$2} for #{str_stop}#{$2}",filename,line+1)
+                            return true
+                        end
+                    else
+                        # just go on
+                end
+            end
+        end
+        line += 1
+    end
+    if levels && levels.length>0 then
+        levels.each do |l|
+            text = l.pop
+            line = l.pop
+            message("missing #{str_stop}#{text} for #{str_start}#{text}",filename,line+1)
+        end
+        return true
+    else
+        return false
+    end
+end
+
+def some_sym_error (data, filename, symbol, template=false)
+    saved = Array.new
+    inside = false
+    level = 0
+    for line in 0..data.length-1 do
+         str = data[line]
+         column = 0
+         while column<str.length do
+            case str[column].chr
+                when "[" then
+                    level += 1 if template
+                when "]" then
+                    level -= 1 if template && level > 0
+                when "\%" then
+                    break
+                when "\\" then
+                    column += 1
+                when symbol then
+                    if level == 0 then
+                        inside = ! inside
+                        saved = [line,column]
+                    else
+                        # we're in some kind of template or so
+                    end
+                else
+                    # go on
+            end
+            column += 1
+        end
+    end
+    if inside && saved && level == 0 then
+        column = saved.pop
+        line = saved.pop
+        message("missing #{symbol} for #{symbol}",filename,line+1)
+        return true
+    else
+        return false
+    end
+end
+
+def some_key_error(data, filename, valid)
+    return if (! valid) || (valid.length == 0)
+    error = false
+    # data.foreach do |line| ... end
+    for line in 0..data.length-1 do
+        data[line].scan(/\\([a-zA-Z]+)/io) do
+            unless valid.has_key?($1) then
+                message("unknown command \\#{$1}",filename,line+1)
+                error = true
+            end
+        end
+    end
+    return error
+end
+
+# todo : language dependent
+
+def check_file_tex (filename)
+    error = false
+    if data = load_file(filename) then
+        message("checking tex file", filename)
+        interface = guess_interface(data)
+        valid = load_valid(data,interface)
+        data = cleanup_data(data,interface)
+        # data.each do |d| print d  end
+        $valid[interface].each do |v|
+            if some_wrd_error(data, filename, v[0], v[1] ,$valid_tex) then
+                error = true
+                break
+            end
+        end
+        # return false if some_wrd_error(data, filename, '\\\\start'   , '\\\\stop'   , $valid_tex)
+        # return false if some_wrd_error(data, filename, '\\\\Start'   , '\\\\Stop'   , $valid_tex)
+        # return false if some_wrd_error(data, filename, '\\\\beginvan', '\\\\eindvan', $valid_tex)
+        # return false if some_wrd_error(data, filename, '\\\\begin'   , '\\\\end|\\\\eind', $valid_tex)
+        error = true if some_sym_error(data, filename, '$', false)
+        error = true if some_sym_error(data, filename, '|', true)
+        error = true if some_chr_error(data, filename, '{', '}')
+        error = true if some_chr_error(data, filename, '[', ']')
+        error = true if some_chr_error(data, filename, '(', ')')
+        error = true if some_key_error(data, filename, valid)
+        message("no errors in tex code", filename) unless error
+        return error
+    else
+        return false
+    end
+end
+
+def check_file_mp (filename)
+    error = false
+    if data = load_file(filename) then
+        message("checking metapost file", filename)
+        interface = guess_interface(data)
+        valid = load_valid(data,interface)
+        $valid[interface].each do |v|
+            if some_wrd_error(data, filename, v[0], v[1] ,$valid_tex) then
+                error = true
+                break
+            end
+        end
+        # return false if some_wrd_error(data, filename, '', 'begin', 'end', $valid_mp)
+        error = true if some_chr_error(data, filename, '{', '}')
+        error = true if some_chr_error(data, filename, '[', ']')
+        error = true if some_chr_error(data, filename, '(', ')')
+        error = true if some_key_error(data, filename, valid)
+        message("no errors in metapost code", filename) unless error
+        return error
+    else
+        return false
+    end
+end
+
+def check_file_text(filename='')
+    if data = load_file(filename) then
+        for line in 0..data.length-1 do
+            # case data[line]
+                # when /\s([\:\;\,\.\?\!])/ then
+                    # message("space before #{$1}",filename,line+1)
+                # when /\D([\:\;\,\.\?\!])\S/ then
+                    # message("no space after #{$1}",filename,line+1)
+            # end
+            if data[line] =~ /\s([\:\;\,\.\?\!])/ then
+                message("space before #{$1}",filename,line+1)
+            else
+                data[line].gsub!(/\[.*?\]/o, '')
+                data[line].gsub!(/\(.*?\)/o, '')
+                data[line].gsub!(/\[.*?$/o, '')
+                data[line].gsub!(/^.*?\]/o, '')
+                if data[line] =~ /\D([\:\;\,\.\?\!])\S/ then
+                    message("no space after #{$1}",filename,line+1)
+                end
+            end
+        end
+    end
+end
+
+def check_file(filename='')
+    case filename
+        when '' then
+            message("provide filename")
+            return false
+        when /\.(tex|mk.+)$/i then
+            return check_file_tex(filename) # && check_file_text(filename)
+        when /\.mp$/i then
+            return check_file_mp(filename)
+        else
+            message("only tex and metapost files are checked")
+            return false
+    end
+end
+
+if ARGV.size > 0 then
+    someerror = false
+    ARGV.each do |filename|
+         somerror = true if check_file(filename)
+    end
+    exit (if someerror then 1 else 0 end)
+else
+    exit 1
+end
+
diff --git a/scripts/context/ruby/ctxtools.rb b/scripts/context/ruby/ctxtools.rb
new file mode 100644
index 000000000..c1cbc7d20
--- /dev/null
+++ b/scripts/context/ruby/ctxtools.rb
@@ -0,0 +1,2785 @@
+#!/usr/bin/env ruby
+#encoding: ASCII-8BIT
+
+# program   : ctxtools
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2004-2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+# This script will harbor some handy manipulations on context
+# related files.
+
+# todo: move scite here
+#
+# todo: move kpse call to kpse class/module, faster and better
+
+# Taco Hoekwater on patterns and lig building (see 'agr'):
+#
+# Any direct use of a ligature (as accessed by \char or through active
+# characters) is wrong and will create faulty hyphenation. Normally,
+# when TeX sees "office", it has six tokens, and it knows from the
+# patterns that it can hyphenate between the "ff". It will build an
+# internal list of four nodes, like this:
+#
+# [char, o  , ffi ]
+# [lig , ffi, c   ,[f,f,i]]
+# [char, c  , e   ]
+# [char, e  , NULL]
+#
+# as you can see from the ffi line, it has remembered the original
+# characters. While hyphenating, it temporarily changes back to
+# that, then re-instates the ligature afterwards.
+#
+# If you feed it the ligature directly, like so:
+#
+# [char, o   , ffi ]
+# [char, ffi , c   ]
+# [char, c   , e   ]
+# [char, e   , NULL]
+#
+# it cannot do that (it tries to hyphenate as if the "ffi" was a
+# character), and the result is wrong hyphenation.
+
+banner = ['CtxTools', 'version 1.3.5', '2004/2008', 'PRAGMA ADE']
+
+$: << File.expand_path(File.dirname($0)) ; $: << File.join($:.last,'lib') ; $:.uniq!
+
+require 'base/switch'
+require 'base/logger'
+require 'base/system'
+require 'base/kpse'
+require 'base/file'
+
+require 'rexml/document'
+require 'net/http'
+require 'fileutils'
+# require 'ftools'
+require 'kconv'
+
+exit if defined?(REQUIRE2LIB)
+
+class String
+
+    def i_translate(element, attribute, category)
+        self.gsub!(/(<#{element}.*?#{attribute}=)([\"\'])(.*?)\2/) do
+            if category.key?($3) then
+                # puts "#{element} #{$3} -> #{category[$3]}\n" if element == 'cd:inherit'
+                # puts "#{element} #{$3} => #{category[$3]}\n" if element == 'cd:command'
+                "#{$1}#{$2}#{category[$3]}#{$2}"
+            else
+                # puts "#{element} #{$3} -> ?\n" if element == 'cd:inherit'
+                # puts "#{element} #{$3} => ?\n" if element == 'cd:command'
+                "#{$1}#{$2}#{$3}#{$2}" # unchanged
+            end
+        end
+    end
+
+    def i_load(element, category)
+        self.scan(/<#{element}.*?name=([\"\'])(.*?)\1.*?value=\1(.*?)\1/) do
+            category[$2] = $3
+        end
+    end
+
+    def nosuffix(suffix)
+        self.sub(/\.#{suffix}/,'') # no /o
+    end
+
+end
+
+class Commands
+
+    include CommandBase
+
+    public
+
+    def touchcontextfile
+        dowithcontextfile(1)
+    end
+
+    def contextversion
+        dowithcontextfile(2)
+    end
+
+    private
+
+    def dowithcontextfile(action)
+        maincontextfile = 'context.tex'
+        unless FileTest.file?(maincontextfile) then
+            begin
+                maincontextfile = Kpse.found(maincontextfile,'context')
+            rescue
+                maincontextfile = ''
+            end
+        end
+        unless maincontextfile.empty? then
+            nextcontextfile = maincontextfile.sub(/context\.tex$/,"cont-new.tex")
+            case action
+                when 1 then
+                    touchfile(maincontextfile)
+                    touchfile(nextcontextfile,@@newcontextversion)
+                when 2 then
+                    reportversion(maincontextfile)
+                    reportversion(nextcontextfile,@@newcontextversion)
+            end
+        end
+
+    end
+
+    @@contextversion    = "\\\\contextversion"
+    @@newcontextversion = "\\\\newcontextversion"
+
+    def touchfile(filename,command=@@contextversion)
+
+        if FileTest.file?(filename) then
+            if data = IO.read(filename) then
+                timestamp = Time.now.strftime('%Y.%m.%d %H:%M')
+                prevstamp = ''
+                begin
+                    data.gsub!(/#{command}\{(\d+\.\d+\.\d+.*?)\}/) do
+                        prevstamp = $1
+                        "#{command.sub(/(\\)+/,"\\")}{#{timestamp}}"
+                    end
+                rescue
+                else
+                    begin
+                        File.delete(filename+'.old')
+                    rescue
+                    end
+                    begin
+                        File.copy(filename,filename+'.old')
+                    rescue
+                    end
+                    begin
+                        if f = File.open(filename,'w') then
+                            f.puts(data)
+                            f.close
+                        end
+                    rescue
+                    end
+                end
+                if prevstamp.empty? then
+                    report("#{filename} is not updated, no timestamp found")
+                else
+                    report("#{filename} is updated from #{prevstamp} to #{timestamp}")
+                end
+            end
+        else
+            report("#{filename} is not found")
+        end
+
+    end
+
+    def reportversion(filename,command=@@contextversion)
+
+        version = 'unknown'
+        begin
+            if FileTest.file?(filename) && IO.read(filename).match(/#{command}\{(\d+\.\d+\.\d+.*?)\}/) then
+                version = $1
+            end
+        rescue
+        end
+        if @commandline.option("pipe") then
+            print version
+        else
+            report("context version: #{version} (#{filename})")
+        end
+
+    end
+
+end
+
+class Commands
+
+    include CommandBase
+
+    public
+
+    def jeditinterface
+        editinterface('jedit')
+    end
+
+    def bbeditinterface
+        editinterface('bbedit')
+    end
+
+    def sciteinterface
+        editinterface('scite')
+    end
+
+    def rawinterface
+        editinterface('raw')
+    end
+
+    private
+
+    def editinterface(type='raw')
+
+        return unless FileTest.file?("cont-en.xml")
+
+        interfaces = @commandline.arguments
+
+        if interfaces.empty? then
+            interfaces = ['en','cs','de','it','nl','ro','fr']
+        end
+
+        interfaces.each do |interface|
+            begin
+                collection = Hash.new
+                mappings   = Hash.new
+                if f = open("keys-#{interface}.xml") then
+                    while str = f.gets do
+                        if str =~ /\<cd\:command\s+name=\"(.*?)\"\s+value=\"(.*?)\".*?\>/o then
+                            mappings[$1] = $2
+                        end
+                    end
+                    f.close
+                    if f = open("cont-en.xml") then
+                        while str = f.gets do
+                            if str =~ /\<cd\:command\s+name=\"(.*?)\"\s+type=\"environment\".*?\>/o then
+                                collection["start#{mappings[$1]}"] = ''
+                                collection["stop#{mappings[$1]}"]  = ''
+                            elsif str =~ /\<cd\:command\s+name=\"(.*?)\".*?\>/o then
+                                collection["#{mappings[$1]}"] = ''
+                            end
+                        end
+                        f.close
+                        case type
+                            when 'jedit' then
+                                if f = open("context-jedit-#{interface}.xml", 'w') then
+                                    f.puts("<?xml version='1.0'?>\n\n")
+                                    f.puts("<!DOCTYPE MODE SYSTEM 'xmode.dtd'>\n\n")
+                                    f.puts("<MODE>\n")
+                                    f.puts("  <RULES>\n")
+                                    f.puts("    <KEYWORDS>\n")
+                                    collection.keys.sort.each do |name|
+                                        f.puts("      <KEYWORD2>\\#{name}</KEYWORD2>\n") unless name.empty?
+                                    end
+                                    f.puts("    </KEYWORDS>\n")
+                                    f.puts("  </RULES>\n")
+                                    f.puts("</MODE>\n")
+                                    f.close
+                                end
+                            when 'bbedit' then
+                                if f = open("context-bbedit-#{interface}.xml", 'w') then
+                                    f.puts("<?xml version='1.0'?>\n\n")
+                                    f.puts("<key>BBLMKeywordList</key>\n")
+                                    f.puts("<array>\n")
+                                    collection.keys.sort.each do |name|
+                                        f.puts("    <string>\\#{name}</string>\n") unless name.empty?
+                                    end
+                                    f.puts("</array>\n")
+                                    f.close
+                                end
+                            when 'scite' then
+                                if f = open("cont-#{interface}-scite.properties", 'w') then
+                                    i = 0
+                                    f.write("keywordclass.macros.context.#{interface}=")
+                                    collection.keys.sort.each do |name|
+                                        unless name.empty? then
+                                            if i==0 then
+                                                f.write("\\\n    ")
+                                                i = 5
+                                            else
+                                                i = i - 1
+                                            end
+                                            f.write("#{name} ")
+                                        end
+                                    end
+                                    f.write("\n")
+                                    f.close
+                                end
+                            else # raw
+                                collection.keys.sort.each do |name|
+                                    puts("\\#{name}\n") unless name.empty?
+                                end
+                        end
+                    end
+                end
+            end
+        end
+
+    end
+
+end
+
+# class Commands
+#
+#     include CommandBase
+#
+#     public
+#
+#     def translateinterface
+#
+#         # since we know what kind of file we're dealing with,
+#         # we do it quick and dirty instead of using rexml or
+#         # xslt
+#
+#         interfaces = @commandline.arguments
+#
+#         if interfaces.empty? then
+#             interfaces = ['cs','de','it','nl','ro','fr']
+#         else
+#             interfaces.delete('en')
+#         end
+#
+#         interfaces.flatten.each do |interface|
+#
+#             variables, constants, strings, list, data = Hash.new, Hash.new, Hash.new, '', ''
+#
+#             keyfile, intfile, outfile = "keys-#{interface}.xml", "cont-en.xml", "cont-#{interface}.xml"
+#
+#             report("generating #{keyfile}")
+#
+#             begin
+#                 one = "texexec --make --all #{interface}"
+#                 two = "texexec --batch --silent --interface=#{interface} x-set-01"
+#                 if @commandline.option("force") then
+#                     system(one)
+#                     system(two)
+#                 elsif not system(two) then
+#                     system(one)
+#                     system(two)
+#                 end
+#             rescue
+#             end
+#
+#             unless File.file?(keyfile) then
+#                 report("no #{keyfile} generated")
+#                 next
+#             end
+#
+#             report("loading #{keyfile}")
+#
+#             begin
+#                 list = IO.read(keyfile)
+#             rescue
+#                 list = empty
+#             end
+#
+#             if list.empty? then
+#                 report("error in loading #{keyfile}")
+#                 next
+#             end
+#
+#             list.i_load('cd:variable', variables)
+#             list.i_load('cd:constant', constants)
+#             list.i_load('cd:command' , strings)
+#             # list.i_load('cd:element' , strings)
+#
+#             report("loading #{intfile}")
+#
+#             begin
+#                 data = IO.read(intfile)
+#             rescue
+#                 data = empty
+#             end
+#
+#             if data.empty? then
+#                 report("error in loading #{intfile}")
+#                 next
+#             end
+#
+#             report("translating interface en to #{interface}")
+#
+#             data.i_translate('cd:string'   , 'value', strings)
+#             data.i_translate('cd:variable' , 'value', variables)
+#             data.i_translate('cd:parameter', 'name' , constants)
+#             data.i_translate('cd:constant' , 'type' , variables)
+#             data.i_translate('cd:variable' , 'type' , variables)
+#             data.i_translate('cd:inherit'  , 'name' , strings)
+#             # data.i_translate('cd:command'  , 'name' , strings)
+#
+#             data.gsub!(/(\<cd\:interface[^\>]*?language=")en(")/) do
+#                 $1 + interface + $2
+#             end
+#
+#             report("saving #{outfile}")
+#
+#             begin
+#                 if f = File.open(outfile, 'w') then
+#                     f.write(data)
+#                     f.close
+#                 end
+#             rescue
+#             end
+#
+#         end
+#
+#     end
+#
+# end
+
+class Commands
+
+    include CommandBase
+
+    public
+
+    # faster is to glob the whole dir and regexp over that list
+
+    def purgefiles
+
+        pattern  = @commandline.arguments
+        purgeall = @commandline.option("all")
+        recurse  = @commandline.option("recurse")
+
+        $dontaskprefixes.push(Dir.glob("mpx-*"))
+
+        if purgeall then
+            $dontaskprefixes.push(Dir.glob("*.tex.prep"))
+            $dontaskprefixes.push(Dir.glob("*.xml.prep"))
+        end
+
+        $dontaskprefixes.flatten!
+        $dontaskprefixes.sort!
+
+        if purgeall then
+          $forsuresuffixes.push($texnonesuffixes)
+          $texnonesuffixes = []
+          $forsuresuffixes.flatten!
+        end
+
+        if ! pattern || pattern.empty? then
+            globbed = if recurse then "**/*.*" else "*.*" end
+            files = Dir.glob(globbed)
+            report("purging#{if purgeall then ' all' end} temporary files : #{globbed}")
+        else
+            report("purging#{if purgeall then ' all' end} temporary files : #{pattern.join(' ')}")
+            pattern.each do |pat|
+                nosuf = File.unsuffixed(pat)
+                globbed = if recurse then "**/#{nosuf}-*.*" else "#{nosuf}-*.*" end
+                report("checking files that match '#{globbed}'")
+                files = Dir.glob(globbed)
+                globbed = if recurse then "**/#{nosuf}.*" else "#{nosuf}.*" end
+                report("checking files that match '#{globbed}'")
+                files.push(Dir.glob(globbed))
+            end
+        end
+        files.flatten!
+        files.sort!
+
+        $dontaskprefixes.each do |file|
+            removecontextfile(file)
+        end
+        $dontasksuffixes.each do |suffix|
+            files.each do |file|
+                removecontextfile(file) if file =~ /#{suffix}$/i
+            end
+        end
+        $forsuresuffixes.each do |suffix|
+            files.each do |file|
+                removecontextfile(file) if file =~ /\.#{suffix}$/i
+            end
+        end
+        files.each do |file|
+            if file =~ /(.*?)\.\d+$/o then
+                basename = $1
+                if file =~ /mp(graph|run)/o || FileTest.file?("#{basename}.mp") then
+                    removecontextfile($file)
+                end
+            end
+        end
+        $dummyfiles.each do |file|
+            (File.delete(file) if (FileTest.size?(file) rescue 10) < 10) rescue false
+        end
+        $texnonesuffixes.each do |suffix|
+            files.each do |file|
+                if file =~ /(.*)\.#{suffix}$/i then
+                    if FileTest.file?("#{$1}.tex") || FileTest.file?("#{$1}.xml") || FileTest.file?("#{$1}.fo") then
+                        keepcontextfile(file)
+                    else
+                        strippedname = $1.gsub(/\-[a-z]$/io, '')
+                        if FileTest.file?("#{strippedname}.tex") || FileTest.file?("#{strippedname}.xml") then
+                            keepcontextfile("#{file} (potential result file)")
+                        else
+                            removecontextfile(file)
+                        end
+                    end
+                end
+            end
+        end
+
+        files = Dir.glob("*.*")
+        $dontasksuffixes.each do |suffix|
+            files.each do |file|
+                removecontextfile(file) if file =~ /^#{suffix}$/i
+            end
+        end
+
+        if $removedfiles || $keptfiles || $persistentfiles then
+            report("removed files : #{$removedfiles}")
+            report("kept files : #{$keptfiles}")
+            report("persistent files : #{$persistentfiles}")
+            report("reclaimed bytes : #{$reclaimedbytes}")
+        end
+
+    end
+
+    private
+
+    $removedfiles    = 0
+    $keptfiles       = 0
+    $persistentfiles = 0
+    $reclaimedbytes  = 0
+
+    $dontaskprefixes = [
+        # "tex-form.tex", "tex-edit.tex", "tex-temp.tex",
+        "texexec.tex", "texexec.tui", "texexec.tuo",
+        "texexec.tuc", "texexec.tua",
+        "texexec.ps", "texexec.pdf", "texexec.dvi",
+        "cont-opt.tex", "cont-opt.bak"
+    ]
+    $dontasksuffixes = [
+        "mp(graph|run)\\.mp", "mp(graph|run)\\.mpd", "mp(graph|run)\\.mpo", "mp(graph|run)\\.mpy",
+        "mp(graph|run)\\.\\d+", "mp(graph|run)\\.mp.keep",
+        "xlscript\\.xsl"
+    ]
+    $forsuresuffixes = [
+        "tui", "tua", "tup", "ted", "tes", "top",
+        "log", "tmp", "run", "bck", "rlg",
+        "mpt", "mpx", "mpd", "mpo", "mpb",
+        "ctl",
+        "pgf", "synctex.gz",
+        "tmp.md5", "tmp.out"
+    ]
+    $texonlysuffixes = [
+        "dvi", "ps", "pdf"
+    ]
+    $texnonesuffixes = [
+        "tuo", "tub", "top", "tuc"
+    ]
+    $dummyfiles = [
+        "mpgraph"
+    ]
+
+    def removecontextfile (filename)
+        if filename && FileTest.file?(filename) then
+            begin
+                filesize = FileTest.size(filename)
+                File.delete(filename)
+            rescue
+                report("problematic : #{filename}")
+            else
+                if FileTest.file?(filename) then
+                    $persistentfiles += 1
+                    report("persistent : #{filename}")
+                else
+                    $removedfiles += 1
+                    $reclaimedbytes += filesize
+                    report("removed : #{filename}")
+                end
+            end
+        end
+    end
+
+    def keepcontextfile (filename)
+        if filename && FileTest.file?(filename)  then
+            $keptfiles += 1
+            report("not removed : #{filename}")
+        end
+    end
+
+end
+
+#D Documentation can be woven into a source file. The next
+#D routine generates a new, \TEX\ ready file with the
+#D documentation and source fragments properly tagged. The
+#D documentation is included as comment:
+#D
+#D \starttypen
+#D %D ......  some kind of documentation
+#D %M ......  macros needed for documenation
+#D %S B       begin skipping
+#D %S E       end skipping
+#D \stoptypen
+#D
+#D The most important tag is \type {%D}. Both \TEX\ and \METAPOST\
+#D files use \type{%} as a comment chacacter, while \PERL, \RUBY\
+#D and alike use \type{#}. Therefore \type{#D} is also handled.
+#D
+#D The generated file gets the suffix \type{ted} and is
+#D structured as:
+#D
+#D \starttypen
+#D \startmodule[type=suffix]
+#D \startdocumentation
+#D \stopdocumentation
+#D \startdefinition
+#D \stopdefinition
+#D \stopmodule
+#D \stoptypen
+#D
+#D Macro definitions specific to the documentation are not
+#D surrounded by start||stop commands. The suffix specifaction
+#D can be overruled at runtime, but defaults to the file
+#D extension. This specification can be used for language
+#D depended verbatim typesetting.
+
+class Commands
+
+    include CommandBase
+
+    public
+
+    def documentation
+        files = @commandline.arguments
+        processtype = @commandline.option("type")
+        files.each do |fullname|
+            if fullname =~ /(.*)\.(.+?)$/o then
+                filename, filesuffix = $1, $2
+            else
+                filename, filesuffix = fullname, 'tex'
+            end
+            filesuffix = 'tex' if filesuffix.empty?
+            fullname, resultname = "#{filename}.#{filesuffix}", "#{filename}.ted"
+            if ! FileTest.file?(fullname)
+                report("empty input file #{fullname}")
+            elsif ! tex = File.open(fullname)
+                report("invalid input file #{fullname}")
+            elsif ! ted = File.open(resultname,'w') then
+                report("unable to openresult file #{resultname}")
+            else
+                report("input file : #{fullname}")
+                report("output file : #{resultname}")
+                nofdocuments, nofdefinitions, nofskips = 0, 0, 0
+                skiplevel, indocument, indefinition, skippingbang = 0, false, false, false
+                if processtype.empty? then
+                  filetype = filesuffix.downcase.sub(/^mk.+$/,'tex') # make sure that mkii and mkiv files are handled
+                else
+                  filetype = processtype.downcase
+                end
+                report("filetype : #{filetype}")
+                # we need to signal to texexec what interface to use
+                firstline = tex.gets
+                if firstline =~ /^\%.*interface\=/ then
+                  ted.puts(firstline)
+                else
+                  tex.rewind # seek(0)
+                end
+                ted.puts("\\startmodule[type=#{filetype}]\n")
+                while str = tex.gets do
+                    if skippingbang then
+                        skippingbang = false
+                    else
+                        str.chomp!
+                        str.sub!(/\s*$/o, '')
+                        case str
+                            when /^[%\#]D($| )/io then
+                                if skiplevel == 0 then
+                                    someline = if str.length < 3 then "" else str[3,str.length-1] end
+                                    if indocument then
+                                        ted.puts("#{someline}\n")
+                                    else
+                                        if indefinition then
+                                            ted.puts("\\stopdefinition\n")
+                                            indefinition = false
+                                        end
+                                        unless indocument then
+                                            ted.puts("\n\\startdocumentation\n")
+                                        end
+                                        ted.puts("#{someline}\n")
+                                        indocument = true
+                                        nofdocuments += 1
+                                    end
+                                end
+                            when /^[%\#]M($| )/io then
+                                if skiplevel == 0 then
+                                    someline = if str.length < 3 then "" else str[3,str.length-1] end
+                                    ted.puts("#{someline}\n")
+                                end
+                            when /^[%\%]S B/io then
+                                skiplevel += 1
+                                nofskips += 1
+                            when /^[%\%]S E/io then
+                                  skiplevel -= 1
+                            when /^[%\#]/io then
+                                  #nothing
+                            when /^eval \'\(exit \$\?0\)\' \&\& eval \'exec perl/o then
+                                skippingbang = true
+                            else
+                                if skiplevel == 0 then
+                                    inlocaldocument = indocument
+                                    inlocaldocument = false # else first line skipped when not empty
+                                    someline = str
+                                    if indocument then
+                                        ted.puts("\\stopdocumentation\n")
+                                        indocument = false
+                                    end
+                                    if indefinition then
+                                        if someline.empty? then
+                                            ted.puts("\\stopdefinition\n")
+                                            indefinition = false
+                                        else
+                                            ted.puts("#{someline}\n")
+                                        end
+                                    elsif ! someline.empty? then
+                                        ted.puts("\n\\startdefinition\n")
+                                        indefinition = true
+                                        if inlocaldocument then
+                                            # nothing
+                                        else
+                                            nofdefinitions += 1
+                                            ted.puts("#{someline}\n")
+                                        end
+                                    end
+                                end
+                        end
+                    end
+                end
+                if indocument then
+                    ted.puts("\\stopdocumentation\n")
+                end
+                if indefinition then
+                    ted.puts("\\stopdefinition\n")
+                end
+                ted.puts("\\stopmodule\n")
+                ted.close
+
+                if nofdocuments == 0 && nofdefinitions == 0 then
+                    begin
+                        File.delete(resultname)
+                    rescue
+                    end
+                end
+                report("documentation sections : #{nofdocuments}")
+                report("definition sections : #{nofdefinitions}")
+                report("skipped sections : #{nofskips}")
+            end
+        end
+    end
+
+end
+
+#D This feature was needed when \PDFTEX\ could not yet access page object
+#D numbers (versions prior to 1.11).
+
+class Commands
+
+    include CommandBase
+
+    public
+
+    def filterpages # temp feature / no reporting
+        filename = @commandline.argument('first')
+        filename.sub!(/\.([a-z]+?)$/io,'')
+        pdffile = "#{filename}.pdf"
+        tuofile = "#{filename}.tuo"
+        if FileTest.file?(pdffile) then
+            begin
+                prevline, n = '', 0
+                if (pdf = File.open(pdffile)) && (tuo = File.open(tuofile,'a')) then
+                    report('filtering page object numbers')
+                    pdf.binmode
+                    while line = pdf.gets do
+                        line.chomp
+                        # typical pdftex search
+                        if (line =~ /\/Type \/Page/o) && (prevline =~ /^(\d+)\s+0\s+obj/o) then
+                            p = $1
+                            n += 1
+                            tuo.puts("\\objectreference{PDFP}{#{n}}{#{p}}{#{n}}\n")
+                        else
+                            prevline = line
+                        end
+                    end
+                end
+                pdf.close
+                tuo.close
+                report("number of pages : #{n}")
+            rescue
+                report("fatal error in filtering pages")
+            end
+        end
+    end
+
+end
+
+# This script is used to generate hyphenation pattern files
+# that suit ConTeXt. One reason for independent files is that
+# over the years too many uncommunicated changes took place
+# as well that inconsistency in content, naming, and location
+# in the texmf tree takes more time than I'm willing to spend
+# on it. Pattern files are normally shipped for LaTeX (and
+# partially plain). A side effect of independent files is that
+# we can make them encoding independent.
+#
+# Maybe I'll make this hyptools.tex
+
+class String
+
+    def markbraces
+        level = 0
+        self.gsub(/([\{\}])/o) do |chr|
+            if    chr == '{' then
+                level = level + 1
+                chr = "((+#{level}))"
+            elsif chr == '}' then
+                chr = "((-#{level}))"
+                level = level - 1
+            end
+            chr
+        end
+    end
+
+    def unmarkbraces
+        self.gsub(/\(\(\+\d+?\)\)/o) do
+            "{"
+        end .gsub(/\(\(\-\d+?\)\)/o) do
+            "}"
+        end
+    end
+
+    def getargument(pattern)
+        if self =~ /(#{pattern})\s*\(\(\+(\d+)\)\)(.*?)\(\(\-\2\)\)/m then # no /o
+            return $3
+        else
+            return ""
+        end
+    end
+
+    def withargument(pattern, &block)
+        if self.markbraces =~ /^(.*)(#{pattern}\s*)\(\(\+(\d+)\)\)(.*?)\(\(\-\3\)\)(.*)$/m then # no /o
+            "#{$1.unmarkbraces}#{$2}{#{yield($4.unmarkbraces)}}#{$5.unmarkbraces}"
+        else
+            self
+        end
+    end
+
+    def filterargument(pattern, &block)
+        if self.markbraces =~ /^(.*)(#{pattern}\s*)\(\(\+(\d+)\)\)(.*?)\(\(\-\3\)\)(.*)$/m then # no /o
+            yield($4.unmarkbraces)
+        else
+            self
+        end
+    end
+
+end
+
+class Language
+
+    include CommandBase
+
+    def initialize(commandline=nil, language='en', filenames=nil, encoding='ec')
+        @commandline= commandline
+        @language = language
+        @filenames = filenames
+        @remapping = Array.new
+        @demapping = Array.new
+        @cloning = Array.new
+        @unicode = Hash.new
+        @encoding = encoding
+        @data = ''
+        @read = ''
+        preload_accents()
+        preload_unicode() if @commandline.option('utf8')
+        case @encoding.downcase
+            when 't1', 'ec', 'cork' then preload_vector('ec',       'enco-ec.tex')
+            when 'y', 'texnansi'    then preload_vector('texnansi', 'enco-ans.tex')
+            when 'agr', 'agreek'    then preload_vector('agr',      'enco-agr.tex')
+            when 't2a'              then preload_vector('t2a',      'enco-cyr.tex')
+            when 'cyr'              then preload_vector() # somehow loading t2a does not work out well
+        end
+    end
+
+    def report(str)
+        if @commandline then
+            @commandline.report(str)
+        else
+            puts("#{str}\n")
+        end
+    end
+
+    def remap(from, to)
+        @remapping.push([from,to])
+    end
+    def demap(from, to)
+        @demapping.push([from,to])
+    end
+    def clone(from, to)
+        @cloning.push([from,to])
+    end
+
+    def load(filenames=@filenames)
+        found = false
+        begin
+            if filenames then
+                @filenames.each do |fileset|
+                    [fileset].flatten.each do |filename|
+                        begin
+                            if fname = located(filename) then
+                                data = IO.read(fname)
+                                @data += data.gsub(/\%.*$/, '').gsub(/\\message\{.*?\}/, '')
+                                data.gsub!(/(\\patterns|\\hyphenation)\s*\{.*/mo) do '' end
+                                @read += "\n% preamble of file #{fname}\n\n#{data}\n"
+                                @data.gsub!(/^[\s\n]+$/moi, '')
+                                report("file #{fname} is loaded")
+                                found = true
+                                break # next fileset
+                            end
+                        rescue
+                            report("file #{filename} is not readable")
+                        end
+                    end
+                end
+            end
+        rescue
+        end
+        return found
+    end
+
+    def valid?
+        ! @data.empty?
+    end
+
+    def convert
+        if @data then
+            n = 0
+            if true then
+                report("")
+                ["\\patterns","\\hyphenation"].each do |what|
+                    @data = @data.withargument(what) do |content|
+                        report("converting #{what}")
+                        report("")
+                        @demapping.each_index do |i|
+                            content.gsub!(@demapping[i][0], @demapping[i][1])
+                        end
+                        content.gsub!(/\\delete\{.*?\}/o) do '' end
+                        content.gsub!(/\\keep\{(.*?)\}/o) do $1 end
+                        done = false
+                        @remapping.each_index do |i|
+                            from, to, m = @remapping[i][0], @remapping[i][1], 0
+                            content.gsub!(from) do
+                                done = true
+                                m += 1
+                                "[#{i}]"
+                            end
+                            report("#{m.to_s.rjust(5)} entries remapped to #{to}") unless m == 0
+                            n += m
+                        end
+                        content.gsub!(/\[(\d+)\]/o) do
+                            @remapping[$1.to_i][1]
+                        end
+                        report("      nothing remapped") unless done
+                        @cloning.each_index do |i|
+                            c = 0
+                            f, s = @cloning[i][0], @cloning[i][1]
+                            str = "#{f}|#{s}"
+                            str.gsub!(/([\[\]])/) do "\\" + "#{$1}" end
+                            reg = /(#{str})/
+                            content.gsub!(/(\S*(#{str})\S*)/) do
+                                a, b = $1, $1
+                                a.gsub!(reg, f)
+                                b.gsub!(reg, s)
+                                c = c + 1
+                                "#{a} #{b}"
+                            end
+                            report("#{c.to_s.rjust(5)} times #{f} cloned to #{s}")
+                            n += c
+                        end
+                        report("")
+                        content.to_s
+                    end
+                end
+            else
+                @remapping.each do |k|
+                    from, to, m = k[0], k[1], 0
+                    @data.gsub!(from) do
+                        m += 1
+                        to
+                    end
+                    report("#{m.to_s.rjust(5)} entries remapped to #{to}") unless m == 0
+                    n += m
+                end
+            end
+            report("#{n} changes in patterns and exceptions")
+            if @commandline.option('utf8') then
+                n = 0
+                @data.gsub!(/\[(.*?)\]/o) do
+                    n += 1
+                    @unicode[$1] || $1
+                end
+                report("#{n} unicode utf8 entries")
+            end
+            return true
+        else
+            return false
+        end
+    end
+
+    def comment(str)
+        str.gsub!(/^\n/o, '')
+        str.chomp!
+        if @commandline.option('xml') then
+            "<!-- #{str.strip} -->\n\n"
+        else
+            "% #{str.strip}\n\n"
+        end
+    end
+
+    def content(tag, str)
+        lst = str.split(/\s+/)
+        lst.collect! do |l|
+            l.strip
+        end
+        if lst.length>0 then
+            lst = "\n#{lst.join("\n")}\n"
+        else
+            lst = ""
+        end
+        if @commandline.option('xml') then
+            lst.gsub!(/\[(.*?)\]/o) do
+                "&#{$1};"
+            end
+            "<#{tag}>#{lst}</#{tag}>\n\n"
+        else
+            "\\#{tag} \{#{lst}\}\n\n"
+        end
+    end
+
+    def banner
+        if @commandline.option('xml') then
+            "<?xml version='1.0' standalone='yes' ?>\n\n"
+        end
+    end
+
+    def triggerunicode
+        return
+        if @commandline.option('utf8') then
+            "% xetex needs utf8 encoded patterns and for patterns\n" +
+            "% coded as such we need to enable this regime when\n" +
+            "% not in xetex; this code will be moved into context\n" +
+            "% as soon as we've spread the generic patterns\n" +
+            "\n" +
+            "\\ifx\\XeTeXversion\\undefined \\else\n" +
+            "  \\ifx\\enableregime\\undefined \\else\n" +
+            "    \\enableregime[utf]\n" +
+            "  \\fi\n" +
+            "\\fi\n" +
+            "\n"
+        end
+    end
+
+    def save
+        xml = @commandline.option("xml")
+
+        patname = "lang-#{@language}.pat"
+        hypname = "lang-#{@language}.hyp"
+        rmename = "lang-#{@language}.rme"
+        logname = "lang-#{@language}.log"
+
+        desname = "lang-all.xml"
+
+        @data.gsub!(/\\[nc]\{(.+?)\}/)  do $1    end
+        @data.gsub!(/\{\}/)             do ''    end
+        @data.gsub!(/\n+/mo)            do "\n"  end
+        @read.gsub!(/\n+/mo)            do "\n"  end
+
+        description = ''
+        commentfile = rmename.dup
+
+        begin
+            desfile = Kpse.found(desname,'context')
+            if f = File.new(desfile) then
+                if doc = REXML::Document.new(f) then
+                    if e = REXML::XPath.first(doc.root,"/descriptions/description[@language='#{@language}']") then
+                        description = e.to_s
+                    end
+                end
+            end
+        rescue
+            description = ''
+        else
+            unless description.empty? then
+                commentfile = desname.dup
+                str  = "<!-- copied from lang-all.xml\n\n"
+                str << "<?xml version='1.0' standalone='yes'?>\n\n"
+                str << description.chomp
+                str << "\n\nend of copy -->\n"
+                str.gsub!(/^/io, "% ") unless @commandline.option('xml')
+                description =  comment("begin description data")
+                description << str + "\n"
+                description << comment("end description data")
+                report("description found for language #{@language}")
+            end
+        end
+
+        begin
+            if description.empty? || @commandline.option('log') then
+                if f = File.open(logname,'w') then
+                    report("saving #{@remapping.length} remap patterns in #{logname}")
+                    @remapping.each do |m|
+                        f.puts("#{m[0].inspect} => #{m[1]}\n")
+                    end
+                    f.close
+                end
+            else
+                File.delete(logname) if FileTest.file?(logname)
+            end
+        rescue
+        end
+
+        begin
+            if description.empty? || @commandline.option('log') then
+                if f = File.open(rmename,'w') then
+                    data = @read.dup
+                    data.gsub!(/(\s*\n\s*)+/mo, "\n")
+                    f << comment("comment copied from public hyphenation files}")
+                    f << comment("source of data: #{@filenames.join(' ')}")
+                    f << comment("begin original comment")
+                    f << "#{data}\n"
+                    f << comment("end original comment")
+                    f.close
+                    report("comment saved in file #{rmename}")
+                else
+                    report("file #{rmename} is not writable")
+                end
+            else
+                File.delete(rmename) if FileTest.file?(rmename)
+            end
+        rescue
+        end
+
+        begin
+            if f = File.open(patname,'w') then
+                data = ''
+                @data.filterargument('\\patterns') do |content|
+                    report("merging patterns")
+                    data += content.strip
+                end
+                data.gsub!(/(\s*\n\s*)+/mo, "\n")
+
+                f << banner
+                f << comment("context pattern file, see #{commentfile} for original comment")
+                f << comment("source of data: #{@filenames.join(' ')}")
+                f << description
+                f << comment("begin pattern data")
+                f << triggerunicode
+                f << content('patterns', data)
+                f << comment("end pattern data")
+                f.close
+                report("patterns saved in file #{patname}")
+            else
+                report("file #{patname} is not writable")
+            end
+        rescue
+            report("problems with file #{patname}")
+        end
+
+        begin
+            if f = File.open(hypname,'w') then
+                data = ''
+                @data.filterargument('\\hyphenation') do |content|
+                    report("merging exceptions")
+                    data += content.strip
+                end
+                data.gsub!(/(\s*\n\s*)+/mo, "\n")
+                f << banner
+                f << comment("context hyphenation file, see #{commentfile} for original comment")
+                f << comment("source of data: #{@filenames.join(' ')}")
+                f << description
+                f << comment("begin hyphenation data")
+                f << triggerunicode
+                f << content('hyphenation', data)
+                f << comment("end hyphenation data")
+                f.close
+                report("exceptions saved in file #{hypname}")
+            else
+                report("file #{hypname} is not writable")
+            end
+        rescue
+            report("problems with file #{hypname}")
+        end
+    end
+
+    def process
+        load
+        if valid? then
+            convert
+            save
+        else
+            report("aborted due to missing files")
+        end
+    end
+
+    def Language::generate(commandline, language='', filenames='', encoding='ec')
+        if ! language.empty? && ! filenames.empty? then
+            commandline.report("processing language #{language}")
+            commandline.report("")
+            language = Language.new(commandline,language,filenames,encoding)
+            if language.load then
+                language.convert
+                language.save
+                commandline.report("")
+            end
+        end
+    end
+
+    private
+
+    def located(filename)
+        begin
+            ["context","plain","latex"].each do |name| # fallbacks needed for czech patterns
+                fname = Kpse.found(filename, name)
+                if FileTest.file?(fname) then
+                    report("using file #{fname}")
+                    return fname
+                end
+            end
+            report("file #{filename} is not present")
+            return nil
+        rescue
+            report("file #{filename} cannot be located using kpsewhich")
+            return nil
+        end
+    end
+
+    def preload_accents
+
+        begin
+            if filename = located("enco-acc.tex") then
+                if data = IO.read(filename) then
+                    report("preloading accent conversions")
+                    data.scan(/\\defineaccent\s*\\*(.+?)\s*\{*(.+?)\}*\s*\{\\(.+?)\}/o) do
+                        one, two, three = $1, $2, $3
+                        one.gsub!(/[\`\~\!\^\*\_\-\+\=\:\;\"\'\,\.\?]/o) do
+                            "\\#{one}"
+                        end
+                        remap(/\\#{one} #{two}/, "[#{three}]")
+                        remap(/\\#{one}#{two}/, "[#{three}]")  unless one =~ /[a-zA-Z]/o
+                        remap(/\\#{one}\{#{two}\}/, "[#{three}]")
+                    end
+                end
+            end
+        rescue
+        end
+
+    end
+
+    def preload_unicode
+
+        # \definecharacter Agrave {\uchar0{192}}
+
+        begin
+            if filename = located("enco-uc.tex") then
+                if data = IO.read(filename) then
+                    report("preloading unicode conversions")
+                    data.scan(/\\definecharacter\s*(.+?)\s*\{\\uchar\{*(\d+)\}*\s*\{(\d+)\}/o) do
+                        one, two, three = $1, $2.to_i, $3.to_i
+                        @unicode[one] = [(two*256 + three)].pack("U")
+                    end
+                end
+            end
+        rescue
+            report("error in loading unicode mapping (#{$!})")
+        end
+
+    end
+
+    def preload_vector(encoding='', filename='')
+
+        # funny polish
+
+        case @language
+            when 'pl' then
+                remap(/\/a/, "[aogonek]")    ; remap(/\/A/, "[Aogonek]")
+                remap(/\/c/, "[cacute]")     ; remap(/\/C/, "[Cacute]")
+                remap(/\/e/, "[eogonek]")    ; remap(/\/E/, "[Eogonek]")
+                remap(/\/l/, "[lstroke]")    ; remap(/\/L/, "[Lstroke]")
+                remap(/\/n/, "[nacute]")     ; remap(/\/N/, "[Nacute]")
+                remap(/\/o/, "[oacute]")     ; remap(/\/O/, "[Oacute]")
+                remap(/\/s/, "[sacute]")     ; remap(/\/S/, "[Sacute]")
+                remap(/\/x/, "[zacute]")     ; remap(/\/X/, "[Zacute]")
+                remap(/\/z/, "[zdotaccent]") ; remap(/\/Z/, "[Zdotaccent]")
+            when 'sl' then
+                remap(/\"c/,"[ccaron]")  ; remap(/\"C/,"[Ccaron]")
+                remap(/\"s/,"[scaron]")  ; remap(/\"S/,"[Scaron]")
+                remap(/\"z/,"[zcaron]")  ; remap(/\"Z/,"[Zcaron]")
+            when 'da' then
+                remap(/X/, "[aeligature]")
+                remap(/Y/, "[ostroke]")
+                remap(/Z/, "[aring]")
+            when 'hu' then
+                # nothing
+            when 'ca' then
+                demap(/\\c\{/, "\\delete{")
+            when 'de', 'deo' then
+                demap(/\\c\{/, "\\delete{")
+                demap(/\\n\{/, "\\keep{")
+                remap(/\\3/, "[ssharp]")
+                remap(/\\9/, "[ssharp]")
+                remap(/\"a/, "[adiaeresis]")
+                remap(/\"o/, "[odiaeresis]")
+                remap(/\"u/, "[udiaeresis]")
+            when 'fr' then
+                demap(/\\n\{/, "\\delete{")
+                remap(/\\ae/, "[aeligature]")
+                remap(/\\oe/, "[oeligature]")
+            when 'la' then
+                # \lccode`'=`' somewhere else, todo
+                demap(/\\c\{/, "\\delete{")
+                remap(/\\a\s*/, "[aeligature]")
+                remap(/\\o\s*/, "[oeligature]")
+            when 'agr' then
+                # bug fix
+                remap("a2|", "[greekalphaiotasub]")
+                remap("h2|", "[greeketaiotasub]")
+                remap("w2|", "[greekomegaiotasub]")
+         		remap(">2r1<2r", "[2ῤ1ῥ]")
+                remap(">a2n1wdu'", "[ἀ2ν1ωδύ]")
+                remap(">e3s2ou'", "[ἐ3σ2ού]")
+                # main conversion
+                remap(/\<\'a\|/, "[greekalphaiotasubdasiatonos]")
+                # remap(/\<\'a\|/, "[greekdasiatonos][greekAlpha][greekiota]")
+                remap(/\>\'a\|/, "[greekalphaiotasubpsilitonos]")
+                remap(/\<\`a\|/, "[greekalphaiotasubdasiavaria]")
+                remap(/\>\`a\|/, "[greekalphaiotasubpsilivaria]")
+                remap(/\<\~a\|/, "[greekalphaiotasubdasiaperispomeni]")
+                remap(/\>\~a\|/, "[greekalphaiotasubpsiliperispomeni]")
+                remap(/\'a\|/,   "[greekalphaiotasubtonos]")
+                remap(/\`a\|/,   "[greekalphaiotasubvaria]")
+                remap(/\~a\|/,   "[greekalphaiotasubperispomeni]")
+                remap(/\<a\|/,   "[greekalphaiotasubdasia]")
+                remap(/\>a\|/,   "[greekalphaiotasubpsili]")
+                remap(/a\|/,     "[greekalphaiotasub]")
+                remap(/\<\'h\|/, "[greeketaiotasubdasiatonos]")
+                remap(/\>\'h\|/, "[greeketaiotasubpsilitonos]")
+                remap(/\<\`h\|/, "[greeketaiotasubdasiavaria]")
+                remap(/\>\`h\|/, "[greeketaiotasubpsilivaria]")
+                remap(/\<\~h\|/, "[greeketaiotasubdasiaperispomeni]")
+                remap(/\>\~h\|/, "[greeketaiotasubpsiliperispomeni]")
+                remap(/\'h\|/,   "[greeketaiotasubtonos]")
+                remap(/\`h\|/,   "[greeketaiotasubvaria]")
+                remap(/\~h\|/,   "[greeketaiotasubperispomeni]")
+                remap(/\<h\|/,   "[greeketaiotasubdasia]")
+                remap(/\>h\|/,   "[greeketaiotasubpsili]")
+                remap(/h\|/,     "[greeketaiotasub]")
+                remap(/\<'w\|/,  "[greekomegaiotasubdasiatonos]")
+                remap(/\>'w\|/,  "[greekomegaiotasubpsilitonos]")
+                remap(/\<`w\|/,  "[greekomegaiotasubdasiavaria]")
+                remap(/\>`w\|/,  "[greekomegaiotasubpsilivaria]")
+                remap(/\<~w\|/,  "[greekomegaiotasubdasiaperispomeni]")
+                remap(/\>~w\|/,  "[greekomegaiotasubpsiliperispomeni]")
+                remap(/\<w\|/,   "[greekomegaiotasubdasia]")
+                remap(/\>w\|/,   "[greekomegaiotasubpsili]")
+                remap(/\'w\|/,   "[greekomegaiotasubtonos]")
+                remap(/\`w\|/,   "[greekomegaiotasubvaria]")
+                remap(/\~w\|/,   "[greekomegaiotasubperispomeni]")
+                remap(/w\|/,     "[greekomegaiotasub]")
+                remap(/\<\'i/,   "[greekiotadasiatonos]")
+                remap(/\>\'i/,   "[greekiotapsilitonos]")
+                remap(/\<\`i/,   "[greekiotadasiavaria]")
+                remap(/\>\`i/,   "[greekiotapsilivaria]")
+                remap(/\<\~i/,   "[greekiotadasiaperispomeni]")
+                remap(/\>\~i/,   "[greekiotapsiliperispomeni]")
+                remap(/\"\'i/,   "[greekiotadialytikatonos]")
+                remap(/\"\`i/,   "[greekiotadialytikavaria]")
+                remap(/\"\~i/,   "[greekiotadialytikaperispomeni]")
+                remap(/\<i/,     "[greekiotadasia]")
+                remap(/\>i/,     "[greekiotapsili]")
+                remap(/\'i/,     "[greekiotaoxia]")
+                remap(/\`i/,     "[greekiotavaria]")
+                remap(/\~i/,     "[greekiotaperispomeni]")
+                remap(/\"i/,     "[greekiotadialytika]")
+                remap(/\>\~e/,   "[greekepsilonpsiliperispomeni]")
+                remap(/\<\~e/,   "[greekepsilondasiaperispomeni]")
+                remap(/\<\'e/,   "[greekepsilondasiatonos]")
+                remap(/\>\'e/,   "[greekepsilonpsilitonos]")
+                remap(/\<\`e/,   "[greekepsilondasiavaria]")
+                remap(/\>\`e/,   "[greekepsilonpsilivaria]")
+                remap(/\<e/,     "[greekepsilondasia]")
+                remap(/\>e/,     "[greekepsilonpsili]")
+                remap(/\'e/,     "[greekepsilonoxia]")
+                remap(/\`e/,     "[greekepsilonvaria]")
+                remap(/\~e/,     "[greekepsilonperispomeni]")
+                remap(/\<\'a/,   "[greekalphadasiatonos]")
+                remap(/\>\'a/,   "[greekalphapsilitonos]")
+                remap(/\<\`a/,   "[greekalphadasiavaria]")
+                remap(/\>\`a/,   "[greekalphapsilivaria]")
+                remap(/\<\~a/,   "[greekalphadasiaperispomeni]")
+                remap(/\>\~a/,   "[greekalphapsiliperispomeni]")
+                remap(/\<a/,     "[greekalphadasia]")
+                remap(/\>a/,     "[greekalphapsili]")
+                remap(/\'a/,     "[greekalphaoxia]")
+                remap(/\`a/,     "[greekalphavaria]")
+                remap(/\~a/,     "[greekalphaperispomeni]")
+                remap(/\<\'h/,   "[greeketadasiatonos]")
+                remap(/\>\'h/,   "[greeketapsilitonos]")
+                remap(/\<\`h/,   "[greeketadasiavaria]")
+                remap(/\>\`h/,   "[greeketapsilivaria]")
+                remap(/\<\~h/,   "[greeketadasiaperispomeni]")
+                remap(/\>\~h/,   "[greeketapsiliperispomeni]")
+                remap(/\<h/,     "[greeketadasia]")
+                remap(/\>h/,     "[greeketapsili]")
+                remap(/\'h/,     "[greeketaoxia]")
+                remap(/\`h/,     "[greeketavaria]")
+                remap(/\~h/,     "[greeketaperispomeni]")
+                remap(/\<\~o/,   "[greekomicrondasiaperispomeni]")
+                remap(/\>\~o/,   "[greekomicronpsiliperispomeni]")
+                remap(/\<\'o/,   "[greekomicrondasiatonos]")
+                remap(/\>\'o/,   "[greekomicronpsilitonos]")
+                remap(/\<\`o/,   "[greekomicrondasiavaria]")
+                remap(/\>\`o/,   "[greekomicronpsilivaria]")
+                remap(/\<o/,     "[greekomicrondasia]")
+                remap(/\>o/,     "[greekomicronpsili]")
+                remap(/\'o/,     "[greekomicronoxia]")
+                remap(/\`o/,     "[greekomicronvaria]")
+                remap(/\~o/,     "[greekomicronperispomeni]")
+                remap(/\<\'u/,   "[greekupsilondasiatonos]")
+                remap(/\>\'u/,   "[greekupsilonpsilitonos]")
+                remap(/\<\`u/,   "[greekupsilondasiavaria]")
+                remap(/\>\`u/,   "[greekupsilonpsilivaria]")
+                remap(/\<\~u/,   "[greekupsilondasiaperispomeni]")
+                remap(/\>\~u/,   "[greekupsilonpsiliperispomeni]")
+                remap(/\"\'u/,   "[greekupsilondialytikatonos]")
+                remap(/\"\`u/,   "[greekupsilondialytikavaria]")
+                remap(/\"\~u/,   "[greekupsilondialytikaperispomeni]")
+                remap(/\<u/,     "[greekupsilondasia]")
+                remap(/\>u/,     "[greekupsilonpsili]")
+                remap(/\'u/,     "[greekupsilonoxia]")
+                remap(/\`u/,     "[greekupsilonvaria]")
+                remap(/\~u/,     "[greekupsilonperispomeni]")
+                remap(/\"u/,     "[greekupsilondiaeresis]")
+                remap(/\<\'w/,   "[greekomegadasiatonos]")
+                remap(/\>\'w/,   "[greekomegapsilitonos]")
+                remap(/\<\`w/,   "[greekomegadasiavaria]")
+                remap(/\>\`w/,   "[greekomegapsilivaria]")
+                remap(/\<\~w/,   "[greekomegadasiaperispomeni]")
+                remap(/\>\~w/,   "[greekomegapsiliperispomeni]")
+                remap(/\<w/,     "[greekomegadasia]")
+                remap(/\>w/,     "[greekomegapsili]")
+                remap(/\'w/,     "[greekomegaoxia]")
+                remap(/\`w/,     "[greekomegavaria]")
+                remap(/\~w/,     "[greekomegaperispomeni]")
+                remap(/\<r/,     "[greekrhodasia]")
+                remap(/\>r/,     "[greekrhopsili]")
+                remap(/\<\~/,    "[greekdasiaperispomeni]")
+                remap(/\>\~/,    "[greekpsiliperispomeni]")
+                remap(/\<\'/,    "[greekdasiatonos]")
+                remap(/\>\'/,    "[greekpsilitonos]")
+                remap(/\<\`/,    "[greekdasiavaria]")
+                remap(/\>\`/,    "[greekpsilivaria]")
+                remap(/\"\'/,    "[greekdialytikatonos]")
+                remap(/\"\`/,    "[greekdialytikavaria]")
+                remap(/\"\~/,    "[greekdialytikaperispomeni]")
+                remap(/\</,      "[greekdasia]")
+                remap(/\>/,      "[greekpsili]")
+                remap(/\d.{0,2}''/, "")
+                remap(/\'/,      "[greekoxia]")
+                remap(/\`/,      "[greekvaria]")
+                remap(/\~/,      "[perispomeni]")
+                remap(/\"/,      "[greekdialytika]")
+                # unknown
+                # remap(/\|/,      "[greekIotadialytika]")
+                # next
+                remap(/A/, "[greekAlpha]")
+                remap(/B/, "[greekBeta]")
+                remap(/D/, "[greekDelta]")
+                remap(/E/, "[greekEpsilon]")
+                remap(/F/, "[greekPhi]")
+                remap(/G/, "[greekGamma]")
+                remap(/H/, "[greekEta]")
+                remap(/I/, "[greekIota]")
+                remap(/J/, "[greekTheta]")
+                remap(/K/, "[greekKappa]")
+                remap(/L/, "[greekLambda]")
+                remap(/M/, "[greekMu]")
+                remap(/N/, "[greekNu]")
+                remap(/O/, "[greekOmicron]")
+                remap(/P/, "[greekPi]")
+                remap(/Q/, "[greekChi]")
+                remap(/R/, "[greekRho]")
+                remap(/S/, "[greekSigma]")
+                remap(/T/, "[greekTau]")
+                remap(/U/, "[greekUpsilon]")
+                remap(/W/, "[greekOmega]")
+                remap(/X/, "[greekXi]")
+                remap(/Y/, "[greekPsi]")
+                remap(/Z/, "[greekZeta]")
+                remap(/a/, "[greekalpha]")
+                remap(/b/, "[greekbeta]")
+                remap(/c/, "[greekfinalsigma]")
+                remap(/d/, "[greekdelta]")
+                remap(/e/, "[greekepsilon]")
+                remap(/f/, "[greekphi]")
+                remap(/g/, "[greekgamma]")
+                remap(/h/, "[greeketa]")
+                remap(/i/, "[greekiota]")
+                remap(/j/, "[greektheta]")
+                remap(/k/, "[greekkappa]")
+                remap(/l/, "[greeklambda]")
+                remap(/m/, "[greekmu]")
+                remap(/n/, "[greeknu]")
+                remap(/o/, "[greekomicron]")
+                remap(/p/, "[greekpi]")
+                remap(/q/, "[greekchi]")
+                remap(/r/, "[greekrho]")
+                remap(/s/, "[greeksigma]")
+                remap(/t/, "[greektau]")
+                remap(/u/, "[greekupsilon]")
+                remap(/w/, "[greekomega]")
+                remap(/x/, "[greekxi]")
+                remap(/y/, "[greekpsi]")
+                remap(/z/, "[greekzeta]")
+                clone("[greekalphatonos]", "[greekalphaoxia]")
+                clone("[greekepsilontonos]", "[greekepsilonoxia]")
+                clone("[greeketatonos]", "[greeketaoxia]")
+                clone("[greekiotatonos]", "[greekiotaoxia]")
+                clone("[greekomicrontonos]", "[greekomicronoxia]")
+                clone("[greekupsilontonos]", "[greekupsilonoxia]")
+                clone("[greekomegatonos]", "[greekomegaoxia]")
+            when 'ru' then
+                remap(/\xC1/, "[cyrillica]")
+                remap(/\xC2/, "[cyrillicb]")
+                remap(/\xD7/, "[cyrillicv]")
+                remap(/\xC7/, "[cyrillicg]")
+                remap(/\xC4/, "[cyrillicd]")
+                remap(/\xC5/, "[cyrillice]")
+                remap(/\xD6/, "[cyrilliczh]")
+                remap(/\xDA/, "[cyrillicz]")
+                remap(/\xC9/, "[cyrillici]")
+                remap(/\xCA/, "[cyrillicishrt]")
+                remap(/\xCB/, "[cyrillick]")
+                remap(/\xCC/, "[cyrillicl]")
+                remap(/\xCD/, "[cyrillicm]")
+                remap(/\xCE/, "[cyrillicn]")
+                remap(/\xCF/, "[cyrillico]")
+                remap(/\xD0/, "[cyrillicp]")
+                remap(/\xD2/, "[cyrillicr]")
+                remap(/\xD3/, "[cyrillics]")
+                remap(/\xD4/, "[cyrillict]")
+                remap(/\xD5/, "[cyrillicu]")
+                remap(/\xC6/, "[cyrillicf]")
+                remap(/\xC8/, "[cyrillich]")
+                remap(/\xC3/, "[cyrillicc]")
+                remap(/\xDE/, "[cyrillicch]")
+                remap(/\xDB/, "[cyrillicsh]")
+                remap(/\xDD/, "[cyrillicshch]")
+                remap(/\xDF/, "[cyrillichrdsn]")
+                remap(/\xD9/, "[cyrillicery]")
+                remap(/\xD8/, "[cyrillicsftsn]")
+                remap(/\xDC/, "[cyrillicerev]")
+                remap(/\xC0/, "[cyrillicyu]")
+                remap(/\xD1/, "[cyrillicya]")
+                remap(/\xA3/, "[cyrillicyo]")
+            when 'tr' then
+                remap(/\^\^11/, "[dotlessi]")
+            else
+        end
+
+        if ! encoding.empty? then
+            begin
+                filename = Kpse.found(filename, 'context')
+                if data = IO.readlines(filename.chomp) then
+                    report("preloading #{encoding} character mappings")
+                    accept = false
+                    data.each do |line|
+                        case line.chomp
+                            when /\\start(en|)coding\s*\[(.*?)\]/io then
+                                enc = $2
+                                if accept = (enc == encoding) then
+                                    report("accepting vector #{enc}")
+                                else
+                                    report("skipping vector #{enc}")
+                                end
+                            when /\\stop(en|)coding/io then
+                                accept = false
+                            when accept && /\\definecharacter\s*([a-zA-Z]+)\s*(\d+)\s*/o then
+                                name, number = $1, $2
+                                remap(/\^\^#{sprintf("%02x",number)}/, "[#{name}]")
+                                if number.to_i > 127 then
+                                    remap(/#{sprintf("\\%03o",number)}/, "[#{name}]")
+                                end
+                        end
+                    end
+                end
+            rescue
+            end
+        end
+    end
+
+end
+
+class Commands
+
+    include CommandBase
+
+    public
+
+    @@languagedata = Hash.new
+
+    def patternfiles
+        language = @commandline.argument('first')
+        if (language == 'all') || language.empty? then
+            languages = @@languagedata.keys.sort
+        elsif @@languagedata.key?(language) then
+            languages = [language]
+        else
+            languages = []
+        end
+        languages.each do |language|
+            encoding = @@languagedata[language][0] || ''
+            files    = @@languagedata[language][1] || []
+            Language::generate(self,language,files,encoding)
+        end
+    end
+
+    private
+
+    # todo: filter the fallback list from context
+
+    # The first entry in the array is the encoding which will be used
+    # when interpreting the raw patterns. The second entry is a list of
+    # filesets (string|aray), each first match of a set is taken.
+
+    @@languagedata['ba' ] = [ 'ec'      , ['bahyph.tex'] ]
+    @@languagedata['ca' ] = [ 'ec'      , ['cahyph.tex'] ]
+    @@languagedata['cy' ] = [ 'ec'      , ['cyhyph.tex'] ]
+    @@languagedata['cs' ] = [ 'ec'      , ['czhyphen.tex','czhyphen.ex'] ]
+    @@languagedata['de' ] = [ 'ec'      , ['dehyphn.tex'] ]
+    @@languagedata['deo'] = [ 'ec'      , ['dehypht.tex'] ]
+    @@languagedata['da' ] = [ 'ec'      , ['dkspecial.tex','dkcommon.tex'] ]
+    # elhyph.tex
+    @@languagedata['es' ] = [ 'ec'      , ['eshyph.tex'] ]
+    @@languagedata['et' ] = [ 'ec'      , ['ethyph.tex'] ]
+    @@languagedata['fi' ] = [ 'ec'      , ['fihyph.tex'] ]
+    @@languagedata['fr' ] = [ 'ec'      , ['frhyph.tex'] ]
+    # ghyphen.readme ghyph31.readme grphyph
+    @@languagedata['hr' ] = [ 'ec'      , ['hrhyph.tex'] ]
+    @@languagedata['hu' ] = [ 'ec'      , ['huhyphn.tex'] ]
+    @@languagedata['us' ] = [ 'default' , [['ushyphmax.tex'],['ushyph.tex'],['hyphen.tex']] ]
+    @@languagedata['us' ] = [ 'default' , [['ushyphmax.tex'],['ushyph.tex'],['hyphen.tex']] ]
+    # inhyph.tex
+    @@languagedata['is' ] = [ 'ec'      , ['ishyph.tex'] ]
+    @@languagedata['it' ] = [ 'ec'      , ['ithyph.tex'] ]
+    @@languagedata['la' ] = [ 'ec'      , ['lahyph.tex'] ]
+    # mnhyph
+    @@languagedata['nl' ] = [ 'ec'      , ['nehyph96.tex'] ]
+    # @@languagedata['no' ] = [ 'ec'      , ['nohyphbx.tex'],['nohyphb.tex'],['nohyph2.tex'],['nohyph1.tex'],['nohyph.tex'] ]
+    @@languagedata['no' ] = [ 'ec'      , [['asxsx.tex','nohyphbx.tex'],['nohyphb.tex'],['nohyph2.tex'],['nohyph1.tex'],['nohyph.tex']] ]
+    @@languagedata['agr'] = [ 'agr'     , [['grahyph4.tex'], ['oldgrhyph.tex']] ] # new, todo
+    @@languagedata['pl' ] = [ 'ec'      , ['plhyph.tex'] ]
+    @@languagedata['pt' ] = [ 'ec'      , ['pthyph.tex'] ]
+    @@languagedata['ro' ] = [ 'ec'      , ['rohyph.tex'] ]
+    @@languagedata['sl' ] = [ 'ec'      , [['slhyph.tex'], ['sihyph.tex']] ]
+    @@languagedata['sk' ] = [ 'ec'      , ['skhyphen.tex','skhyphen.ex'] ]
+    # sorhyph.tex / upper sorbian
+    # srhyphc.tex / cyrillic
+    @@languagedata['sv' ] = [ 'ec'      , ['svhyph.tex'] ]
+    @@languagedata['tr' ] = [ 'ec'      , ['tkhyph.tex'] ]
+    @@languagedata['gb' ] = [ 'default' , [['ukhyphen.tex'],['ukhyph.tex']] ]
+  # @@languagedata['ru' ] = [ 't2a'     , ['ruhyphal.tex'] ] # t2a does not work
+    @@languagedata['ru' ] = [ 'cyr'     , ['ruhyphal.tex'] ]
+end
+
+class Commands
+
+    include CommandBase
+
+    def dpxmapfiles
+
+        force = @commandline.option("force")
+
+        texmfroot = @commandline.argument('first')
+        texmfroot = '.' if texmfroot.empty?
+        if @commandline.option('maproot') != "" then
+            maproot = @commandline.option('maproot')
+        else
+            maproot = "#{texmfroot.gsub(/\\/,'/')}/fonts/map/pdftex/context"
+        end
+        if File.directory?(maproot) then
+            files = Dir.glob("#{maproot}/*.map")
+            if files.size > 0 then
+                files.each do |pdffile|
+                    next if File.basename(pdffile) == 'pdftex.map'
+                    pdffile = File.expand_path(pdffile)
+                    dpxfile = File.expand_path(pdffile.sub(/(dvips|pdftex)/i,'dvipdfm'))
+                    unless pdffile == dpxfile then
+                        begin
+                            if data = File.read(pdffile) then
+                                report("< #{File.basename(pdffile)} - pdf(e)tex")
+                                n = 0
+                                data = data.collect do |line|
+                                    if line =~ /^[\%\#]+/mo then
+                                        ''
+                                    else
+                                        encoding = if line =~ /([a-z0-9\-]+)\.enc/io        then $1 else ''  end
+                                        fontfile = if line =~ /([a-z0-9\-]+)\.(pfb|ttf)/io  then $1 else nil end
+                                        metrics  = if line =~ /^([a-z0-9\-]+)[\s\<]+/io     then $1 else nil end
+                                        slant    = if line =~ /\"([\d\.]+)\s+SlantFont\"/io then "-s #{$1}" else '' end
+                                        if metrics && encoding && fontfile then
+                                            n += 1
+                                            "#{metrics} #{encoding} #{fontfile} #{slant}"
+                                        else
+                                            ''
+                                        end
+                                    end
+                                end
+                                data.delete_if do |line|
+                                    line.gsub(/\s+/,'').empty?
+                                end
+                                data.collect! do |line|
+                                    # remove line with "name name" lines
+                                    line.gsub(/^(\S+)\s+\1\s*$/) do
+                                        $1
+                                    end
+                                end
+                                begin
+                                    if force then
+                                        if n > 0 then
+                                            File.makedirs(File.dirname(dpxfile))
+                                            if f = File.open(dpxfile,'w') then
+                                                report("> #{File.basename(dpxfile)} - dvipdfm(x) - #{n}")
+                                                f.puts(data)
+                                                f.close
+                                            else
+                                                report("? #{File.basename(dpxfile)} - dvipdfm(x)")
+                                            end
+                                        else
+                                            report("- #{File.basename(dpxfile)} - dvipdfm(x) - no entries")
+                                            # begin File.delete(dpxname) ; rescue ; end
+                                            if f = File.open(dpxfile,'w') then
+                                                f.puts("% no map entries")
+                                                f.close
+                                            end
+                                        end
+                                    else
+                                        report(". #{File.basename(dpxfile)} - dvipdfm(x) - #{n}")
+                                    end
+                                rescue
+                                    report("error in saving dvipdfm file")
+                                end
+                            else
+                                report("error in loading pdftex file")
+                            end
+                        rescue
+                            report("error in processing pdftex file")
+                        end
+                    end
+                end
+                if force then
+                    begin
+                        report("regenerating database for #{texmfroot}")
+                        system("mktexlsr #{texmfroot}")
+                    rescue
+                    end
+                end
+            else
+                report("no mapfiles found in #{maproot}")
+            end
+        else
+            report("provide proper texmfroot")
+        end
+
+    end
+
+end
+
+class Array
+
+    def add_shebang(filename,program)
+        unless self[0] =~ /^\#/ then
+            self.insert(0,"\#!/usr/bin/env #{program}")
+        end
+        unless self[2] =~ /^\#.*?copyright\=/ then
+            self.insert(1,"\#")
+            self.insert(2,"\# copyright=pragma-ade readme=readme.pdf licence=cc-gpl")
+            self.insert(3,"") unless self[3].chomp.strip.empty?
+            self[2].gsub!(/ +/, ' ')
+            return true
+        else
+            return false
+        end
+    end
+
+    def add_directive(filename,program)
+        unless self[0] =~ /^\%/ then
+            self.insert(0,"\% content=#{program}")
+        end
+        unless self[2] =~ /^\%.*?copyright\=/ then
+            self.insert(1,"\%")
+            if File.expand_path(filename) =~ /[\\\/](doc|manuals)[\\\/]/ then
+                self.insert(2,"\% copyright=pragma-ade readme=readme.pdf licence=cc-by-nc-sa")
+            else
+                self.insert(2,"\% copyright=pragma-ade readme=readme.pdf licence=cc-gpl")
+            end
+            self.insert(3,"") unless self[3].chomp.strip.empty?
+            self[0].gsub!(/ +/, ' ')
+            return true
+        else
+            return false
+        end
+    end
+
+    def add_comment(filename)
+        if self[0] =~ /<\?xml.*?\?>/ && self[2] !~ /^<\!\-\-.*?copyright\=.*?\-\->/ then
+            self.insert(1,"")
+            if File.expand_path(filename) =~ /[\\\/](doc|manuals)[\\\/]/ then
+                self.insert(2,"<!-- copyright='pragma-ade' readme='readme.pdf' licence='cc-by-nc-sa' -->")
+            else
+                self.insert(2,"<!-- copyright='pragma-ade' readme='readme.pdf' licence='cc-gpl' -->")
+            end
+            self.insert(3,"") unless self[3].chomp.strip.empty?
+            return true
+        else
+            return false
+        end
+    end
+
+end
+
+class Commands
+
+    include CommandBase
+
+    def brandfiles
+
+        force = @commandline.option("force")
+        files = @commandline.arguments # Dir.glob("**/*.*")
+        done  = false
+
+        files.each do |filename|
+            if FileTest.file?(filename) then
+                ok = false
+                begin
+                    data = IO.readlines(filename)
+                    case filename
+                        when /\.rb$/ then
+                            ok = data.add_shebang(filename,'ruby')
+                        when /\.pl$/ then
+                            ok = data.add_shebang(filename,'perl')
+                        when /\.py$/ then
+                            ok = data.add_shebang(filename,'python')
+                        when /\.lua$/ then
+                            ok = data.add_shebang(filename,'lua')
+                        when /\.tex$/ then
+                            ok = data.add_directive(filename,'tex')
+                        when /\.mp$/ then
+                            ok = data.add_directive(filename,'metapost')
+                        when /\.mf$/ then
+                            ok = data.add_directive(filename,'metafont')
+                        when /\.(xml|xsl|fo|fx|rlx|rng|exa)$/ then
+                            ok = data.add_comment(filename)
+                    end
+                rescue
+                    report("fatal error in processing #{filename}") # maybe this catches the mac problem taco reported
+                else
+                    if ok then
+                        report()
+                        report(filename)
+                        report()
+                        for i in 0..4 do
+                           report('  ' + data[i].chomp)
+                        end
+                        if force && f = File.open(filename,'w') then
+                            f.puts data
+                            f.close
+                        end
+                        done = true
+                    end
+                end
+            else
+                report("no file named #{filename}")
+            end
+        end
+        report() if done
+    end
+
+end
+
+class Commands
+
+    include CommandBase
+
+    # usage   : ctxtools --listentities entities.xml
+    # document: <!DOCTYPE something SYSTEM "entities.xml">
+
+    def flushentities(handle,entities,doctype=nil) # 'stylesheet'
+        if doctype then
+            tab = "\t"
+            handle << "<?xml version='1.0' encoding='utf-8'?>\n\n"
+            handle << "<!-- !DOCTYPE entities SYSTEM 'entities.xml' -->\n\n"
+            handle << "<!DOCTYPE #{doctype} [\n"
+        else
+            tab = ""
+        end
+        entities.keys.sort.each do |k|
+            handle << "#{tab}<!ENTITY #{k} \"\&\#x#{entities[k]};\">\n"
+        end
+        if doctype then
+            handle << "]>\n"
+        end
+    end
+
+    def listentities
+
+        filenames  = ['enco-uc.tex','contextnames.txt']
+        outputname = @commandline.argument('first')
+        doctype    = @commandline.option('doctype')
+        entities   = Hash.new
+
+        filenames.each do |filename|
+            filename = Kpse.found(filename, 'context')
+            if filename and not filename.empty? and FileTest.file?(filename) then
+                report("loading #{filename.gsub(/\\/,'/')}") unless outputname.empty?
+                IO.readlines(filename).each do |line|
+                    case line
+                        when /^[\#\%]/io then
+                            # skip comment line
+                        when /\\definecharacter\s+([a-z]+)\s+\{\\uchar\{*(\d+)\}*\{(\d+)\}\}/io then
+                            name, code = $1, sprintf("%04X",$2.to_i*256 + $3.to_i)
+                            entities[name] = code.rjust(4,'0') unless entities.key?(name)
+                        when /^([A-F0-9]+)\;([a-z][a-z]+)\;(.*?)\;(.*?)\s*$/io then
+                            code, name, adobe, comment = $1, $2, $3, $4
+                            entities[name] = code.rjust(4,'0') unless entities.key?(name)
+                    end
+                end
+            end
+        end
+        if outputname and not outputname.empty? then
+            if f = File.open(outputname,'w') then
+                report("saving #{entities.size} entities in #{outputname}")
+                flushentities(f,entities,doctype)
+                f.close
+            else
+                flushentities($stdout,entities,doctype)
+            end
+        else
+            flushentities($stdout,entities,doctype)
+        end
+    end
+
+end
+
+class Commands
+
+    include CommandBase
+
+    def platformize
+
+        pattern = if @commandline.arguments.empty? then "*.{rb,pl,py}" else @commandline.arguments end
+        recurse = @commandline.option("recurse")
+        force = @commandline.option("force")
+        pattern = "#{if recurse then '**/' else '' end}#{pattern}"
+        Dir.glob(pattern).each do |file|
+            if File.file?(file) then
+                size = File.size(file)
+                data = IO.readlines(file)
+                if force then
+                    if f = File.open(file,'w')
+                        data.each do |line|
+                            f.puts(line.chomp)
+                        end
+                        f.close
+                    end
+                    if File.size(file) == size then # not robust
+                        report("file '#{file}' is unchanged")
+                    else
+                        report("file '#{file}' is platformized")
+                    end
+                else
+                    report("file '#{file}' is a candidate")
+                end
+            end
+        end
+    end
+
+end
+
+class TexDeps
+
+    @@cs_tex = %q/
+        above abovedisplayshortskip abovedisplayskip
+        abovewithdelims accent adjdemerits advance afterassignment
+        aftergroup atop atopwithdelims
+        badness baselineskip batchmode begingroup
+        belowdisplayshortskip belowdisplayskip binoppenalty botmark
+        box boxmaxdepth brokenpenalty
+        catcode char chardef cleaders closein closeout clubpenalty
+        copy count countdef cr crcr csname
+        day deadcycles def defaulthyphenchar defaultskewchar
+        delcode delimiter delimiterfactor delimeters
+        delimitershortfall delimeters dimen dimendef discretionary
+        displayindent displaylimits displaystyle
+        displaywidowpenalty displaywidth divide
+        doublehyphendemerits dp dump
+        edef else emergencystretch end endcsname endgroup endinput
+        endlinechar eqno errhelp errmessage errorcontextlines
+        errorstopmode escapechar everycr everydisplay everyhbox
+        everyjob everymath everypar everyvbox exhyphenpenalty
+        expandafter
+        fam fi finalhyphendemerits firstmark floatingpenalty font
+        fontdimen fontname futurelet
+        gdef global group globaldefs
+        halign hangafter hangindent hbadness hbox hfil horizontal
+        hfill horizontal hfilneg hfuzz hoffset holdinginserts hrule
+        hsize hskip hss horizontal ht hyphenation hyphenchar
+        hyphenpenalty hyphen
+        if ifcase ifcat ifdim ifeof iffalse ifhbox ifhmode ifinner
+        ifmmode ifnum ifodd iftrue ifvbox ifvmode ifvoid ifx
+        ignorespaces immediate indent input inputlineno input
+        insert insertpenalties interlinepenalty
+        jobname
+        kern
+        language lastbox lastkern lastpenalty lastskip lccode
+        leaders left lefthyphenmin leftskip leqno let limits
+        linepenalty line lineskip lineskiplimit long looseness
+        lower lowercase
+        mag mark mathaccent mathbin mathchar mathchardef mathchoice
+        mathclose mathcode mathinner mathop mathopen mathord
+        mathpunct mathrel mathsurround maxdeadcycles maxdepth
+        meaning medmuskip message mkern month moveleft moveright
+        mskip multiply muskip muskipdef
+        newlinechar noalign noboundary noexpand noindent nolimits
+        nonscript scriptscript nonstopmode nulldelimiterspace
+        nullfont number
+        omit openin openout or outer output outputpenalty over
+        overfullrule overline overwithdelims
+        pagedepth pagefilllstretch pagefillstretch pagefilstretch
+        pagegoal pageshrink pagestretch pagetotal par parfillskip
+        parindent parshape parskip patterns pausing penalty
+        postdisplaypenalty predisplaypenalty predisplaysize
+        pretolerance prevdepth prevgraf
+        radical raise read relax relpenalty right righthyphenmin
+        rightskip romannumeral
+        scriptfont scriptscriptfont scriptscriptstyle scriptspace
+        scriptstyle scrollmode setbox setlanguage sfcode shipout
+        show showbox showboxbreadth showboxdepth showlists showthe
+        skewchar skip skipdef spacefactor spaceskip span special
+        splitbotmark splitfirstmark splitmaxdepth splittopskip
+        string
+        tabskip textfont textstyle the thickmuskip thinmuskip time
+        toks toksdef tolerance topmark topskip tracingcommands
+        tracinglostchars tracingmacros tracingonline tracingoutput
+        tracingpages tracingparagraphs tracingrestores tracingstats
+        uccode uchyph underline unhbox unhcopy unkern unpenalty
+        unskip unvbox unvcopy uppercase
+        vadjust valign vbadness vbox vcenter vfil vfill vfilneg
+        vfuzz voffset vrule vsize vskip vsplit vss vtop
+        wd widowpenalty write
+        xdef xleaders xspaceskip
+        year
+    /.split
+
+    @@cs_etex = %q/
+        beginL beginR botmarks
+        clubpenalties currentgrouplevel currentgrouptype
+        currentifbranch currentiflevel currentiftype
+        detokenize dimexpr displaywidowpenalties
+        endL endR eTeXrevision eTeXversion everyeof
+        firstmarks fontchardp fontcharht fontcharic fontcharwd
+        glueexpr glueshrink glueshrinkorder gluestretch
+        gluestretchorder gluetomu
+        ifcsname ifdefined iffontchar interactionmode
+        interactionmode interlinepenalties
+        lastlinefit lastnodetype
+        marks topmarks middle muexpr mutoglue
+        numexpr
+        pagediscards parshapedimen parshapeindent parshapelength
+        predisplaydirection
+        savinghyphcodes savingvdiscards scantokens showgroups
+        showifs showtokens splitdiscards splitfirstmarks
+        TeXXeTstate tracingassigns tracinggroups tracingifs
+        tracingnesting tracingscantokens
+        unexpanded unless
+        widowpenalties
+    /.split
+
+    @@cs_pdftex = %q/
+        pdfadjustspacing pdfannot pdfavoidoverfull
+        pdfcatalog pdfcompresslevel
+        pdfdecimaldigits pdfdest pdfdestmargin
+        pdfendlink pdfendthread
+        pdffontattr pdffontexpand pdffontname pdffontobjnum pdffontsize
+        pdfhorigin
+        pdfimageresolution pdfincludechars pdfinfo
+        pdflastannot pdflastdemerits pdflastobj
+        pdflastvbreakpenalty pdflastxform pdflastximage
+        pdflastximagepages pdflastxpos pdflastypos
+        pdflinesnapx pdflinesnapy pdflinkmargin pdfliteral
+        pdfmapfile pdfmaxpenalty pdfminpenalty pdfmovechars
+        pdfnames
+        pdfobj pdfoptionpdfminorversion pdfoutline pdfoutput
+        pdfpageattr pdfpageheight pdfpageresources pdfpagesattr
+        pdfpagewidth pdfpkresolution pdfprotrudechars
+        pdfrefobj pdfrefxform pdfrefximage
+        pdfsavepos pdfsnaprefpoint pdfsnapx pdfsnapy pdfstartlink
+        pdfstartthread
+        pdftexrevision pdftexversion pdfthread pdfthreadmargin
+        pdfuniqueresname
+        pdfvorigin
+        pdfxform pdfximage
+    /.split
+
+    @@cs_omega = %q/
+        odelimiter omathaccent omathchar oradical omathchardef omathcode odelcode
+        leftghost rightghost
+        charwd charht chardp charit
+        localleftbox localrightbox
+        localinterlinepenalty localbrokenpenalty
+        pagedir bodydir pardir textdir mathdir
+        boxdir nextfakemath
+        pagewidth pageheight pagerightoffset pagebottomoffset
+        nullocp nullocplist ocp externalocp ocplist pushocplist popocplist clearocplists ocptracelevel
+        addbeforeocplist addafterocplist removebeforeocplist removeafterocplist
+        OmegaVersion
+        InputTranslation OutputTranslation DefaultInputTranslation DefaultOutputTranslation
+        noInputTranslation noOutputTranslation
+        InputMode OutputMode DefaultInputMode DefaultOutputMode
+        noInputMode noOutputMode noDefaultInputMode noDefaultOutputMode
+    /.split
+
+    @@cs_metatex = %q/
+    /.split
+
+    @@cs_xetex = %q/
+    /.split
+
+    @@cs_skip = %q/
+        v\! c\! s\! e\! m\! f\!
+        \!tf \!tt \!tq \!ta \?\?
+        csname endcsname relax
+        \!\!string[a-f] \!\!dimen[a-k] \!\!count[a-f] \!\!toks[a-e] \!\!box[a-e]
+        \!\!width[a-c] \!\!height[a-c] \!\!depth[a-c]
+        \!\!done[a-f] if\!\!done[a-f] if\:\!\!done[a-f]
+        scratch globalscratch
+        ascii[a-d] globalascii
+        @@expanded @@globalexpanded @EA @EAEA @EAEAEA
+        bgroup egroup par next nextnext docommand dodocommand dododocommand
+        \!\!width \!\!height \!\!depth \!\!plus \!\!minus \!\!to
+    /.split
+
+    @@cs_skip = %q/
+        [vcsemf]\! \?\?
+        \!t[ftqa]
+        csname endcsname relax
+        \!\!string[a-f] \!\!dimen[a-k] \!\!count[a-f] \!\!toks[a-e] \!\!box[a-e]
+        \!\!width[a-c] \!\!height[a-c] \!\!depth[a-c]
+        \!\!done[a-f] if\!\!done[a-f] if\:\!\!done[a-f]
+        scratch globalscratch
+        ascii[a-d] globalascii
+        @@expanded @@globalexpanded @(EA)+
+        [be]group par next nextnext (do)+command
+        \!\!(width|height|depth|plus|minus|to)
+    /.split
+
+    # let's ignore \dimendef etc
+
+    @@primitives_def = %q/
+         def edef xdef gdef let
+         newcount newdimen newskip newbox newtoks newmarks newif newinsert newmuskip
+         chardef mathchardef dimendef countdef toksdef
+         newconditional definecomplexorsimple definecomplexorsimpleempty
+         newcounter newpersistentmark
+         installinsertion installspecial\s*\\[* installoutput\s*\\[*
+    /.split
+
+    @@types = [['invalid','*'],['okay','='],['forward','>'],['backward','<'],['unknown','?']]
+
+    @@skips = /^(#{@@cs_skip.join('|')})/o
+
+    def initialize(logger=nil,compact=false)
+        @defined      = Hash.new
+        @definitive   = Hash.new
+        @used_before  = Hash.new
+        @used_after   = Hash.new
+        @dependencies = Hash.new
+        @fineorder    = Hash.new
+        @forward      = Hash.new
+        @backward     = Hash.new
+        @disorder     = Hash.new
+        @disordercs   = Hash.new
+        @type         = Hash.new
+        @filename     = 'context.tex'
+        @files        = Array.new # keep load order !
+        @order        = Hash.new
+        @logger       = logger
+        @filefilter   = nil
+        @namefilter   = nil
+        @compact      = compact
+        #
+        @@cs_tex.each     do |cs| @defined[cs] = ['-tex--------'] end
+        @@cs_etex.each    do |cs| @defined[cs] = ['-etex-------'] end
+        @@cs_pdftex.each  do |cs| @defined[cs] = ['-pdftex-----'] end
+        @@cs_omega.each   do |cs| @defined[cs] = ['-omega------'] end
+        @@cs_xetex.each   do |cs| @defined[cs] = ['-xetex------'] end
+        @@cs_metatex.each do |cs| @defined[cs] = ['-metatex----'] end
+    end
+
+    def report(str)
+        @logger.report(str) rescue false
+    end
+
+    def setfilter(data)
+        data.split(/\s*\,\s*/).each do |d|
+            if d =~ /\.tex$/ then
+                @filefilter = Array.new unless @filefilter
+                @filefilter << d
+            else
+                @namefilter = Array.new unless @namefilter
+                @namefilter << d
+            end
+        end
+    end
+
+    def load(filename='context.tex')
+        begin
+            @filename = filename
+            n = 0
+            File.open(filename) do |f|
+                f.each do |line|
+                    if line =~ /^(\\input\s+|\\load[a-z]+\{)([a-z\-\.]+)(\}*)/ then
+                        ante, name, post = $1, $2, $3
+                        @files.push(name)
+                        @order[name] = n += 1
+                    end
+                end
+            end
+        rescue
+            @files = Array.new
+            @order = Hash.new
+        end
+    end
+
+    def save(filename='context.tex')
+        unless @filefilter || @namefilter then
+            begin
+                data = IO.readlines(filename).each do |line|
+                    line.gsub!(/^(\\input\s+|\\load[a-z]+\{)([a-z\-\.]+)(\}*)\s*$/) do
+                        ante, name, post = $1, $2, $3
+                        fin = (@fineorder[name]    || [])-[name]
+                        dep = (@dependencies[name] || [])-[name]
+                        dis = (@disorder[name]     || [])-[name]
+                        fin = if fin.size > 0 then " B[#{fin.join(' ')}]" else "" end
+                        dep = if dep.size > 0 then " A[#{dep.join(' ')}]" else "" end
+                        dis = if dis.size > 0 then " D[#{dis.join(' ')}]" else "" end
+                        "#{ante}#{name}#{post} %#{fin}#{dep}#{dis}\n"
+                    end
+                end
+            rescue
+                report("error: #{$!}")
+            else
+                begin
+                    newname = filename.sub(/\..*$/,'.log')
+                    report("")
+                    report("writing to #{newname}")
+                    report("")
+                    File.open(newname,'w') do |f|
+                        f << data
+                    end
+                rescue
+                    report("error: #{$!}")
+                end
+            end
+        end
+    end
+
+    def analyze
+        report('')
+        report("loading files")
+        report('')
+        n = 0
+# try tex and mkiv
+        @files.each do |filename|
+            if File.file?(filename) and f = File.open(filename) then
+                defs, uses, l = 0, 0, 0
+                n += 1
+                report("#{n.to_s.rjust(5,' ')} #{filename}")
+                f.each do |line|
+                    l += 1
+                    line.chomp!
+
+
+                    line.sub!(/\%.*$/, '')
+                    line.gsub!(/\\(unexpanded|unprotected|global|protected|long)\s*(\\)/, "\\")
+                    # the superseded, overloaded, forwarded, and predefined macros
+                    # are at the outer level anyway, so there we may ignore leading
+                    # spaces (could be inside an \if); other definitions are only
+                    # accepted when they start at the beginning of a line
+                    case line
+                        when /^\\ifx\s*\\[a-zA-Z\@\!\?]+\s*\\undefined\s*(\\else)*(.*?)$/ then
+                            if $2 =~ /^\s*\\(#{@@primitives_def.join('|')})\s*\\([a-zA-Z\@\?\!]{3,})/o then
+                                pushdef(filename,l,$2,5) # kind of auto-predefined
+                            end
+                        when /^\s*\\superseded\s*\\(#{@@primitives_def.join('|')})\s*\\([a-zA-Z\@\?\!]{3,})(.*)$/o
+                            name, rest = $2, $3
+                            pushdef(filename,l,name,1)
+                            moreuse(filename,l,rest)
+                        when /^\s*\\overloaded\s*\\(#{@@primitives_def.join('|')})\s*\\([a-zA-Z\@\?\!]{3,})(.*)$/o
+                            name, rest = $2, $3
+                            pushdef(filename,l,name,2)
+                            moreuse(filename,l,rest)
+                        when /^\s*\\forwarded\s*\\(#{@@primitives_def.join('|')})\s*\\([a-zA-Z\@\?\!]{3,})(.*)$/o
+                            name, rest = $2, $3
+                            pushdef(filename,l,name,3)
+                            moreuse(filename,l,rest)
+                        when /^\s*\\predefined\s*\\(#{@@primitives_def.join('|')})\s*\\([a-zA-Z\@\?\!]{3,})(.*)$/o
+                            name, rest = $2, $3
+                            pushdef(filename,l,name,4)
+                            moreuse(filename,l,rest)
+                        when /^\\(#{@@primitives_def.join('|')})[\=\s]*\\([a-zA-Z\@\?\!]{3,})(.*)$/o
+                            name, rest = $2, $3 # \=* catches the \let \a = \b
+                            pushdef(filename,l,name,0)
+                            moreuse(filename,l,rest)
+                        when /\\newevery\s*\\([a-zA-Z\@\?\!]+)\s*\\([a-zA-Z\@\?\!]+)/ then
+                            a, b = $1, $2
+                            pushdef(filename,l,a,0)
+                            pushdef(filename,l,b,0)
+                        else
+                            moreuse(filename,l,line)
+                    end
+                end
+                f.close
+            end
+        end
+        @used_after.each do |cs,files|
+            (@defined[cs] || []).each do |name|
+                @dependencies[name] = Array.new unless @dependencies[name]
+                files.each do |file|
+                    @dependencies[name] << file unless @dependencies[name].include?(file)
+                end
+            end
+        end
+        @used_before.each do |cs,files|
+            (@defined[cs] || []).each do |name|
+                @disorder[name]   = Array.new unless @disorder[name]
+                @disordercs[name] = Array.new unless @disordercs[name]
+                @fineorder[name]  = Array.new unless @fineorder[name]
+                files.each do |file|
+                    unless @disorder[name].include?(file) || name == file then
+                        unless @defined[cs].include?(file) then
+                            if @order[name] > @order[file] then
+                                @disorder[name]   << file
+                                @disordercs[name] << "#{file}:#{cs}"
+                            end
+                        end
+                    end
+                    @fineorder[name] << file unless @fineorder[name].include?(file) || name == file
+                end
+            end
+        end
+    end
+
+    def moreuse(filename,l,line)
+        line.scan(/\\if([a-zA-Z@\?\!]{3,})/) do |name, rest| # rest, else array
+            pushuse(filename,l,"if#{name}") unless name =~ /^(true|false)$/
+        end
+        line.scan(/\\([a-zA-Z@\?\!]{3,})/) do |name, rest| # rest, else array
+            if name =~ /(true|false)$/ then
+                pushuse(filename,l,"if#{name}") unless name =~ /^(if|set)$/
+            else
+                pushuse(filename,l,name)
+            end
+        end
+    end
+
+    def feedback
+        begin
+            # get max length
+            l = 0
+            list = @defined.keys.sort
+            list.each do |cs|
+                l = cs.length if cs.length > l
+            end
+            if ! @compact then
+                n = 0
+                report('')
+                report("defined: #{@defined.size}")
+                report('')
+                @defined.keys.sort.each do |cs|
+                    next if @namefilter && ! @namefilter.include?(cs)
+                    next if @filefilter && ! @defined[cs].include?(cs)
+                    if @defined[cs].size > 1 then
+                        dlist = @defined[cs].collect do |d|
+                            if d == @definitive[cs] then d else "[#{d}]" end
+                        end
+                    else
+                        dlist = @defined[cs]
+                    end
+                    report("#{(n += 1).to_s.rjust(5,' ')} #{cs.ljust(l,' ')} == #{dlist.join(' ')}")
+                end
+            end
+            if true then
+                n = 0
+                report('')
+                report("used before defined: #{@used_before.size}")
+                report('')
+                @used_before.keys.sort.each do |cs|
+                    next if @namefilter && ! @namefilter.include?(cs)
+                    next if @filefilter && (@used_before[cs] & @filefilter).size == 0
+                    used = @used_before[cs] - (@defined[cs] || [])
+                    defined = (@defined[cs] || []).join(' ')
+                    defined = "[ ? ]" if defined.empty?
+                    if used.size > 0 then
+                        report("#{(n += 1).to_s.rjust(5,' ')} #{cs.ljust(l,' ')} == #{defined} -> #{used.join(' ')}")
+                    else
+                        report("#{(n += 1).to_s.rjust(5,' ')} #{cs.ljust(l,' ')} == #{defined}")
+                    end
+                end
+                report('      none') if n == 0
+            end
+            if ! @compact then
+                n = 0
+                report('')
+                report("used after defined: #{@used_after.size}")
+                report('')
+                @used_after.keys.sort.each do |cs|
+                    next if @namefilter && ! @namefilter.include?(cs)
+                    next if @filefilter &&  (@used_after[cs] & @filefilter).size == 0
+                    used = @used_after[cs] - (@defined[cs] || [])
+                    defined = (@defined[cs] || []).join(' ')
+                    if used.size > 0 then
+                        report("#{(n += 1).to_s.rjust(5,' ')} #{cs.ljust(l,' ')} == #{defined} <- #{used.join(' ')}")
+                    else
+                        report("#{(n += 1).to_s.rjust(5,' ')} #{cs.ljust(l,' ')} == #{defined}")
+                    end
+                end
+                report('      none') if n == 0
+            end
+            if ! @compact then
+                unless @filefilter || @namefilter then
+                    [false,true].each do |mode|
+                        n = 0
+                        report("")
+                        report("file dependecies #{if mode then '(critical)' end}")
+                        [@dependencies].each do |dependencies|
+                            report("")
+                            dependencies.keys.sort.each do |f|
+                                if dependencies[f].size > 0 then
+                                    dependencies[f].delete(f)
+                                end
+                                if mode then
+                                    dep = dependencies[f].delete_if do |d|
+                                        f[0..3] == d[0..3] # same xxxx- prefix
+                                    end
+                                else
+                                    dep = dependencies[f]
+                                end
+                                if dep.size > 0 then
+                                    name = f.nosuffix('tex').ljust(8,' ')
+                                    list = dep.sort.collect do |k| k.nosuffix('tex') end
+                                    report("#{(n += 1).to_s.rjust(5,' ')} #{name} !! #{list.join(' ')}")
+                                end
+                            end
+                        end
+                        report('      none') if n == 0
+                    end
+                end
+            end
+            if true then
+                unless @filefilter || @namefilter then
+                    [false,true].each do |mode|
+                        [@disorder,@disordercs].each do |disorder|
+                            n = 0
+                            report("")
+                            report("file disorder #{if mode then '(critical)' end}")
+                            report("")
+                            disorder.keys.sort.each do |f|
+                                if disorder[f].size > 0 then
+                                    disorder[f].delete(f)
+                                end
+                                if mode then
+                                    dis = disorder[f].delete_if do |d|
+                                        f[0..3] == d[0..3] # same xxxx- prefix
+                                    end
+                                else
+                                    dis = disorder[f]
+                                end
+                                if dis.size > 0 then
+                                    name = f.nosuffix('tex').ljust(8,' ')
+                                    list = dis.sort.collect do |k| k.nosuffix('tex') end
+                                    report("#{(n += 1).to_s.rjust(3,' ')} #{name} ?? #{list.join(' ')}")
+                                end
+                            end
+                        end
+                        report('      none') if n == 0
+                    end
+                end
+            end
+        rescue
+            puts("fatal error: #{$!} #{$@.join("\n")}")
+        end
+    end
+
+    private
+
+    def csdefined?(cs,filename)
+        @defined[cs] && @defined[cs].include?(filename)
+    end
+    def csbefore?(cs,filename)
+        @used_before[cs] && @used_before[cs].include?(filename)
+    end
+    def csafter?(cs,filename)
+        @used_after[cs] && @used_after[cs].include?(filename)
+    end
+
+    def csignored?(cs)
+        cs.to_s =~ @@skips
+    end
+
+    def pushdef(filename,n,cs,type)
+        if csignored?(cs) then
+            # nothing
+        elsif @defined[cs] then
+            case type
+                when 5 then
+                    # if test, no definition done
+                else
+                    @definitive[cs] = filename
+                    unless @filefilter || @namefilter then
+                        report("#{cs} is redefined") unless csdefined?(cs,filename) || @compact
+                    end
+            end
+            @defined[cs] << filename unless @defined[cs].include?(filename)
+        else
+            @defined[cs] = Array.new
+            @defined[cs] << filename
+            @definitive[cs] = filename
+            @type[cs] = type
+        end
+    end
+
+    def pushuse(filename,n,cs)
+        if csignored?(cs) then
+            # nothing
+        elsif @defined[cs] then
+            @used_after[cs] = Array.new unless @used_after[cs]
+            @used_after[cs] << filename unless csafter?(cs,filename)
+        else
+            @used_before[cs] = Array.new unless @used_before[cs]
+            @used_before[cs] << filename unless csbefore?(cs,filename)
+        end
+    end
+
+end
+
+class Commands
+
+    include CommandBase
+
+    def dependencies
+
+        filename = if @commandline.arguments.empty? then 'context.tex' else @commandline.arguments.first end
+        compact  = @commandline.option('compact')
+
+        ['context',''].each do |progname|
+            unless FileTest.file?(filename) then
+                name = Kpse.found(filename, progname)
+                if FileTest.file?(name) then
+                    filename = name
+                    break
+                end
+            end
+        end
+
+        if FileTest.file?(filename) && deps = TexDeps.new(logger,compact) then
+            deps.setfilter(@commandline.option('filter'))
+            deps.load
+            deps.analyze
+            deps.feedback
+            deps.save if @commandline.option('save')
+        else
+            report("unknown file #{filename}")
+        end
+
+    end
+
+end
+
+class Commands
+
+    @@re_utf_bom = /^\357\273\277/o # just utf-8
+
+    def disarmutfbom
+
+        if @commandline.arguments.empty? then
+            report("provide filename")
+        else
+            @commandline.arguments.each do |filename|
+                report("checking '#{filename}'")
+                if FileTest.file?(filename) then
+                    begin
+                        data = IO.read(filename)
+                        if data.sub!(@@re_utf_bom,'') then
+                            if @commandline.option('force') then
+                                if f = File.open(filename,'wb') then
+                                    f << data
+                                    f.close
+                                    report("bom found and removed")
+                                else
+                                    report("bom found and removed, but saving file fails")
+                                end
+                            else
+                                report("bom found, use '--force' to remove it")
+                            end
+                        else
+                            report("no bom found")
+                        end
+                    rescue
+                        report("bom found, but removing it fails")
+                    end
+                else
+                    report("provide valid filename")
+                end
+            end
+        end
+    end
+
+end
+
+class Commands
+
+    include CommandBase
+
+    def updatecontext
+
+        def fetchfile(site, name, target=nil)
+            begin
+                proxy = @commandline.option('proxy')
+                if proxy && ! proxy.empty? then
+                    address, port = proxy.split(":")
+                    if address && port then
+                        http = Net::HTTP::Proxy(address, port).new(site)
+                    else
+                        http = Net::HTTP::Proxy(proxy, 80).new(site)
+                    end
+                else
+                    http = Net::HTTP.new(site)
+                end
+                resp, data = http.get(name.gsub(/^\/*/, '/'))
+            rescue
+                return false
+            else
+                begin
+                    if data then
+                        name = File.basename(name)
+                        File.open(target || name, 'wb') do |f|
+                            f << data
+                        end
+                    else
+                        return false
+                    end
+                rescue
+                    return false
+                else
+                    return true
+                end
+            end
+        end
+
+        def locatedlocaltree
+            tree = Kpse.used_path('TEXMFLOCAL')
+            unless tree && FileTest.directory?(tree) then
+                tree = Kpse.used_path('TEXMF')
+            end
+            return tree
+        end
+
+        def extractarchive(archive)
+            unless FileTest.file?(archive) then
+                 report("fatal error, '#{archive}' has not been downloaded")
+                 return false
+            end
+            # unless system("unzip -uo #{archive}") then
+            unless system("unzip -o #{archive}") then
+                report("fatal error, make sure that you have 'unzip' in your path")
+                return false
+            end
+            stubs = "scripts/context/stubs/unix/*"
+            if System.unix? and not system("chmod +x #{stubs}") then
+                report("change x-permissions of '#{stubs}' manually")
+            end
+            return true
+         end
+
+        def remakeformats
+            system("mktexlsr")
+            system("luatools --selfupdate")
+            system("mtxrun --selfupdate")
+            system("luatools --generate")
+            system("texmfstart texexec --make --all  --fast --pdftex")
+            system("texmfstart texexec --make --all  --fast --luatex")
+            system("texmfstart texexec --make --all  --fast --xetex")
+            return true
+        end
+
+        if localtree = locatedlocaltree then
+            report("updating #{localtree}")
+            begin
+                Dir.chdir(localtree)
+            rescue
+                report("unable to change to #{localtree}")
+            else
+                archive = 'cont-tmf.zip'
+                report("fetching #{archive}")
+                unless fetchfile("www.pragma-ade.com","/context/latest/#{archive}") then
+                    report("unable to fetch #{archive}")
+                    return
+                end
+                report("extracting #{archive}")
+                unless extractarchive(archive) then
+                    report("unable to extract #{archive}")
+                    return
+                end
+                report("remaking formats")
+                unless remakeformats then
+                    report("unable to remak formats")
+                end
+            end
+        else
+            report("unable to locate local tree")
+        end
+
+    end
+
+end
+
+logger      = Logger.new(banner.shift)
+commandline = CommandLine.new
+
+commandline.registeraction('touchcontextfile'  , 'update context version')
+commandline.registeraction('contextversion'    , 'report context version')
+commandline.registeraction('jeditinterface'    , 'generate jedit syntax files [--pipe]')
+commandline.registeraction('bbeditinterface'   , 'generate bbedit syntax files [--pipe]')
+commandline.registeraction('sciteinterface'    , 'generate scite syntax files [--pipe]')
+commandline.registeraction('rawinterface'      , 'generate raw syntax files [--pipe]')
+# commandline.registeraction('translateinterface', 'generate interface files (xml) [nl de ..]')
+commandline.registeraction('purgefiles'        , 'remove temporary files [--all --recurse] [basename]')
+commandline.registeraction('documentation'     , 'generate documentation [--type=] [filename]')
+commandline.registeraction('filterpages'       ) # no help, hidden temporary feature
+commandline.registeraction('patternfiles'      , 'generate pattern files [--all --xml --utf8] [languagecode]')
+commandline.registeraction('dpxmapfiles'       , 'convert pdftex mapfiles to dvipdfmx [--force] [texmfroot]')
+commandline.registeraction('listentities'      , 'create doctype entity definition from enco-uc.tex')
+commandline.registeraction('brandfiles'        , 'add context copyright notice [--force]')
+commandline.registeraction('platformize'       , 'replace line-endings [--recurse --force] [pattern]')
+commandline.registeraction('dependencies'      , 'analyze depedencies within context [--save --compact --filter=[macros|filenames]] [filename]')
+commandline.registeraction('updatecontext'     , 'download latest version and remake formats [--proxy]')
+commandline.registeraction('disarmutfbom'      , 'remove utf bom [--force]')
+
+commandline.registervalue('type','')
+commandline.registervalue('filter','')
+commandline.registervalue('maproot','')
+commandline.registervalue('proxy','')
+
+commandline.registerflag('recurse')
+commandline.registerflag('force')
+commandline.registerflag('compact')
+commandline.registerflag('pipe')
+commandline.registerflag('save')
+commandline.registerflag('all')
+commandline.registerflag('xml')
+commandline.registerflag('log')
+commandline.registerflag('utf8')
+commandline.registerflag('doctype')
+
+# general
+
+commandline.registeraction('help')
+commandline.registeraction('version')
+
+commandline.expand
+
+Commands.new(commandline,logger,banner).send(commandline.action || 'help')
diff --git a/scripts/context/ruby/fcd_start.rb b/scripts/context/ruby/fcd_start.rb
new file mode 100644
index 000000000..b1fa42a2a
--- /dev/null
+++ b/scripts/context/ruby/fcd_start.rb
@@ -0,0 +1,472 @@
+# Hans Hagen / PRAGMA ADE / 2005 / www.pragma-ade.com
+#
+# Fast Change Dir
+#
+# This is a kind of variant of the good old ncd
+# program. This script uses the same indirect cmd
+# trick as Erwin Waterlander's wcd program.
+#
+# === windows: fcd.cmd ===
+#
+# @echo off
+# ruby -S fcd_start.rb %1 %2 %3 %4 %5 %6 %7 %8 %9
+# if exist "%HOME%/fcd_stage.cmd" call %HOME%/fcd_stage.cmd
+#
+# === linux: fcd (fcd.sh) ===
+#
+# !/usr/bin/env sh
+# ruby -S fcd_start.rb $1 $2 $3 $4 $5 $6 $7 $8 $9
+# if test -f "$HOME/fcd_stage.sh" ; then
+#   . $HOME/fcd_stage.sh ;
+# fi;
+#
+# ===
+#
+# On linux, one should source the file: ". fcd args" in order
+# to make the chdir persistent.
+#
+# You can create a stub with:
+#
+# ruby fcd_start.rb --stub --verbose
+#
+# usage:
+#
+# fcd --make t:\
+# fcd --add f:\project
+# fcd [--find] whatever
+# fcd [--find] whatever c (c being a list entry)
+# fcd [--find] whatever . (last choice with this pattern)
+# fcd --list
+
+# todo: HOMEDRIVE\HOMEPATH
+
+require 'rbconfig'
+
+class FastCD
+
+    @@rootpath = nil
+
+    ['HOME','TEMP','TMP','TMPDIR'].each do |key|
+        if ENV[key] then
+            if FileTest.directory?(ENV[key]) then
+                @@rootpath = ENV[key]
+                break
+            end
+        end
+    end
+
+    exit unless @@rootpath
+
+    @@mswindows = Config::CONFIG['host_os'] =~ /mswin/
+    @@maxlength = 26
+
+    require 'Win32API' if @@mswindows
+
+    if @@mswindows then
+        @@stubcode = [
+            '@echo off',
+            '',
+            'if not exist "%HOME%" goto temp',
+            '',
+            ':home',
+            '',
+            'ruby -S fcd_start.rb %1 %2 %3 %4 %5 %6 %7 %8 %9',
+            '',
+            'if exist "%HOME%\fcd_stage.cmd" call %HOME%\fcd_stage.cmd',
+            'goto end',
+            '',
+            ':temp',
+            '',
+            'ruby -S fcd_start.rb %1 %2 %3 %4 %5 %6 %7 %8 %9',
+            '',
+            'if exist "%TEMP%\fcd_stage.cmd" call %TEMP%\fcd_stage.cmd',
+            'goto end',
+            '',
+            ':end'
+        ].join("\n")
+    else
+        @@stubcode = [
+            '#!/usr/bin/env sh',
+            '',
+            'ruby -S fcd_start.rb $1 $2 $3 $4 $5 $6 $7 $8 $9',
+            '',
+            'if test -f "$HOME/fcd_stage.sh" ; then',
+            '  . $HOME/fcd_stage.sh ;',
+            'fi;'
+        ].join("\n")
+    end
+
+    @@selfpath = File.dirname($0)
+    @@datafile = File.join(@@rootpath,'fcd_state.dat')
+    @@histfile = File.join(@@rootpath,'fcd_state.his')
+    @@cdirfile = File.join(@@rootpath,if @@mswindows then 'fcd_stage.cmd' else 'fcd_stage.sh' end)
+    @@stubfile = File.join(@@selfpath,if @@mswindows then 'fcd.cmd'       else 'fcd'          end)
+
+    def initialize(verbose=false)
+        @list = Array.new
+        @hist = Hash.new
+        @result = Array.new
+        @pattern = ''
+        @result = ''
+        @verbose = verbose
+        if f = File.open(@@cdirfile,'w') then
+            f << "#{if @@mswindows then 'rem' else '#' end} no dir to change to"
+            f.close
+        else
+            report("unable to create stub #{@@cdirfile}")
+        end
+    end
+
+    def filename(name)
+        File.join(@@root,name)
+    end
+
+    def report(str,verbose=@verbose)
+        puts(">> #{str}") if verbose
+    end
+
+    def flush(str,verbose=@verbose)
+        print(str) if verbose
+    end
+
+    def clear
+        if FileTest.file?(@@histfile)
+            begin
+                File.delete(@@histfile)
+            rescue
+                report("error in deleting history file '#{@histfile}'")
+            else
+                report("history file '#{@histfile}' is deleted")
+            end
+        else
+            report("no history file '#{@histfile}'")
+        end
+    end
+
+    def scan(dir='.')
+        begin
+            [dir].flatten.sort.uniq.each do |dir|
+                begin
+                    Dir.chdir(dir)
+                    report("scanning '#{dir}'")
+                    # flush(">> ")
+                    Dir.glob("**/*").each do |d|
+                        if FileTest.directory?(d) then
+                            @list << File.expand_path(d)
+                            # flush(".")
+                        end
+                    end
+                    # flush("\n")
+                    @list = @list.sort.uniq
+                    report("#{@list.size} entries found")
+                rescue
+                    report("unknown directory '#{dir}'")
+                end
+            end
+        rescue
+            report("invalid dir specification   ")
+        end
+    end
+
+    def save
+        begin
+            if f = File.open(@@datafile,'w') then
+                @list.each do |l|
+                    f.puts(l)
+                end
+                f.close
+                report("#{@list.size} status bytes saved in #{@@datafile}")
+            else
+                report("unable to save status in #{@@datafile}")
+            end
+        rescue
+            report("error in saving status in #{@@datafile}")
+        end
+    end
+
+    def remember
+        if @hist[@pattern] == @result then
+            # no need to save result
+        else
+            begin
+                if f = File.open(@@histfile,'w') then
+                    @hist[@pattern] = @result
+                    @hist.keys.each do |k|
+                        f.puts("#{k} #{@hist[k]}")
+                    end
+                    f.close
+                    report("#{@hist.size} history entries saved in #{@@histfile}")
+                else
+                    report("unable to save history in #{@@histfile}")
+                end
+            rescue
+                report("error in saving history in #{@@histfile}")
+            end
+        end
+    end
+
+    def load
+        begin
+            @list = IO.read(@@datafile).split("\n")
+            report("#{@list.length} status bytes loaded from #{@@datafile}")
+        rescue
+            report("error in loading status from #{@@datafile}")
+        end
+        begin
+            IO.readlines(@@histfile).each do |line|
+                if line =~ /^(.*?)\s+(.*)$/i then
+                    @hist[$1] = $2
+                end
+            end
+            report("#{@hist.length} history entries loaded from #{@@histfile}")
+        rescue
+            report("error in loading history from #{@@histfile}")
+        end
+    end
+
+    def show
+        begin
+            puts("directories:")
+            puts("\n")
+            if @list.length > 0 then
+                @list.each do |l|
+                    puts(l)
+                end
+            else
+                puts("no entries")
+            end
+            puts("\n")
+            puts("history:")
+            puts("\n")
+            if @hist.length > 0 then
+                @hist.keys.sort.each do |h|
+                    puts("#{h} >> #{@hist[h]}")
+                end
+            else
+                puts("no entries")
+            end
+        rescue
+        end
+    end
+
+    def find(pattern=nil)
+        begin
+            if pattern = [pattern].flatten.first then
+                if  pattern.length > 0 and @pattern = pattern then
+                    @result = @list.grep(/\/#{@pattern}$/i)
+                    if @result.length == 0 then
+                        @result = @list.grep(/\/#{@pattern}[^\/]*$/i)
+                    end
+                end
+            else
+                puts(Dir.pwd.gsub(/\\/o, '/'))
+            end
+        rescue
+            puts("some error")
+        end
+    end
+
+    def chdir(dir)
+        begin
+            if dir then
+                if f = File.open(@@cdirfile,'w') then
+                    if @@mswindows then
+                        f.puts("cd /d #{dir.gsub('/','\\')}")
+                    else
+                        f.puts("cd #{dir.gsub("\\",'/')}")
+                    end
+                    f.close
+                end
+                @result = dir
+                report("changing to #{dir}",true)
+            else
+                report("not changing dir")
+            end
+        rescue
+        end
+    end
+
+    def choose(args=[])
+        offset = 97
+        unless @pattern.empty? then
+            begin
+                case @result.size
+                    when 0 then
+                        report("dir '#{@pattern}' not found",true)
+                    when 1 then
+                        chdir(@result[0])
+                    else
+                        list = @result.dup
+                        begin
+                            if answer = args[1] then # assignment & test
+                                if answer == '.' and @hist.key?(@pattern) then
+                                    if FileTest.directory?(@hist[@pattern]) then
+                                        print("last choice ")
+                                        chdir(@hist[@pattern])
+                                        return
+                                    end
+                                else
+                                    index = answer[0] - offset
+                                    if dir = list[index] then
+                                        chdir(dir)
+                                        return
+                                    end
+                                end
+                            end
+                        rescue
+                            puts("some error")
+                        end
+                        loop do
+                            print("\n")
+                            list.each_index do |i|
+begin
+                                if i < @@maxlength then
+                                    # puts("#{(i+?a).chr}  #{list[i]}")
+                                    puts("#{(i+offset).chr}  #{list[i]}")
+                                else
+                                    puts("\n   there are #{list.length-@@maxlength} entries more")
+                                    break
+                                end
+rescue
+                            puts("some error")
+end
+                            end
+                            print("\n>> ")
+                            if answer = wait then
+                                if answer >= offset and answer <= offset+25 then
+                                    index = answer - offset
+                                    if dir = list[index] then
+                                        print("#{answer.chr} ")
+                                        chdir(dir)
+                                    elsif @hist.key?(@pattern) and FileTest.directory?(@hist[@pattern]) then
+                                        print("last choice ")
+                                        chdir(@hist[@pattern])
+                                    else
+                                        print("quit\n")
+                                    end
+                                    break
+                                elsif list.length >= @@maxlength then
+                                    @@maxlength.times do |i| list.shift end
+                                    print("next set")
+                                    print("\n")
+                                elsif @hist.key?(@pattern) and FileTest.directory?(@hist[@pattern]) then
+                                    print("last choice ")
+                                    chdir(@hist[@pattern])
+                                    break
+                                else
+                                    print("quit\n")
+                                    break
+                                end
+                            end
+                        end
+                    end
+            rescue
+                 report($!)
+            end
+        end
+    end
+
+    def wait
+        begin
+            $stdout.flush
+            return getc
+        rescue
+            return nil
+        end
+    end
+
+    def getc
+        begin
+            if @@mswindows then
+                ch = Win32API.new('crtdll','_getch',[],'L').call
+            else
+                system('stty raw -echo')
+                ch = $stdin.getc
+                system('stty -raw echo')
+            end
+        rescue
+            ch = nil
+        end
+        return ch
+    end
+
+    def check
+        unless FileTest.file?(@@stubfile) then
+            report("creating stub #{@@stubfile}")
+            begin
+                if f = File.open(@@stubfile,'w') then
+                    f.puts(@@stubcode)
+                    f.close
+                end
+            rescue
+                report("unable to create stub #{@@stubfile}")
+            else
+                unless @mswindows then
+                    begin
+                        File.chmod(0755,@@stubfile)
+                    rescue
+                        report("unable to change protections on #{@@stubfile}")
+                    end
+                end
+            end
+        else
+            report("stub #{@@stubfile} already present")
+        end
+    end
+
+end
+
+$stdout.sync = true
+
+verbose, action, args = false, :find, Array.new
+
+usage   = "fcd [--add|clear|find|list|make|show|stub] [--verbose] [pattern]"
+version = "1.0.2"
+
+def quit(message)
+    puts(message)
+    exit
+end
+
+ARGV.each do |a|
+    case a
+        when '-a', '--add'     then action = :add
+        when '-c', '--clear'   then action = :clear
+        when '-f', '--find'    then action = :find
+        when '-l', '--list'    then action = :show
+        when '-m', '--make'    then action = :make
+        when '-s', '--show'    then action = :show
+        when       '--stub'    then action = :stub
+        when '-v', '--verbose' then verbose = true
+        when       '--version' then quit("version: #{version}")
+        when '-h', '--help'    then quit("usage: #{usage}")
+        when /^\-\-.*/         then quit("error: unknown switch #{a}, try --help")
+                               else args << a
+    end
+end
+
+fcd = FastCD.new(verbose)
+fcd.report("Fast Change Dir / version #{version}")
+
+case action
+    when :make then
+        fcd.clear
+        fcd.scan(args)
+        fcd.save
+    when :clear then
+        fcd.clear
+    when :add then
+        fcd.load
+        fcd.scan(args)
+        fcd.save
+    when :show then
+        fcd.load
+        fcd.show
+    when :find then
+        fcd.load
+        fcd.find(args)
+        fcd.choose(args)
+        fcd.remember
+    when :stub
+        fcd.check
+end
diff --git a/scripts/context/ruby/graphics/gs.rb b/scripts/context/ruby/graphics/gs.rb
new file mode 100644
index 000000000..6143c8812
--- /dev/null
+++ b/scripts/context/ruby/graphics/gs.rb
@@ -0,0 +1,684 @@
+# module    : graphics/gs
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2002-2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+# ['base/variables','../variables','variables'].each do |r| begin require r ; rescue Exception ; else break ; end ; end
+# ['base/system',   '../system',   'system'   ].each do |r| begin require r ; rescue Exception ; else break ; end ; end
+
+require 'base/variables'
+require 'base/system'
+require 'fileutils'
+# Require 'ftools'
+
+class GhostScript
+
+    include Variables
+
+    @@pdftrimwhite = 'pdftrimwhite.pl'
+
+    @@pstopdfoptions = [
+        'AntiAliasColorImages',
+        'AntiAliasGrayImages',
+        'AntiAliasMonoImages',
+        'ASCII85EncodePages',
+        'AutoFilterColorImages',
+        'AutoFilterGrayImages',
+        'AutoPositionEPSFiles',
+        'AutoRotatePages',
+        'Binding',
+        'ColorConversionStrategy',
+        'ColorImageDepth',
+        'ColorImageDownsampleThreshold',
+        'ColorImageDownsampleType',
+        'ColorImageFilter',
+        'ColorImageResolution',
+        'CompatibilityLevel',
+        'CompressPages',
+        #'ConvertCMYKImagesToRGB', # buggy
+        #'ConvertImagesToIndexed', # buggy
+        'CreateJobTicket',
+        'DetectBlends',
+        'DoThumbnails',
+        'DownsampleColorImages',
+        'DownsampleGrayImages',
+        'DownsampleMonoImages',
+        'EmbedAllFonts',
+        'EncodeColorImages',
+        'EncodeGrayImages',
+        'EncodeMonoImages',
+        'EndPage',
+        'FirstPage',
+        'GrayImageDepth',
+        'GrayImageDownsampleThreshold',
+        'GrayImageDownsampleType',
+        'GrayImageFilter',
+        'GrayImageResolution',
+        'MaxSubsetPct',
+        'MonoImageDepth',
+        'MonoImageDownsampleThreshold',
+        'MonoImageDownsampleType',
+        'MonoImageFilter',
+        'MonoImageResolution',
+        'Optimize',
+        'ParseDCSComments',
+        'ParseDCSCommentsForDocInfo',
+        'PreserveCopyPage',
+        'PreserveEPSInfo',
+        'PreserveHalftoneInfo',
+        'PreserveOPIComments',
+        'PreserveOverprintSettings',
+        'SubsetFonts',
+        'UseFlateCompression'
+    ]
+
+    @@methods = Hash.new
+
+    @@methods['raw']         = '1'
+    @@methods['bound']       = '2'
+    @@methods['bounded']     = '2'
+    @@methods['crop']        = '3'
+    @@methods['cropped']     = '3'
+    @@methods['down']        = '4'
+    @@methods['downsample']  = '4'
+    @@methods['downsampled'] = '4'
+    @@methods['simplify']    = '5'
+    @@methods['simplified']  = '5'
+
+    @@tempfile    = 'gstemp'
+    @@pstempfile  = @@tempfile + '.ps'
+    @@pdftempfile = @@tempfile + '.pdf'
+
+    @@bboxspec = '\s*([\-\d\.]+)' + '\s+([\-\d\.]+)'*3
+
+    def initialize(logger=nil)
+
+        unless logger then
+            puts('gs class needs a logger')
+            exit
+        end
+
+        @variables = Hash.new
+        @psoptions = Hash.new
+        @logger    = logger
+
+        setvariable('profile',    'gsprofile.ini')
+        setvariable('pipe',       true)
+        setvariable('method',     2)
+        setvariable('force',      false)
+        setvariable('colormodel', 'cmyk')
+        setvariable('inputfile',  '')
+        setvariable('outputfile', '')
+
+        @@pstopdfoptions.each do |key|
+            @psoptions[key] = ''
+        end
+
+        reset
+
+    end
+
+    def reset
+        @llx = @lly = @ulx = @uly = 0
+        @oldbbox = [@llx,@lly,@urx,@ury]
+        @width = @height = @xoffset = @yoffset = @offset = 0
+        @rs = Tool.default_line_separator
+    end
+
+    def supported?(filename)
+        psfile?(filename) || pdffile?(filename)
+    end
+
+    def psfile?(filename)
+        filename =~ /\.(eps|epsf|ps|ai\d*)$/io
+    end
+
+    def pdffile?(filename)
+        filename =~ /\.(pdf)$/io
+    end
+
+    def setpsoption(key,value)
+        @psoptions[key] = value unless value.empty?
+    end
+
+    def setdimensions (llx,lly,urx,ury)
+        @oldbbox = [llx,lly,urx,ury]
+        @llx, @lly = llx.to_f-@offset, lly.to_f-@offset
+        @urx, @ury = urx.to_f+@offset, ury.to_f+@offset
+        @width,   @height  = @urx - @llx, @ury - @lly
+        @xoffset, @yoffset =    0 - @llx,    0 - @lly
+    end
+
+    def setoffset (offset=0)
+        @offset = offset.to_f
+        setdimensions(@llx,@lly,@urx,@ury) if dimensions?
+    end
+
+    def resetdimensions
+        setdimensions(0,0,0,0)
+    end
+
+    def dimensions?
+        (@width>0) && (@height>0)
+    end
+
+    def convert
+
+        inpfile = getvariable('inputfile')
+
+        if inpfile.empty? then
+            report('no inputfile specified')
+            return false
+        end
+
+        unless FileTest.file?(inpfile) then
+            report("unknown input file #{inpfile}")
+            return false
+        end
+
+        outfile = getvariable('outputfile')
+
+        if outfile.empty? then
+            outfile = inpfile
+            outfile = outfile.sub(/^.*[\\\/]/,'')
+        end
+
+        outfile = outfile.sub(/\.(pdf|eps|ps|ai)/i, "")
+        resultfile = outfile + '.pdf'
+        setvariable('outputfile', resultfile)
+
+        # flags
+
+        saveprofile(getvariable('profile'))
+
+        begin
+            gsmethod = method(getvariable('method')).to_i
+            report("conversion method #{gsmethod}")
+        rescue
+            gsmethod = 1
+            report("fallback conversion method #{gsmethod}")
+        end
+
+        debug('piping data') if getvariable('pipe')
+
+        ok = false
+        begin
+            case gsmethod
+                when 0, 1 then ok = convertasis(inpfile,resultfile)
+                when 2    then ok = convertbounded(inpfile,resultfile)
+                when 3    then ok = convertcropped(inpfile,resultfile)
+                when 4    then ok = downsample(inpfile,resultfile,'screen')
+                when 5    then ok = downsample(inpfile,resultfile,'prepress')
+                else report("invalid conversion method #{gsmethod}")
+            end
+        rescue
+            report("job aborted due to some error: #{$!}")
+            begin
+                File.delete(resultfile) if FileTest.file?(resultfile)
+            rescue
+                report("unable to delete faulty #{resultfile}")
+            end
+            ok = false
+        ensure
+            deleteprofile(getvariable('profile'))
+            File.delete(@@pstempfile)  if FileTest.file?(@@pstempfile)
+            File.delete(@@pdftempfile) if FileTest.file?(@@pdftempfile)
+        end
+        return ok
+    end
+
+    # private
+
+    def method (str)
+        if @@methods.key?(str) then
+            @@methods[str]
+        else
+            str
+        end
+    end
+
+    def pdfmethod? (str)
+        case method(str).to_i
+            when 1, 3, 4, 5 then return true
+        end
+        return false
+    end
+
+    def pdfprefix (str)
+        case method(str).to_i
+            when 1 then return 'raw-'
+            when 4 then return 'lowres-'
+            when 5 then return 'normal-'
+        end
+        return ''
+    end
+
+    def psmethod? (str)
+        ! pdfmethod?(str)
+    end
+
+    def insertprofile (flags)
+        for key in flags.keys do
+            replacevariable("flag.#{key}", flags[key])
+        end
+    end
+
+    def deleteprofile (filename)
+        begin
+            File.delete(filename) if FileTest.file?(filename)
+        rescue
+        end
+    end
+
+    def saveprofile (filename)
+        return if filename.empty? || ! (ini = open(filename,"w"))
+        @@pstopdfoptions.each do |k|
+            str = @psoptions[k]
+            # beware, booleans are translated, but so are yes/no which is dangerous
+            if str.class == String then
+                if ! str.empty? && (str != 'empty') then
+                    str.sub!(/(.+)\-/io, '')
+                    str = "/" + str unless str =~ /^(true|false|none|[\d\.\-\+]+)$/
+                    ini.puts("-d#{k}=#{str}\n")
+                end
+            end
+        end
+        ini.close
+        debug("gs profile #{filename} saved")
+    end
+
+    def gsstream # private
+        if getvariable('pipe') then '-' else @@pstempfile end
+    end
+
+    def gscolorswitch
+        case getvariable('colormodel')
+            when 'cmyk' then '-dProcessColorModel=/DeviceCMYK -dColorConversionStrategy=/CMYK '
+            when 'rgb'  then '-dProcessColorModel=/DeviceRGB  -dColorConversionStrategy=/RGB '
+            when 'gray' then '-dProcessColorModel=/DeviceGRAY -dColorConversionStrategy=/GRAY '
+        else
+            ''
+        end
+    end
+
+    def gsdefaults
+        defaults = ''
+        begin
+            defaults << '-dAutoRotatePages=/None ' if @psoptions['AutoRotatePages'].empty?
+        rescue
+            defaults << '-dAutoRotatePages=/None '
+        end
+        return defaults
+    end
+
+    def convertasis (inpfile, outfile)
+
+        report("converting #{inpfile} as-is")
+
+        @rs = Tool.line_separator(inpfile)
+        debug("platform mac") if @rs == "\r"
+
+        arguments = ''
+        arguments << "\@gsprofile.ini "
+        arguments << "-q -sDEVICE=pdfwrite -dNOPAUSE -dNOCACHE -dBATCH "
+        arguments << "#{gsdefaults} "
+        arguments << "#{gscolorswitch} "
+        arguments << "-sOutputFile=#{outfile} #{inpfile} -c quit "
+
+        debug("ghostscript: #{arguments}")
+        unless ok = System.run('ghostscript',arguments) then
+            begin
+                report("removing file #{outfile}")
+                File.delete(outfile) if FileTest.file?(outfile)
+            rescue
+                debug("file #{outfile} may be invalid")
+            end
+        end
+        return ok
+
+    end
+
+    def convertbounded(inpfile, outfile)
+        report("converting #{inpfile} bounded")
+        do_convertbounded(inpfile, outfile)
+    end
+
+    def do_convertbounded(inpfile, outfile)
+
+        begin
+            return false if FileTest.file?(outfile) && (! File.delete(outfile))
+        rescue
+            return false
+        end
+
+        arguments = ''
+        arguments << "\@gsprofile.ini "
+        arguments << "-q -sDEVICE=pdfwrite -dNOPAUSE -dNOCACHE -dBATCH -dSAFER "
+        arguments << "#{gscolorswitch} "
+        arguments << "#{gsdefaults} "
+        arguments << "-sOutputFile=#{outfile} #{gsstream} -c quit "
+
+        debug("ghostscript: #{arguments}")
+        debug('opening input file')
+
+        @rs = Tool.line_separator(inpfile)
+        debug("platform mac") if @rs == "\r"
+
+        if FileTest.file?(outfile) and not File.writable?(outfile) then
+            report("output file cannot be written")
+            return false
+        elsif not tmp = open(inpfile, 'rb') then
+            report("input file cannot be opened")
+            return false
+        end
+
+        debug('opening pipe/file')
+
+        if getvariable('pipe') then
+
+            return false unless eps = IO.popen(System.command('ghostscript',arguments),'wb')
+            debug('piping data')
+            unless pipebounded(tmp,eps) then
+                debug('something went wrong in the pipe')
+                File.delete(outfile) if FileTest.file?(outfile)
+            end
+            debug('closing pipe')
+            eps.close_write
+
+        else
+
+            return false unless eps = File.open(@@pstempfile, 'wb')
+
+            debug('copying data')
+
+            if pipebounded(tmp,eps) then
+                eps.close
+                debug('processing temp file')
+                begin
+                    ok = System.run('ghostscript',arguments)
+                rescue
+                    ok = false
+                    # debug("fatal error: #{$!}")
+                ensure
+                end
+            else
+                eps.close
+                ok = false
+            end
+
+            unless ok then
+                begin
+                    report('no output file due to error')
+                    File.delete(outfile) if FileTest.file?(outfile)
+                rescue
+                    # debug("fatal error: #{$!}")
+                    debug('file',outfile,'may be invalid')
+                end
+            end
+
+            debug('deleting temp file')
+            begin
+                File.delete(@@pstempfile) if FileTest.file?(@@pstempfile)
+            rescue
+            end
+
+        end
+
+        tmp.close
+        return FileTest.file?(outfile)
+
+    end
+
+    # hm, strange, no execute here, todo ! ! !
+
+    def getdimensions (inpfile)
+
+        # -dEPSFitPage and -dEPSCrop behave weird (don't work)
+
+        arguments = "-sDEVICE=bbox -dSAFER -dNOPAUSE -dBATCH #{inpfile} "
+
+        debug("ghostscript: #{arguments}")
+
+        begin
+            bbox = System.run('ghostscript',arguments,true,true)
+        rescue
+            bbox = ''
+        end
+
+        resetdimensions
+
+        debug('bbox spec', bbox)
+
+        if bbox =~ /(Exact|HiRes)BoundingBox:#{@@bboxspec}/moi then
+            debug("high res bbox #{$2} #{$3} #{$4} #{$5}")
+            setdimensions($2,$3,$4,$5)
+        elsif bbox =~ /BoundingBox:#{@@bboxspec}/moi
+            debug("low res bbox #{$1} #{$2} #{$3} #{$4}")
+            setdimensions($1,$2,$3,$4)
+        end
+
+        return dimensions?
+
+    end
+
+    # def convertcropped (inpfile, outfile)
+        # report("converting #{inpfile} cropped")
+        # do_convertbounded(inpfile, @@pdftempfile)
+        # return unless FileTest.file?(@@pdftempfile)
+        # arguments = " --offset=#{@offset} #{@@pdftempfile} #{outfile}"
+        # report("calling #{@@pdftrimwhite}")
+        # unless ok = System.run(@@pdftrimwhite,arguments) then
+            # report('cropping failed')
+            # begin
+                # File.delete(outfile)
+            # rescue
+            # end
+            # begin
+                # File.move(@@pdftempfile,outfile)
+            # rescue
+                # File.copy(@@pdftempfile,outfile)
+                # File.delete(@@pdftempfile)
+            # end
+        # end
+        # return ok
+    # end
+
+    def convertcropped (inpfile, outfile)
+        report("converting #{inpfile} cropped")
+        if File.expand_path(inpfile) == File.expand_path(outfile) then
+            report("output filename must be different")
+        elsif inpfile =~ /\.pdf$/io then
+            System.run("pdftops -eps #{inpfile} #{@@pstempfile}")
+            if getdimensions(@@pstempfile) then
+                report("tight boundingbox found")
+            end
+            do_convertbounded(@@pstempfile, outfile)
+            File.delete(@@pstempfile) if FileTest.file?(@@pstempfile)
+        else
+            if getdimensions(inpfile) then
+                report("tight boundingbox found")
+            end
+            do_convertbounded(inpfile, outfile)
+        end
+        resetdimensions
+        return true
+    end
+
+
+    def pipebounded (eps, out)
+
+        epsbbox, skip, buffer = false, false, ''
+
+        while str = eps.gets(rs=@rs) do
+            if str =~ /^%!PS/oi then
+                debug("looks like a valid ps file")
+                break
+            elsif str =~ /%PDF\-\d+\.\d+/oi then
+                debug("looks like a pdf file, so let\'s quit")
+                return false
+            end
+        end
+
+        # why no BeginData check
+
+        eps.rewind
+
+if dimensions? then
+
+        debug('using found boundingbox')
+
+else
+
+        debug('locating boundingbox')
+        while str = eps.gets(rs=@rs) do
+            case str
+                when /^%%Page:/io then
+                    break
+                when /^%%(Crop|HiResBounding|ExactBounding)Box:#{@@bboxspec}/moi then
+                    debug('high res boundingbox found')
+                    setdimensions($2,$3,$4,$5)
+                    break
+                when /^%%BoundingBox:#{@@bboxspec}/moi then
+                    debug('low res boundingbox found')
+                    setdimensions($1,$2,$3,$4)
+            end
+        end
+        debug('no boundingbox found') if @width == 0
+
+end
+
+        eps.rewind
+
+        while str = eps.gets(rs=@rs) do
+            if str.sub!(/^(.*)%!PS/moi, "%!PS") then
+                debug("removing pre banner data")
+                out.puts(str)
+                break
+            end
+        end
+
+        while str = eps.gets(rs=@rs) do
+            if skip then
+                skip = false if str =~ /^%+(EndData|EndPhotoshop|BeginProlog).*$/o
+                out.puts(str) if $1 == "BeginProlog"
+            elsif str =~ /^%(BeginPhotoshop)\:\s*\d+.*$/o then
+                skip = true
+            elsif str =~ /^%%/mos then
+                if ! epsbbox && str =~ /^%%(Page:|EndProlog)/io then
+                    out.puts(str) if $1 == "EndProlog"
+                    debug('faking papersize')
+                    # out.puts("<< /PageSize [#{@width} #{@height}] >> setpagedevice\n")
+                    if ! dimensions? then
+                        out.puts("<< /PageSize [1 1] >> setpagedevice\n")
+                    else
+                        out.puts("<< /PageSize [#{@width} #{@height}] >> setpagedevice\n")
+                    end
+                    out.puts("gsave #{@xoffset} #{@yoffset} translate\n")
+                    epsbbox = true
+                elsif str =~ /^%%BeginBinary\:\s*\d+\s*$/o then
+                    debug('copying binary data')
+                    out.puts(str)
+                    while str = eps.gets(rs=@rs)
+                        if str =~ /^%%EndBinary\s*$/o then
+                            out.puts(str)
+                        else
+                            out.write(str)
+                        end
+                    end
+                elsif str =~ /^%AI9\_PrivateDataBegin/o then
+                    debug('ignore private ai crap')
+                    break
+                elsif str =~ /^%%EOF/o then
+                    debug('ignore post eof crap')
+                    break
+                # elsif str =~ /^%%PageTrailer/o then
+                    # debug('ignoring post page trailer crap')
+                    # break
+                elsif str =~ /^%%Trailer/o then
+                    debug('ignoring post trailer crap')
+                    break
+                elsif str =~ /^%%Creator.*Illustrator.*$/io then
+                    debug('getting rid of problematic creator spec')
+                    str = "% Creator: Adobe Illustrator ..."
+                    out.puts(str)
+                elsif str =~ /^%%AI.*(PaperRect|Margin)/io then
+                    debug('removing AI paper crap')
+                elsif str =~ /^%%AI.*Version.*$/io then
+                    debug('removing dangerous version info')
+                elsif str =~ /^(%+AI.*Thumbnail.*)$/o then
+                    debug('skipping AI thumbnail')
+                    skip = true
+                else
+                    out.puts(str)
+                end
+            else
+                out.puts(str)
+            end
+        end
+
+        debug('done, sending EOF')
+
+        out.puts "grestore\n%%EOF\n"
+
+        # ok = $? == 0
+        # report('process aborted, broken pipe, fatal error') unless ok
+        # return ok
+
+resetdimensions
+
+        return true
+
+    end
+
+    def downsample (inpfile, outfile, method='screen')
+
+        # gs <= 8.50
+
+        report("downsampling #{inpfile}")
+
+        doit = true
+        unless getvariable('force') then
+            begin
+                if f = File.open(inpfile) then
+                    f.binmode
+                    while doit && (data = f.gets) do
+                        if data =~ /\/ArtBox\s*\[\s*[\d\.]+\s+[\d\.]+\s+[\d\.]+\s+[\d\.]+\s*\]/io then
+                            doit = false
+                        end
+                    end
+                    f.close
+                end
+            rescue
+            end
+        end
+
+        if doit then
+            arguments = ''
+            arguments << "-dPDFSETTINGS=/#{method} -dEmbedAllFonts=true "
+            arguments << "#{gscolorswitch} "
+            arguments << "#{gsdefaults} "
+            arguments << "-q -sDEVICE=pdfwrite -dNOPAUSE -dNOCACHE -dBATCH -dSAFER "
+            arguments << "-sOutputFile=#{outfile} #{inpfile} -c quit "
+            unless ok = System.run('ghostscript',arguments) then
+                begin
+                    File.delete(outfile) if FileTest.file?(outfile)
+                    report("removing file #{outfile}")
+                rescue
+                    debug("file #{outfile} may be invalid")
+                end
+            end
+            return ok
+        else
+            report("crop problem, straight copying #{inpfile}")
+            File.copy(inpfile,outfile)
+            return false
+        end
+
+    end
+
+end
diff --git a/scripts/context/ruby/graphics/inkscape.rb b/scripts/context/ruby/graphics/inkscape.rb
new file mode 100644
index 000000000..8d3b26468
--- /dev/null
+++ b/scripts/context/ruby/graphics/inkscape.rb
@@ -0,0 +1,112 @@
+# module    : graphics/inkscape
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2002-2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+# ['base/variables','variables'].each do |r| begin require r ; rescue Exception ; else break ; end ; end
+# ['graphics/gs','gs'].each   do |r| begin require r ; rescue Exception ; else break ; end ; end
+
+require 'base/variables'
+require 'base/system'
+require 'graphics/gs'
+
+class InkScape
+
+    include Variables
+
+    def initialize(logger=nil)
+
+        unless logger then
+            puts('inkscape class needs a logger')
+            exit
+        end
+
+        @variables = Hash.new
+        @logger    = logger
+
+        reset
+
+    end
+
+    def reset
+        # nothing yet
+    end
+
+    def supported?(filename)
+        filename =~ /.*\.(svg|svgz)/io
+    end
+
+    def convert(logfile=System.null)
+
+        directpdf = false
+
+        logfile = logfile.gsub(/\/+$/,"")
+
+        inpfilename = getvariable('inputfile').dup
+        outfilename = getvariable('outputfile').dup
+        outfilename = inpfilename.dup if outfilename.empty?
+        outfilename.gsub!(/(\.[^\.]*?)$/, ".pdf")
+        tmpfilename = outfilename.gsub(/(\.[^\.]*?)$/, ".ps")
+
+        if inpfilename.empty? || outfilename.empty? then
+            report("no filenames given")
+            return false
+        end
+        if inpfilename == outfilename then
+            report("filenames must differ (#{inpfilename} #{outfilename})")
+            return false
+        end
+        unless FileTest.file?(inpfilename) then
+            report("unknown file #{inpfilename}")
+            return false
+        end
+
+        # we need to redirect the error info else we get a pop up console
+
+        if directpdf then
+            report("converting #{inpfilename} to #{outfilename}")
+            resultpipe = "--without-gui --export-pdf=\"#{outfilename}\" 2>#{logfile}"
+        else
+            report("converting #{inpfilename} to #{tmpfilename}")
+            resultpipe = "--without-gui --print=\">#{tmpfilename}\" 2>#{logfile}"
+        end
+
+        arguments = [resultpipe,inpfilename].join(' ').gsub(/\s+/,' ')
+
+        ok = true
+        begin
+            debug("inkscape: #{arguments}")
+            # should work
+            # ok = System.run('inkscape',arguments) # does not work here
+            # but 0.40 only works with this:
+            command = "inkscape #{arguments}"
+            report(command)
+            ok = system(command)
+            # and 0.41 fails with everything
+            # and 0.45 is better
+        rescue
+            report("aborted due to error")
+            return false
+        else
+            return false unless ok
+        end
+
+        if not directpdf then
+            ghostscript = GhostScript.new(@logger)
+            ghostscript.setvariable('inputfile',tmpfilename)
+            ghostscript.setvariable('outputfile',outfilename)
+            report("converting #{tmpfilename} to #{outfilename}")
+            ghostscript.convert
+            begin
+                File.delete(tmpfilename)
+            rescue
+            end
+        end
+    end
+
+end
diff --git a/scripts/context/ruby/graphics/magick.rb b/scripts/context/ruby/graphics/magick.rb
new file mode 100644
index 000000000..f59087bdf
--- /dev/null
+++ b/scripts/context/ruby/graphics/magick.rb
@@ -0,0 +1,161 @@
+# module    : graphics/inkscape
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2002-2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+# ['base/variables','variables'].each do |r| begin require r ; rescue Exception ; else break ; end ; end
+
+require 'base/variables'
+
+class ImageMagick
+
+    include Variables
+
+    def initialize(logger=nil)
+
+        unless logger then
+            puts('magick class needs a logger')
+            exit
+        end
+
+        @variables = Hash.new
+        @logger    = logger
+
+        reset
+
+    end
+
+    def reset
+        ['compression','depth','colorspace','quality'].each do |key|
+            setvariable(key)
+        end
+    end
+
+    def supported?(filename) # ? pdf
+        filename =~ /.*\.(png|gif|tif|tiff|jpg|jpeg|eps|ai\d*)/io
+    end
+
+    def convert(suffix='pdf')
+
+        inpfilename = getvariable('inputfile').dup
+        outfilename = getvariable('outputfile').dup
+        outfilename = inpfilename.dup if outfilename.empty?
+        outfilename.gsub!(/(\.[^\.]*?)$/, ".#{suffix}")
+
+        if inpfilename.empty? || outfilename.empty? then
+            report("no filenames given")
+            return false
+        end
+        if inpfilename == outfilename then
+            report("filenames must differ (#{inpfilename} #{outfilename})")
+            return false
+        end
+        unless FileTest.file?(inpfilename) then
+            report("unknown file #{inpfilename}")
+            return false
+        end
+
+        if inpfilename =~ /\.tif+$/io then
+            tmpfilename = 'temp.png'
+            arguments = "#{inpfilename} #{tmpfilename}"
+            begin
+                debug("imagemagick: #{arguments}")
+                ok = System.run('imagemagick',arguments)
+            rescue
+                report("aborted due to error")
+                return false
+            else
+                return false unless ok
+            end
+            inpfilename = tmpfilename
+        end
+
+        compression = depth = colorspace = quality = ''
+
+        if getvariable('compression') =~ /(zip|jpeg)/o then
+            compression = " -compress #{$1}"
+        end
+        if getvariable('depth') =~ /(8|16)/o then
+            depth = "-depth #{$1}"
+        end
+        if getvariable('colorspace') =~ /(gray|rgb|cmyk)/o then
+            colorspace = "-colorspace #{$1}"
+        end
+        case getvariable('quality')
+            when 'low'    then quality = '-quality 0'
+            when 'medium' then quality = '-quality 75'
+            when 'high'   then quality = '-quality 100'
+        end
+
+        report("converting #{inpfilename} to #{outfilename}")
+
+        arguments = [compression,depth,colorspace,quality,inpfilename,outfilename].join(' ').gsub(/\s+/,' ')
+
+        begin
+            debug("imagemagick: #{arguments}")
+            ok = System.run('imagemagick',arguments)
+        rescue
+            report("aborted due to error")
+            return false
+        else
+            return ok
+        end
+
+    end
+
+    def autoconvert
+
+        inpfilename = getvariable('inputfile')
+        outfilename = getvariable('outputfile')
+
+        if inpfilename.empty? || ! FileTest.file?(inpfilename) then
+            report("missing file #{inpfilename}")
+            return
+        end
+
+        outfilename = inpfilename.dup if outfilename.empty?
+        tmpfilename = 'temp.jpg'
+
+        reset
+
+        megabyte = 1024*1024
+
+        ok = false
+
+        if FileTest.size(inpfilename)>2*megabyte
+            setvariable('compression','zip')
+            ok = convert
+        else
+            setvariable('compression','jpeg')
+            if FileTest.size(inpfilename)>10*megabyte then
+                setvariable('quality',85)
+            elsif FileTest.size(inpfilename)>5*megabyte then
+                setvariable('quality',90)
+            else
+                setvariable('quality',95)
+            end
+            report("auto quality #{getvariable('quality')}")
+            setvariable('outputfile', tmpfilename)
+            ok = convert('jpg')
+            setvariable('inputfile', tmpfilename)
+            setvariable('outputfile', outfilename)
+            ok = convert
+            begin
+                File.delete(tmpfilename)
+            rescue
+                report("#{tmpfilename} cannot be deleted")
+            end
+        end
+
+        reset
+
+        return ok
+
+ end
+
+end
diff --git a/scripts/context/ruby/imgtopdf.rb b/scripts/context/ruby/imgtopdf.rb
new file mode 100644
index 000000000..98a36c17f
--- /dev/null
+++ b/scripts/context/ruby/imgtopdf.rb
@@ -0,0 +1,86 @@
+#!/usr/bin/env ruby
+
+# program   : newimgtopdf
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2002-2006
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+unless defined? ownpath
+  ownpath = $0.sub(/[\\\/]\w*?\.rb/i,'')
+  $: << ownpath
+end
+
+require 'base/switch'
+require 'base/logger'
+
+require 'graphics/magick'
+
+banner = ['ImgToPdf', 'version 1.1.2', '2002-2006', 'PRAGMA ADE/POD']
+
+class Commands
+
+    include CommandBase
+
+    # nowadays we would force a directive, but
+    # for old times sake we handle default usage
+
+    def main
+        filename = @commandline.argument('first')
+
+        if filename.empty? then
+            help
+        else
+            convert
+        end
+    end
+
+    # actions
+
+    def convert
+
+        magick = Magick.new(session)
+
+        ['compression','depth','colorspace','quality','inputpath','outputpath'].each do |v|
+            magick.setvariable(v,@commandline.option(v))
+        end
+
+        @commandline.arguments.each do |fullname|
+            magick.setvariable('inputfile',fullname)
+            magick.setvariable('outputfile',fullname.gsub(/(\..*?$)/io, '.pdf'))
+            if @commandline.option('auto') then
+                magick.autoconvert
+            else
+                magick.convert
+            end
+        end
+    end
+
+end
+
+logger      = Logger.new(banner.shift)
+commandline = CommandLine.new
+
+commandline.registerflag('auto')
+
+commandline.registervalue('compression')
+commandline.registervalue('depth')
+commandline.registervalue('colorspace')
+commandline.registervalue('quality')
+
+commandline.registervalue('inputpath')
+commandline.registervalue('outputpath')
+
+
+commandline.registeraction('help')
+commandline.registeraction('version')
+
+commandline.registeraction('convert', 'convert image into pdf')
+
+commandline.expand
+
+Commands.new(commandline,logger,banner).send(commandline.action || 'main')
diff --git a/scripts/context/ruby/mpstools.rb b/scripts/context/ruby/mpstools.rb
new file mode 100644
index 000000000..534bfb95b
--- /dev/null
+++ b/scripts/context/ruby/mpstools.rb
@@ -0,0 +1,7 @@
+# todo
+#
+# this script will replace mptopdf and makempy
+
+puts("This program is yet unfinished, for the moment it just calls 'mptopdf'.\n\n")
+
+system("texmfstart mptopdf #{ARGV.join(' ')}")
diff --git a/scripts/context/ruby/mtxtools.rb b/scripts/context/ruby/mtxtools.rb
new file mode 100644
index 000000000..41d0f7085
--- /dev/null
+++ b/scripts/context/ruby/mtxtools.rb
@@ -0,0 +1,475 @@
+#!/usr/bin/env ruby
+
+# program   : mtxtools
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2004-2005
+# author    : Hans Hagen
+#
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+# This script hosts MetaTeX related features.
+
+banner = ['MtxTools', 'version 1.0.0', '2006', 'PRAGMA ADE/POD']
+
+$: << File.expand_path(File.dirname($0)) ; $: << File.join($:.last,'lib') ; $:.uniq!
+
+require 'base/switch'
+require 'base/logger'
+require 'base/system'
+require 'base/kpse'
+
+class Reporter
+    def report(str)
+        puts(str)
+    end
+end
+
+module ConTeXt
+
+    def ConTeXt::banner(filename,companionname,compact=false)
+        "-- filename : #{File.basename(filename)}\n" +
+        "-- comment  : companion to #{File.basename(companionname)} (in ConTeXt)\n" +
+        "-- author   : Hans Hagen, PRAGMA-ADE, Hasselt NL\n" +
+        "-- copyright: PRAGMA ADE / ConTeXt Development Team\n" +
+        "-- license  : see context related readme files\n" +
+        if compact then "\n-- remark   : compact version\n" else "" end
+    end
+
+end
+
+class UnicodeTables
+
+    @@version = "1.001"
+
+    @@shape_a = /^((GREEK|LATIN|HEBREW)\s*(SMALL|CAPITAL|)\s*LETTER\s*[A-Z]+)$/
+    @@shape_b = /^((GREEK|LATIN|HEBREW)\s*(SMALL|CAPITAL|)\s*LETTER\s*[A-Z]+)\s*(.+)$/
+
+    @@shape_a = /^(.*\s*LETTER\s*[A-Z]+)$/
+    @@shape_b = /^(.*\s*LETTER\s*[A-Z]+)\s+WITH\s+(.+)$/
+
+    attr_accessor :context, :comment
+
+    def initialize(logger=Reporter.new)
+        @data    = Array.new
+        @logger  = logger
+        @error   = false
+        @context = true
+        @comment = true
+        @shapes  = Hash.new
+    end
+
+    def load_unicode_data(filename='unicodedata.txt')
+        # beware, the unicodedata table is bugged, sometimes ending
+        @logger.report("reading base data from #{filename}") if @logger
+        begin
+            IO.readlines(filename).each do |line|
+                if line =~ /^[0-9A-F]{4,4}/ then
+                    d = line.chomp.sub(/\;$/, '').split(';')
+                    if d then
+                        while d.size < 15 do d << '' end
+                        n = d[0].hex
+                        @data[n] = d
+                        if d[1] =~ @@shape_a then
+                            @shapes[$1] = d[0]
+                        end
+                    end
+                end
+            end
+        rescue
+            @error = true
+            @logger.report("error while reading base data from #{filename}") if @logger
+        end
+    end
+
+    def load_context_data(filename='contextnames.txt')
+        @logger.report("reading data from #{filename}") if @logger
+        begin
+            IO.readlines(filename).each do |line|
+                if line =~ /^[0-9A-F]{4,4}/ then
+                    d = line.chomp.split(';')
+                    if d then
+                        n = d[0].hex
+                        if @data[n] then
+                            @data[d[0].hex] << d[1] # adobename   == 15
+                            @data[d[0].hex] << d[2] # contextname == 16
+                        else
+                            @logger.report("missing information about #{d} in #{filename}") if @logger
+                        end
+                    end
+                end
+            end
+        rescue
+            @error = true
+            @logger.report("error while reading context data from #{filename}") if @logger
+        end
+    end
+
+    def save_metatex_data(filename='char-def.lua',compact=false)
+        if not @error then
+            begin
+                File.open(filename,'w') do |f|
+                    @logger.report("saving data in #{filename}") if @logger
+                    f << ConTeXt::banner(filename,'char-def.tex',compact)
+                    f << "\n"
+                    f << "\nif not versions then versions = { } end versions['#{filename.gsub(/\..*?$/,'')}'] = #{@@version}\n"
+                    f << "\n"
+                    f << "if not characters      then characters      = { } end\n"
+                    f << "if not characters.data then characters.data = { } end\n"
+                    f << "\n"
+                    f << "characters.data = {\n" if compact
+                    @data.each do |d|
+                        if d then
+                            r = metatex_data(d)
+                            if compact then
+                                f << "\t" << "[0x#{d[0]}]".rjust(8,' ') << " = { #{r.join(", ").gsub(/\t/,'')} }, \n"
+                            else
+                                f << "characters.define { -- #{d[0].hex}" << "\n"
+                                f << r.join(",\n") << "\n"
+                                f << "}" << "\n"
+                            end
+                        end
+                    end
+                    f << "}\n" if compact
+                end
+            rescue
+                @logger.report("error while saving data in #{filename}") if @logger
+            else
+                @logger.report("#{@data.size} (#{sprintf('%X',@data.size)}) entries saved in #{filename}") if @logger
+            end
+        else
+            @logger.report("not saving data in #{filename} due to previous error") if @logger
+        end
+    end
+
+    def metatex_data(d)
+        r = Array.new
+        r << "\tunicodeslot=0x#{d[0]}"
+        if d[2] && ! d[2].empty? then
+            r << "\tcategory='#{d[2].downcase}'"
+        end
+        if @context then
+            if d[15] && ! d[15].empty? then
+                r << "\tadobename='#{d[15]}'"
+            end
+            if d[16] && ! d[16].empty? then
+                r << "\tcontextname='#{d[16]}'"
+            end
+        end
+        if @comment then
+            if d[1] == "<control>" then
+                r << "\tdescription='#{d[10]}'" unless d[10].empty?
+            else
+                r << "\tdescription='#{d[1]}'" unless d[1].empty?
+            end
+        end
+        if d[1] =~ @@shape_b then
+            r << "\tshcode=0x#{@shapes[$1]}" if @shapes[$1]
+        end
+        if d[12] && ! d[12].empty? then
+            r << "\tuccode=0x#{d[12]}"
+        elsif d[14] && ! d[14].empty? then
+            r << "\tuccode=0x#{d[14]}"
+        end
+        if d[13] && ! d[13].empty? then
+            r << "\tlccode=0x#{d[13]}"
+        end
+        if d[5] && ! d[5].empty? then
+            special, specials = '', Array.new
+            c = d[5].split(/\s+/).collect do |cc|
+                if cc =~ /^\<(.*)\>$/io then
+                    special = $1.downcase
+                else
+                    specials << "0x#{cc}"
+                end
+            end
+            if specials.size > 0 then
+                special = 'char' if special.empty?
+                r << "\tspecials={'#{special}',#{specials.join(',')}}"
+            end
+        end
+        return r
+    end
+
+    def save_xetex_data(filename='enco-utf.tex')
+        if not @error then
+            begin
+                minnumber, maxnumber, n = 0x001F, 0xFFFF, 0
+                File.open(filename,'w') do |f|
+                    @logger.report("saving data in #{filename}") if @logger
+                    f << "% filename : #{filename}\n"
+                    f << "% comment  : poor man's alternative for a proper enco file\n"
+                    f << "%            this file is generated by mtxtools and can be\n"
+                    f << "%            used in xetex and luatex mkii mode\n"
+                    f << "% author   : Hans Hagen, PRAGMA-ADE, Hasselt NL\n"
+                    f << "% copyright: PRAGMA ADE / ConTeXt Development Team\n"
+                    f << "% license  : see context related readme files\n"
+                    f << "\n"
+                    f << "\\ifx\\setcclcucx\\undefined\n"
+                    f << "\n"
+                    f << "  \\def\\setcclcucx #1 #2 #3 %\n"
+                    f << "    {\\global\\catcode\"#1=11 \n"
+                    f << "     \\global\\lccode \"#1=\"#2 \n"
+                    f << "     \\global\\uccode \"#1=\"#3 }\n"
+                    f << "\n"
+                    f << "\\fi\n"
+                    f << "\n"
+                    @data.each do |d|
+                        if d then
+                            number, type = d[0], d[2].downcase
+                            if number.hex >= minnumber && number.hex <= maxnumber && type =~ /^l(l|u|t)$/o then
+                                if d[13] && ! d[13].empty? then
+                                    lc = d[13]
+                                else
+                                    lc = number
+                                end
+                                if d[12] && ! d[12].empty? then
+                                    uc = d[12]
+                                elsif d[14] && ! d[14].empty? then
+                                    uc = d[14]
+                                else
+                                    uc = number
+                                end
+                                if @comment then
+                                    f << "\\setcclcuc #{number} #{lc} #{uc} % #{d[1]}\n"
+                                else
+                                    f << "\\setcclcuc #{number} #{lc} #{uc} \n"
+                                end
+                                n += 1
+                            end
+                        end
+                    end
+                    f << "\n"
+                    f << "\\endinput\n"
+                end
+            rescue
+                @logger.report("error while saving data in #{filename}") if @logger
+            else
+                @logger.report("#{n} entries saved in #{filename}") if @logger
+            end
+        else
+            @logger.report("not saving data in #{filename} due to previous error") if @logger
+        end
+    end
+
+end
+
+class RegimeTables
+
+    @@version = "1.001"
+
+    def initialize(logger=Reporter.new)
+        @logger = logger
+        reset
+    end
+
+    def reset
+        @code, @regime, @filename, @loaded = Array.new(256), '', '', false
+        (32..127).each do |i|
+            @code[i] = [sprintf('%04X',i), i.chr]
+        end
+    end
+
+    def load(filename)
+        begin
+            reset
+            if filename =~ /regi\-(ini|run|uni|utf|syn)/ then
+                report("skipping #{filename}")
+            else
+                report("loading file #{filename}")
+                @regime, unicodeset = File.basename(filename).sub(/\..*?$/,''), false
+                IO.readlines(filename).each do |line|
+                    case line
+                        when /^\#/ then
+                            # skip
+                        when /^(0x[0-9A-F]+)\s+(0x[0-9A-F]+)\s+\#\s+(.*)$/ then
+                            @code[$1.hex], unicodeset = [$2, $3], true
+                        when /^(0x[0-9A-F]+)\s+(0x[0-9A-F]+)\s+/ then
+                            @code[$1.hex], unicodeset = [$2, ''], true
+                    end
+                end
+                reset if not unicodeset
+            end
+        rescue
+            report("problem in loading file #{filename}")
+            reset
+        else
+            if ! @regime.empty? then
+                @loaded = true
+            else
+                reset
+            end
+        end
+    end
+
+    def save(filename,compact=false)
+        begin
+            if @loaded && ! @regime.empty? then
+                if File.expand_path(filename) == File.expand_path(@filename) then
+                    report("saving in #{filename} is blocked")
+                else
+                    report("saving file #{filename}")
+                    File.open(filename,'w') do |f|
+                        f << ConTeXt::banner(filename,'regi-ini.tex',compact)
+                        f << "\n"
+                        f << "\nif not versions then versions = { } end versions['#{filename.gsub(/\..*?$/,'')}'] = #{@@version}\n"
+                        f << "\n"
+                        f << "if not regimes      then regimes      = { } end\n"
+                        f << "if not regimes.data then regimes.data = { } end\n"
+                        f << "\n"
+                        if compact then
+                            f << "regimes.data[\"#{@regime}\"] = { [0] = \n\t"
+                            i = 17
+                            @code.each_index do |c|
+                                if (i-=1) == 0 then
+                                    i = 16
+                                    f << "\n\t"
+                                end
+                                if @code[c] then
+                                    f << @code[c][0].rjust(6,' ')
+                                else
+                                    f << "0x0000".rjust(6,' ')
+                                end
+                                f << ', ' if c<@code.length-1
+                            end
+                            f << "\n}\n"
+                        else
+                            @code.each_index do |c|
+                                if @code[c] then
+                                    f << someregimeslot(@regime,c,@code[c][0],@code[c][1])
+                                else
+                                    f << someregimeslot(@regime,c,'','')
+                                end
+                            end
+                        end
+                    end
+                end
+            end
+        rescue
+            report("problem in saving file #{filename} #{$!}")
+        end
+    end
+
+    def report(str)
+        @logger.report(str)
+    end
+
+    private
+
+    def someregimeslot(regime,slot,unicodeslot,comment)
+        "regimes.define { #{if comment.empty? then '' else '-- ' end} #{comment}\n" +
+            "\tregime='#{regime}',\n" +
+            "\tslot='#{sprintf('0x%02X',slot)}',\n" +
+            "\tunicodeslot='#{if unicodeslot.empty? then '0x0000' else unicodeslot end}'\n" +
+        "}\n"
+    end
+
+    public
+
+    def RegimeTables::convert(filenames,compact=false)
+        filenames.each do |filename|
+            txtfile = File.expand_path(filename)
+            luafile = File.join(File.dirname(txtfile),'regi-'+File.basename(txtfile.sub(/\..*?$/, '.lua')))
+            unless txtfile == luafile then
+                regime = RegimeTables.new
+                regime.load(txtfile)
+                regime.save(luafile,compact)
+            end
+        end
+    end
+
+end
+
+class Commands
+
+    include CommandBase
+
+    def unicodetable
+        unicode = UnicodeTables.new(logger)
+        unicode.load_unicode_data
+        unicode.load_context_data
+        unicode.save_metatex_data('char-def.lua',@commandline.option('compact'))
+    end
+
+    def xetextable
+        unicode = UnicodeTables.new(logger)
+        unicode.load_unicode_data
+        unicode.load_context_data
+        # unicode.comment = false
+        unicode.save_xetex_data
+    end
+
+    def regimetable
+        if @commandline.arguments.length > 0 then
+            RegimeTables::convert(@commandline.arguments, @commandline.option('compact'))
+        else
+            RegimeTables::convert(Dir.glob("cp*.txt")   , @commandline.option('compact'))
+            RegimeTables::convert(Dir.glob("8859*.txt") , @commandline.option('compact'))
+        end
+    end
+
+    def pdftextable
+        # instead of directly saving the data, we use luatex (kind of test)
+        pdfrdef = 'pdfr-def.tex'
+        tmpfile = 'mtxtools.tmp'
+        File.delete(pdfrdef) rescue false
+        if f = File.open(tmpfile,'w') then
+            f << "\\starttext\n"
+            f << "\\ctxlua{characters.pdftex.make_pdf_to_unicodetable('#{pdfrdef}')}\n"
+            f << "\\stoptext\n"
+            f.close()
+            system("texmfstart texexec --luatex --once --purge mtxtools.tmp")
+            report("vecor saved in #{pdfrdef}")
+        end
+        File.delete(tmpfile) rescue false
+    end
+
+    def xmlmapfile
+        # instead of directly saving the data, we use luatex (kind of test)
+        tmpfile   = 'mtxtools.tmp'
+        xmlsuffix = 'frx'
+        @commandline.arguments.each do |mapname|
+            if f = File.open(tmpfile,'w') then
+                xmlname = mapname.gsub(/\.map$/,".#{xmlsuffix}")
+                File.delete(xmlname) rescue false
+                f << "\\starttext\n"
+                f << "\\ctxlua{\n"
+                f << "  mapname = input.find_file(texmf.instance,'#{mapname}') or ''\n"
+                f << "  xmlname = '#{xmlname}'\n"
+                f << "  if mapname and not mapname:is_empty() then\n"
+                f << "    ctx.fonts.map.convert_file(mapname,xmlname)\n"
+                f << "  end\n"
+                f << "}\n"
+                f << "\\stoptext\n"
+                f.close()
+                system("texmfstart texexec --luatex --once --purge mtxtools.tmp")
+                if FileTest.file?(xmlname) then
+                    report("map file #{mapname} converted to #{xmlname}")
+                else
+                    report("no valid map file #{mapname}")
+                end
+            end
+        end
+        File.delete(tmpfile) rescue false
+    end
+
+end
+
+logger      = Logger.new(banner.shift)
+commandline = CommandLine.new
+
+commandline.registeraction('unicodetable', 'create unicode table for metatex/luatex')
+commandline.registeraction('regimetable' , 'create regime table(s) for metatex/luatex [--compact]')
+commandline.registeraction('xetextable'  , 'create unicode table for xetex')
+commandline.registeraction('pdftextable' , 'create unicode table for xetex')
+commandline.registeraction('xmlmapfile'  , 'convert traditional mapfile to xml font resourse')
+
+# general
+
+commandline.registeraction('help')
+commandline.registeraction('version')
+commandline.registerflag('compact')
+
+commandline.expand
+
+Commands.new(commandline,logger,banner).send(commandline.action || 'help')
diff --git a/scripts/context/ruby/pdftools.rb b/scripts/context/ruby/pdftools.rb
new file mode 100644
index 000000000..8ad74ec4f
--- /dev/null
+++ b/scripts/context/ruby/pdftools.rb
@@ -0,0 +1,861 @@
+#!/usr/bin/env ruby
+
+# program   : pdftools
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2003-2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+# This script will harbor some handy manipulations on tex
+# related files.
+
+banner = ['PDFTools', 'version 1.2.1', '2003/2005', 'PRAGMA ADE/POD']
+
+$: << File.expand_path(File.dirname($0)) ; $: << File.join($:.last,'lib') ; $:.uniq!
+
+require 'base/switch'
+require 'base/logger'
+
+require 'fileutils'
+# require 'ftools'
+
+class File
+
+    def File.deletefiles(*filenames)
+        filenames.flatten.each do |filename|
+            begin
+                delete(filename) if FileTest.file?(filename)
+            rescue
+            end
+        end
+    end
+
+    def File.needsupdate(oldname,newname)
+        begin
+            return File.stat(oldname).mtime != File.stat(newname).mtime
+        rescue
+            return true
+        end
+    end
+
+    def File.syncmtimes(oldname,newname)
+        begin
+            t = Time.now # i'm not sure if the time is frozen, so we do it here
+            File.utime(0,t,oldname,newname)
+        rescue
+        end
+    end
+
+    def File.replacesuffix(oldname,subpath='')
+        newname = File.expand_path(oldname.sub(/\.\w+?$/,'.pdf'))
+        File.join(File.dirname(newname),subpath,File.basename(newname))
+    end
+
+end
+
+class ImageMagick
+
+    def initialize
+
+        begin
+            version = `convert -version`
+        rescue
+            @binary = nil
+        ensure
+            if (version) && (! version.empty?) && (version =~ /ImageMagick/mo) && (version =~ /version/mio) then
+                @binary = 'convert'
+            else
+                @binary = 'imagemagick'
+            end
+        end
+
+    end
+
+    def process(arguments)
+        begin
+            @binary && system("#{@binary} #{arguments}")
+        rescue
+            false
+        end
+    end
+
+end
+
+class TexExec
+
+    def initialize
+        @binary = 'texmfstart texexec.pl --pdf --batch --silent --purge'
+    end
+
+    def process(arguments,once=true)
+       begin
+            if once then
+                @binary && system("#{@binary} --once #{arguments}")
+            else
+                @binary && system("#{@binary} #{arguments}")
+            end
+       rescue
+            false
+       end
+    end
+
+end
+
+class PdfImages
+
+    def initialize
+        @binary = "pdfimages"
+    end
+
+    def process(arguments)
+       begin
+            @binary && system("#{@binary} #{arguments}")
+       rescue
+            false
+       end
+    end
+
+end
+
+class ConvertImage
+
+    def initialize(command=nil)
+        @command = command
+    end
+
+    def convertimage(filename)
+
+        return if filename =~ /\.(pdf)$/io
+
+        retain = @command.option('retain')
+        subpath = @command.option('subpath')
+
+        if filename =~ /\s/ then
+            @command.report("skipping strange filename '#{filename}'")
+        else
+            newname = File.replacesuffix(filename,subpath)
+            # newname.gsub!(s/[^a-zA-Z0-9\_-\.]/o, '-')
+            begin
+                File.makedirs(File.dirname(newname))
+            rescue
+            end
+            if ! retain || File.needsupdate(filename,newname) then
+                imagemagick = ImageMagick.new
+                if imagemagick then
+                    ok = imagemagick.process("-compress zip -quality 99 #{filename} #{newname}")
+                    File.syncmtimes(oldname,newname) if retain
+                end
+            end
+        end
+    end
+
+end
+
+class DownsampleImage
+
+    def initialize(command=nil)
+        @command = command
+    end
+
+    def convertimage(filename)
+
+        return if filename =~ /\.(pdf)$/io
+
+        retain  = @command.option('retain')
+        subpath = @command.option('subpath')
+
+        if @command.option('lowres') then
+            method = '4'
+        elsif @command.option('medres') || @command.option('normal') then
+            method = '5'
+        else
+            method = '4'
+        end
+
+        if filename =~ /\s/ then
+            @command.report("skipping strange filename '#{filename}'")
+        else
+            newname = File.replacesuffix(filename,subpath)
+            begin
+                File.makedirs(File.dirname(newname))
+            rescue
+            end
+            if ! retain || File.needsupdate(filename,newname) then
+                ok = system("texmfstart pstopdf.rb --method=#{method} #{filename} #{newname}")
+                File.syncmtimes(oldname,newname) if retain
+            end
+        end
+    end
+
+end
+
+class ColorImage
+
+    def initialize(command=nil,tmpname='pdftools')
+        @command    = command
+        @tmpname    = tmpname
+        @colorname  = nil
+        @colorspec  = nil
+        @colorspace = nil
+    end
+
+    def registercolor(spec='.5',name='c')
+        name = name || 'c'
+        spec = spec.split(',')
+        case spec.length
+            when 4
+                @colorname, @colorspec, @colorspace = name, spec.join('/'), 'cmyk'
+            when 3
+                @colorname, @colorspec, @colorspace = name, spec.join('/'), 'rgb'
+            when 1
+                @colorname, @colorspec, @colorspace = name, spec.join('/'), 'gray'
+            else
+                @colorname, @colorspec, @colorspace = nil, nil, nil
+        end
+    end
+
+    def convertimage(filename)
+
+        invert = @command.option('invert')
+        retain = @command.option('retain')
+        subpath = @command.option('subpath')
+
+        subpath += '/' unless subpath.empty?
+
+        if @colorname && ! @colorname.empty? && @colorspec && ! @colorspec.empty? then
+            basename = filename.sub(/\.\w+?$/,'')
+            oldname = filename
+            ppmname = @tmpname + '-000.ppm'
+            jpgname = @tmpname + '-000.jpg'
+            newname = File.expand_path(oldname)
+            newname = File.dirname(newname) + '/' + subpath + @colorname + '-' + File.basename(newname)
+            newname.sub!(/\.\w+?$/, '.pdf')
+            begin
+                File.makedirs(File.dirname(newname))
+            rescue
+            end
+            if ! retain || File.needsupdate(filename,newname) then
+                pdfimages = PdfImages.new
+                imagemagick = ImageMagick.new
+                if pdfimages && imagemagick then
+                    File.deletefiles(ppmname,jpgname,newname)
+                    if filename =~ /\.(pdf)$/io then
+                        ok = pdfimages.process("-j -f 1 -l 1 #{filename} #{@tmpname}")
+                        if ok then
+                            if FileTest.file?(ppmname) then
+                                inpname = ppmname
+                            elsif FileTest.file?(jpgname) then
+                                inpname = jpgname
+                            else
+                                ok = false
+                            end
+                            if ok then
+                                switch = if ! invert then '-negate' else '' end
+                                # make sure that we keep the format
+                                tmpname = File.basename(inpname)
+                                tmpname = tmpname.sub(/(.*)\..*?$/,@tmpname) # somehow sub! fails here
+                                ok = imagemagick.process("-colorspace gray #{switch} #{inpname} #{tmpname}")
+                                if ! ok || ! FileTest.file?(tmpname) then
+                                    # problems
+                                else
+                                    ok = imagemagick.process("-colorspace #{switch} #{@colorspace} -colorize #{@colorspec} -compress zip #{tmpname} #{newname}")
+                                    if ! ok || ! FileTest.file?(newname) then
+                                        # unable to colorize image
+                                    else
+                                        # conversion done
+                                    end
+                                end
+                            end
+                        end
+                    else
+                        # make sure that we keep the format
+                        tmpname = File.basename(basename)
+                        tmpname = tmpname.sub(/(.*)\..*?$/,@tmpname) # somehow sub! fails here
+                        ok = imagemagick.process("-colorspace gray #{oldname} #{tmpname}")
+                        if ! ok || ! FileTest.file?(tmpname) then
+                            # unable to convert color to gray
+                        else
+                            ok = imagemagick.process("-colorspace #{@colorspace} -colorize #{@colorspec} -compress zip #{tmpname} #{newname}")
+                            if ! ok || ! FileTest.file?(newname) then
+                                # unable to colorize image
+                            else
+                                # conversion done
+                            end
+                        end
+                    end
+                    File.deletefiles(ppmname,jpgname,tmpname)
+                    File.syncmtimes(filename,newname) if retain
+                end
+            end
+        end
+    end
+
+end
+
+class SpotColorImage
+
+    def initialize(command=nil, tmpname='pdftools')
+        @command    = command
+        @tmpname    = tmpname
+        @colorname  = nil
+        @colorspec  = nil
+        @colorspace = nil
+        @colorfile  = nil
+    end
+
+    def registercolor(spec='.5',name='unknown')
+        name = name || 'unknown'
+        if spec =~ /^[\d\.\,]+$/ then
+            spec = spec.split(',')
+            case spec.length
+                when 4
+                    @colorname, @colorspec, @colorspace = name, ["c=#{spec[0]}","m=#{spec[1]}","y=#{spec[2]}","k=#{spec[3]}"].join(','), 'cmyk'
+                when 3
+                    @colorname, @colorspec, @colorspace = name, ["r=#{spec[0]}","g=#{spec[1]}","b=#{spec[2]}"].join(','), 'rgb'
+                when 1
+                    @colorname, @colorspec, @colorspace = name, ["s=#{spec[0]}"].join(','), 'gray'
+                else
+                    @colorname, @colorspec, @colorspace = nil, nil, nil
+            end
+        else
+            @colorname, @colorfile = name, spec
+        end
+    end
+
+    def convertgrayimage(filename)
+
+        invert  = @command.option('invert')
+        retain  = @command.option('retain')
+        subpath = @command.option('subpath')
+
+        subpath += '/' unless subpath.empty?
+
+        if @colorname && ! @colorname.empty? && ((@colorspec && ! @colorspec.empty?) || (@colorfile && ! @colorfile.empty?))  then
+            basename = filename.sub(/\.\w+?$/,'')
+            oldname  = filename # png jpg pdf
+            newname  = File.expand_path(oldname)
+            ppmname  = @tmpname + '-000.ppm'
+            jpgname  = @tmpname + '-000.jpg'
+            outname  = @tmpname + '-000.pdf'
+            texname  = @tmpname + '-temp.tex'
+            pdfname  = @tmpname + '-temp.pdf'
+            newname  = File.dirname(newname) + '/' + subpath + @colorname + '-' + File.basename(newname)
+            newname.sub!(/\.\w+?$/, '.pdf')
+            begin
+                File.makedirs(File.dirname(newname))
+            rescue
+            end
+            if ! retain || File.needsupdate(filename,newname) then
+                pdfimages = PdfImages.new
+                imagemagick = ImageMagick.new
+                texexec  = TexExec.new
+                if pdfimages && imagemagick && texexec then
+                    if filename =~ /\.(jpg|png|pdf)$/io then
+                        @command.report("processing #{basename}")
+                        File.deletefiles(ppmname,jpgname,newname)
+                        switch = if ! invert then '-negate' else '' end
+                        if filename =~ /\.(pdf)$/io then
+                            ok = pdfimages.process("-j -f 1 -l 1 #{oldname} #{@tmpname}")
+                            if ok then
+                                if FileTest.file?(ppmname) then
+                                    inpname = ppmname
+                                elsif FileTest.file?(jpgname) then
+                                    inpname = jpgname
+                                else
+                                    ok = false
+                                end
+                                if ok then
+                                    ok = imagemagick.process("-colorspace gray #{switch} -compress zip #{inpname} #{outname}")
+                                end
+                            end
+                        else
+                            ok = imagemagick.process("-colorspace gray #{switch} -compress zip #{oldname} #{outname}")
+                        end
+                        if ok then
+                            ok = false unless FileTest.file?(outname)
+                        end
+                        if ok then
+                            if f = File.open(texname, 'w') then
+                                f.puts(conversionfile(filename,outname,newname))
+                                f.close
+                                ok = texexec.process(texname)
+                            else
+                                ok = false
+                            end
+                            @command.report("error in processing #{newname}") unless ok
+                            if FileTest.file?(pdfname) then
+                                if f = File.open(pdfname,'r') then
+                                    f.binmode
+                                    begin
+                                        if g = File.open(newname,'w') then
+                                            g.binmode
+                                            data = f.read
+                                            # pdftex (direct) & imagemagick (indirect)
+                                            if data =~ /(\d+)\s+0\s+obj\s+\[\/Separation\s+\/#{@colorname}/mos then
+                                                @command.report("replacing separation color")
+                                                object = $1
+                                                data.gsub!(/(\/Type\s+\/XObject.*?)(\/ColorSpace\s*(\/DeviceGray|\/DeviceCMYK|\/DeviceRGB|\d+\s+\d+\s+R))/moi) do
+                                                    $1 + "/ColorSpace #{object} 0 R".ljust($2.length)
+                                                end
+                                            elsif data =~ /(\d+)\s+0\s+obj\s+\[\/Indexed\s*\[/mos then
+                                                @command.report("replacing indexed color")
+                                                # todo: more precise check on color
+                                                object = $1
+                                                data.gsub!(/(\/Type\s+\/XObject.*?)(\/ColorSpace\s*(\/DeviceGray|\/DeviceCMYK|\/DeviceRGB|\d+\s+\d+\s+R))/moi) do
+                                                    $1 + "/ColorSpace #{object} 0 R".ljust($2.length)
+                                                end
+                                            elsif data =~ /(\d+)\s+0\s+obj\s+\[\/Separation/mos then
+                                                @command.report("replacing separation color")
+                                                object = $1
+                                                data.gsub!(/(\/Type\s+\/XObject.*?)(\/ColorSpace\s*(\/DeviceGray|\/DeviceCMYK|\/DeviceRGB|\d+\s+\d+\s+R))/moi) do
+                                                    $1 + "/ColorSpace #{object} 0 R".ljust($2.length)
+                                                end
+                                            end
+                                            g.write(data)
+                                            g.close
+                                        end
+                                    rescue
+                                        @command.report("error in converting #{newname}")
+                                    else
+                                        @command.report("#{newname} is converted")
+                                    end
+                                    f.close
+                                end
+                            else
+                                @command.report("error in writing #{newname}")
+                            end
+                        else
+                            @command.report("error in producing #{newname}")
+                        end
+                        File.deletefiles(ppmname,jpgname,outname)
+                        # File.deletefiles(texname,pdfname)
+                        File.syncmtimes(filename,newname) if retain
+                    end
+                else
+                    @command.report("error in locating binaries")
+                end
+            else
+                @command.report("#{newname} is not changed")
+            end
+        end
+    end
+
+    private
+
+    # % example colorfile:
+    #
+    # \definecolor [darkblue]   [c=1,m=.38,y=0,k=.64] % pantone pms 2965 uncoated m
+    # \definecolor [darkyellow] [c=0,m=.28,y=1,k=.06] % pantone pms  124 uncoated m
+    #
+    # % \definecolor [darkblue-100]    [darkblue]   [p=1]
+    # % \definecolor [darkyellow-100]  [darkyellow] [p=1]
+    #
+    # \definecolorcombination [pdftoolscolor] [darkblue=.12,darkyellow=.28] [c=.1,m=.1,y=.3,k=.1]
+
+    def conversionfile(originalname,filename,finalname)
+        tex = "\\setupcolors[state=start]\n"
+        if @colorfile then
+            tex += "\\readfile{#{@colorfile}}{}{}\n"
+            tex += "\\starttext\n"
+            # tex += "\\predefineindexcolor[pdftoolscolor]\n"
+            tex += "\\startTEXpage\n"
+            tex += "\\pdfimage{#{filename}}\n"
+            tex += "\\stopTEXpage\n"
+            tex += "\\stoptext\n"
+        else
+            tex += "\\definecolor[#{@colorname}][#{@colorspec}]\n"
+            tex += "\\definecolor[pdftoolscolor][#{@colorname}][p=1]\n"
+            tex += "\\starttext\n"
+            tex += "\\startTEXpage\n"
+            tex += "\\hbox{\\color[pdftoolscolor]{\\pdfimage{#{filename}}}}\n"
+            tex += "\\stopTEXpage\n"
+            tex += "\\stoptext\n"
+        end
+        tex += "\n"
+        tex += "% old: #{originalname}\n"
+        tex += "% new: #{finalname}\n"
+        return tex
+    end
+
+end
+
+module XML
+
+    def XML::version
+        "<?xml version='1.0'?>"
+    end
+
+    def XML::start(element, attributes='')
+        if attributes.empty? then
+            "<#{element}>"
+        else
+            "<#{element} #{attributes}>"
+        end
+    end
+
+    def XML::end(element)
+        "</#{element}>"
+    end
+
+    def XML::empty(element, attributes='')
+        if attributes && attributes.empty? then
+            "<#{element}/>"
+        else
+            "<#{element} #{attributes}/>"
+        end
+    end
+
+    def XML::element(element, attributes='', content='')
+        if content && ! content.empty? then
+            XML::start(element,attributes) + content + XML::end(element)
+        else
+            XML::empty(element,attributes)
+        end
+    end
+
+    def XML::box(tag, rect, type=1)
+        case type
+            when 1
+                if rect && ! rect.empty? then
+                    rect = rect.split(' ')
+                    XML::element("#{tag}box", '',
+                        XML::element("llx", '', rect[0]) +
+                        XML::element("lly", '', rect[1]) +
+                        XML::element("ulx", '', rect[2]) +
+                        XML::element("uly", '', rect[3]) )
+                else
+                    XML::empty("#{tag}box")
+                end
+            when 2
+                if rect && ! rect.empty? then
+                    rect = rect.split(' ')
+                    XML::element("box", "type='#{tag}'",
+                        XML::element("llx", '', rect[0]) +
+                        XML::element("lly", '', rect[1]) +
+                        XML::element("ulx", '', rect[2]) +
+                        XML::element("uly", '', rect[3]) )
+                else
+                    XML::empty("box", "type='#{tag}'")
+                end
+            when 3
+                if rect && ! rect.empty? then
+                    rect = rect.split(' ')
+                    XML::element("box", "type='#{tag}' llx='#{rect[0]}' lly='#{rect[1]}' ulx='#{rect[2]}' uly='#{rect[3]}'")
+                else
+                    XML::empty("box", "type='#{tag}'")
+                end
+            else
+                ''
+        end
+    end
+
+    def XML::crlf
+        "\n"
+    end
+
+    def XML::skip(n=1)
+        '  '*n
+    end
+
+end
+
+class Commands
+
+    include CommandBase
+
+    # alias savedhelp :help
+
+    # def help
+        # savedhelp
+        # report("under construction (still separate tools)")
+    # end
+
+    # filename.pdf --spotimage --colorname=darkblue --colorspec=1,0.38,0,0.64
+
+    def spotimage
+
+        if ! @commandline.argument('first').empty? && files = findfiles() then
+            colorname = @commandline.option('colorname')
+            colorspec = @commandline.option('colorspec')
+            if colorname && ! colorname.empty? && colorspec && ! colorspec.empty? then
+                files.each do |filename|
+                    s = SpotColorImage.new(self)
+                    s.registercolor(colorspec,colorname)
+                    s.convertgrayimage(filename)
+                end
+            else
+                report("provide --colorname=somename --colorspec=c,m,y,k")
+            end
+        else
+            report("provide filename (png, jpg, pdf)")
+        end
+
+    end
+
+    def colorimage
+
+        if ! @commandline.argument('first').empty? && files = findfiles() then
+            colorname = @commandline.option('colorname')
+            colorspec = @commandline.option('colorspec')
+            if colorspec && ! colorspec.empty? then
+                files.each do |filename|
+                    s = ColorImage.new(self)
+                    s.registercolor(colorspec,colorname) # name optional
+                    s.convertimage(filename)
+                end
+            else
+                report("provide --colorspec=c,m,y,k")
+            end
+        else
+            report("provide filename")
+        end
+
+    end
+
+    def convertimage
+
+        if ! @commandline.argument('first').empty? && files = findfiles() then
+            files.each do |filename|
+                s = ConvertImage.new(self)
+                s.convertimage(filename)
+            end
+        else
+            report("provide filename")
+        end
+
+    end
+
+    def downsampleimage
+
+        if ! @commandline.argument('first').empty? && files = findfiles() then
+            files.each do |filename|
+                s = DownsampleImage.new(self)
+                s.convertimage(filename)
+            end
+        else
+            report("provide filename")
+        end
+
+    end
+
+    def info
+
+        if files = findfiles() then
+
+            print(XML.version + XML.crlf)
+            print(XML.start('pdfinfo', "xmlns='http://www.pragma-ade.com/schemas/pdfinfo.rng'") + XML.crlf)
+
+            files.each do |filename|
+
+                if filename =~ /\.pdf$/io then
+
+                    begin
+                        data = `pdfinfo -box #{filename}`.chomp.split("\n")
+                    rescue
+                        data = nil
+                    end
+
+                    if data then
+
+                        pairs = Hash.new
+
+                        data.each do |d|
+                            if (d =~ /^\s*(.*?)\s*\:\s*(.*?)\s*$/moi) then
+                                key, val = $1, $2
+                                pairs[key.downcase.sub(/ /,'')] = val
+                            end
+                        end
+
+                        print(XML.skip(1) + XML.start('pdffile', "filename='#{filename}'") + XML.crlf)
+
+                        print(XML.skip(2) + XML.element('path', '', File.expand_path(filename)) + XML.crlf)
+
+                        if pairs.key?('error') then
+
+                            print(XML.skip(2) + XML.element('comment', '', pairs['error']) + XML.crlf)
+
+                        else
+
+                            print(XML.skip(2) + XML.element('version',  '', pairs['pdfversion']) + XML.crlf)
+                            print(XML.skip(2) + XML.element('pages',    '', pairs['pages'     ]) + XML.crlf)
+                            print(XML.skip(2) + XML.element('title',    '', pairs['title'     ]) + XML.crlf)
+                            print(XML.skip(2) + XML.element('subject',  '', pairs['subject'   ]) + XML.crlf)
+                            print(XML.skip(2) + XML.element('author',   '', pairs['author'    ]) + XML.crlf)
+                            print(XML.skip(2) + XML.element('producer', '', pairs['producer'  ]) + XML.crlf)
+
+                            if pairs.key?('creationdate') then
+                                pairs['creationdate'].sub!(/(\d\d)\/(\d\d)\/(\d\d)/) do
+                                    '20' + $3 + '-' + $1 + '-' +$2
+                                end
+                                pairs['creationdate'].sub!(/(\d\d)\/(\d\d)\/(\d\d\d\d)/) do
+                                    $3 + '-' + $1 + '-' + $2
+                                end
+                                print(XML.skip(2) + XML.element('creationdate', '', pairs['creationdate']) + XML.crlf)
+                            end
+
+                            if pairs.key?('moddate') then
+                                if pairs['moddate'] =~ /(\d\d\d\d)(\d\d)(\d\d)/ then
+                                    pairs['moddate'] = "#{$1}-#{$2}-#{$3}"
+                                end
+                                print(XML.skip(2) + XML.element('modificationdate', '', pairs['moddate']) + XML.crlf)
+                            end
+
+                            print(XML.skip(2) + XML.element('tagged',    '', pairs['tagged'   ]) + XML.crlf)
+                            print(XML.skip(2) + XML.element('encrypted', '', pairs['encrypted']) + XML.crlf)
+                            print(XML.skip(2) + XML.element('optimized', '', pairs['optimized']) + XML.crlf)
+
+                            if pairs.key?('PageSize') then
+                                print(XML.skip(2) + XML.element('width',  '', pairs['pagesize'].sub(/\s*(.*?)\s+(.*?)\s+.*/, $1)) + XML.crlf)
+                                print(XML.skip(2) + XML.element('height', '', pairs['pagesize'].sub(/\s*(.*?)\s+(.*?)\s+.*/, $2)) + XML.crlf)
+                            end
+
+                            if pairs.key?('FileSize') then
+                                print(XML.skip(2) + XML.element('size', '', pairs['filesize'].sub(/\s*(.*?)\s+.*/, $1)) + XML.crlf)
+                            end
+
+                            print(XML.skip(2) + XML.box('media', pairs['mediabox']) + XML.crlf)
+                            print(XML.skip(2) + XML.box('crop' , pairs['cropbox' ]) + XML.crlf)
+                            print(XML.skip(2) + XML.box('bleed', pairs['bleedbox']) + XML.crlf)
+                            print(XML.skip(2) + XML.box('trim' , pairs['trimBox' ]) + XML.crlf)
+                            print(XML.skip(2) + XML.box('art'  , pairs['artbox'  ]) + XML.crlf)
+
+                        end
+
+                        print(XML.skip(1) + XML.end('pdffile') + XML.crlf)
+
+                    end
+
+                end
+
+            end
+
+            print(XML.end('pdfinfo') + XML.crlf)
+
+        end
+
+    end
+
+    # name                                 type         emb sub uni object ID
+    # ------------------------------------ ------------ --- --- --- ---------
+    # EOPLBP+TimesNewRomanPSMT             TrueType     yes yes no     167  0
+    # Times-Roman                          TrueType     no  no  no      95  0
+    # EPBAAB+Helvetica                     Type 1C      yes yes yes    108  0
+    # EPBMLE+Helvetica-Oblique             Type 1C      yes yes yes    111  0
+    # Helvetica                            TrueType     no  no  no     112  0
+
+    def checkembedded
+        $stderr = $stdout
+        $stdout.flush
+        if @commandline.option('pattern') then
+            # **/*.pdf
+            filenames, n = globfiles(@commandline.option('pattern'),'pdf'), 0
+        else
+            filenames, n = findfiles('pdf'), 0
+        end
+        filenames.sort.each do |file|
+            report("= checking #{File.expand_path(file)}")
+            result = `pdffonts #{file}`.chomp
+            lines = result.split(/\n/)
+            if result =~ /emb\s+sub\s+uni/io then
+                lines.each do |line|
+                    report("! #{line}") if line =~ /no\s+(no|yes)\s+(no|yes)/io
+                end
+            else
+                lines.each do |line|
+                    report("? #{line}")
+                end
+            end
+            report("")
+        end
+    end
+
+    def countpages
+        if @commandline.option('pattern') then
+            filenames, n = globfiles(@commandline.option('pattern'),'pdf'), 0
+        else
+            filenames, n = findfiles('pdf'), 0
+        end
+        threshold = @commandline.option('threshold').to_i rescue 0
+        filenames.each do |filename|
+            if `pdfinfo #{filename}`.chomp =~ /^pages\s*\:\s*(\d+)/moi then
+                p = $1
+                m = p.to_i rescue 0
+                if threshold == 0 or m > threshold then
+                    report("#{p.rjust(4)} pages found in #{filename}")
+                    n += m
+                end
+           end
+        end
+        report("")
+        report("#{n.to_s.rjust(4)} pages in total")
+    end
+
+    def analyzefile
+        # needs an update
+        filenames = @commandline.arguments
+        filenames.each do |filename|
+            if filename && FileTest.file?(filename) && filename =~ /\.pdf/io then
+                filesize = FileTest.size(filename)
+                report("analyzing file : #{filename}")
+                report("file size : #{filesize}")
+                if pdf = File.open(filename) then
+                    pdf.binmode
+                    nofobject, nofxform, nofannot, noflink, nofwidget, nofnamed, nofscript, nofcross = 0, 0, 0, 0, 0, 0, 0, 0
+                    while data = pdf.gets do
+                        data.scan(/\d+\s+\d+\s+obj/o)      do nofobject += 1 end
+                        data.scan(/\/Type\s*\/XObject/o)   do nofxform  += 1 end
+                        data.scan(/\/Type\s*\/Annot/o)     do nofannot  += 1 end
+                        data.scan(/\/GoToR\s*\/F/o)        do nofcross  += 1 end
+                        data.scan(/\/Subtype\s*\/Link/o)   do noflink   += 1 end
+                        data.scan(/\/Subtype\s*\/Widget/o) do nofwidget += 1 end
+                        data.scan(/\/S\s*\/Named/o)        do nofnamed  += 1 end
+                        data.scan(/\/S\s*\/JavaScript/o)   do nofscript += 1 end
+                    end
+                    pdf.close
+                    report("objects : #{nofobject}")
+                    report("xforms : #{nofxform}")
+                    report("annotations : #{nofannot}")
+                    report("links : #{noflink} (#{nofnamed} named / #{nofscript} scripts / #{nofcross} files)")
+                    report("widgets : #{nofwidget}")
+                end
+            end
+        end
+    end
+
+end
+
+logger      = Logger.new(banner.shift)
+commandline = CommandLine.new
+
+commandline.registeraction('spotimage' ,      'filename --colorspec=  --colorname=  [--retain --invert --subpath=]')
+commandline.registeraction('colorimage',      'filename --colorspec= [--retain --invert --colorname= ]')
+commandline.registeraction('convertimage',    'filename [--retain --subpath]')
+commandline.registeraction('downsampleimage', 'filename [--retain --subpath --lowres --normal]')
+commandline.registeraction('info',            'filename')
+commandline.registeraction('countpages',      '[--pattern --threshold]')
+commandline.registeraction('checkembedded',   '[--pattern]')
+
+commandline.registeraction('analyzefile' ,    'filename')
+
+commandline.registeraction('help')
+commandline.registeraction('version')
+
+commandline.registervalue('colorname')
+commandline.registervalue('colorspec')
+commandline.registervalue('subpath')
+commandline.registervalue('pattern')
+commandline.registervalue('threshold',0)
+
+commandline.registerflag('lowres')
+commandline.registerflag('medres')
+commandline.registerflag('normal')
+commandline.registerflag('invert')
+commandline.registerflag('retain')
+
+commandline.expand
+
+Commands.new(commandline,logger,banner).send(commandline.action || 'help')
diff --git a/scripts/context/ruby/pstopdf.rb b/scripts/context/ruby/pstopdf.rb
new file mode 100644
index 000000000..73e628df2
--- /dev/null
+++ b/scripts/context/ruby/pstopdf.rb
@@ -0,0 +1,533 @@
+#!/usr/bin/env ruby
+
+# program   : pstopdf
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2002-2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+banner = ['PsToPdf', 'version 2.0.1', '2002-2006', 'PRAGMA ADE/POD']
+
+$: << File.expand_path(File.dirname($0)) ; $: << File.join($:.last,'lib') ; $:.uniq!
+
+# todo: paden/prefix in magick and inkscape
+# todo: clean up method handling (pass strings, no numbers)
+# --method=crop|bounded|raw|...
+# --resolution=low|normal|medium|high|printer|print|screen|ebook|default
+# + downward compatible flag handling
+
+require 'base/switch'
+require 'base/tool'
+require 'base/logger'
+
+require 'graphics/gs'
+require 'graphics/magick'
+require 'graphics/inkscape'
+
+require 'rexml/document'
+
+exit if defined?(REQUIRE2LIB)
+
+class Commands
+
+    include CommandBase
+
+    # nowadays we would force a directive, but
+    # for old times sake we handle default usage
+
+    def main
+        filename = @commandline.argument('first')
+        pattern  = @commandline.option('pattern')
+        if filename.empty? && ! pattern.empty? then
+            pattern = "**/#{pattern}" if @commandline.option('recurse')
+            globfiles(pattern)
+        end
+        filename = @commandline.argument('first')
+        if filename.empty? then
+            help
+        elsif filename =~ /\.exa$/ then
+            request
+        else
+            convert
+        end
+    end
+
+    # actions
+
+    def convert
+
+        ghostscript = GhostScript.new(logger)
+        magick      = ImageMagick.new(logger)
+        inkscape    = InkScape.new(logger)
+
+        outpath = @commandline.option('outputpath')
+        unless outpath.empty? then
+            begin
+                File.expand_path(outpath)
+                outpath = File.makedirs(outpath) unless FileTest.directory?(outpath)
+            rescue
+                # sorry
+            end
+        end
+
+        @commandline.arguments.each do |filename|
+
+            filename = Tool.cleanfilename(filename,@commandline) # brrrr
+            inppath = @commandline.option('inputpath')
+            if inppath.empty? then
+               inppath = '.'
+               fullname = filename # avoid duplicate './'
+            else
+               fullname = File.join(inppath,filename)
+            end
+            if FileTest.file?(fullname) then
+                handle_whatever(ghostscript,inkscape,magick,filename)
+            else
+                report("file #{fullname} does not exist")
+            end
+
+        end
+
+    end
+
+    def request
+
+        # <exa:request>
+        #   <exa:application>
+        #     <exa:command>pstopdf</exa:command>
+        #     <exa:filename>E:/tmp/demo.ps</exa:filename>
+        #   </exa:application>
+        #   <exa:data>
+        #     <exa:variable label='gs:DoThumbnails'>false</exa:variable>
+        #     <exa:variable label='gs:ColorImageDepth'>-1</exa:variable>
+        #   </exa:data>
+        # </exa:request>
+
+        ghostscript = GhostScript.new(logger)
+        magick      = ImageMagick.new(logger)
+        inkscape    = InkScape.new(logger)
+
+        dataname = @commandline.argument('first')  || ''
+        filename = @commandline.argument('second') || ''
+
+        if dataname.empty? || ! FileTest.file?(dataname) then
+            report('provide valid exa file')
+            return
+        else
+            begin
+                request = REXML::Document.new(File.new(dataname))
+            rescue
+                report('provide valid exa file (xml error)')
+                return
+            end
+        end
+        if filename.empty? then
+            begin
+                if filename = REXML::XPath.first(request.root,"exa:request/exa:application/exa:filename/text()") then
+                    filename = filename.to_s
+                else
+                    report('no filename found in exa file')
+                    return
+                end
+            rescue
+                filename = ''
+            end
+        end
+        if filename.empty? then
+            report('provide valid filename')
+            return
+        elsif ! FileTest.file?(filename) then
+            report("invalid filename #{filename}")
+            return
+        end
+
+        [ghostscript,inkscape,magick].each do |i|
+            i.setvariable('inputfile',filename)
+        end
+
+        # set ghostscript variables
+        REXML::XPath.each(request.root,"/exa:request/exa:data/exa:variable") do |v|
+            begin
+                if (key = v.attributes['label']) and (value = v.text.to_s) then
+                    case key
+                        when /gs[\:\.](var[\:\.])*(offset)/io then ghostscript.setoffset(value)
+                        when /gs[\:\.](var[\:\.])*(method)/io then ghostscript.setvariable('method',value)
+                        when /gs[\:\.](var[\:\.])*(.*)/io     then ghostscript.setpsoption($2,value)
+                    end
+                end
+            rescue
+            end
+        end
+
+        # no inkscape and magick variables (yet)
+
+        handle_whatever(ghostscript,inkscape,magick,filename)
+
+    end
+
+    def watch
+
+        ghostscript = GhostScript.new(logger)
+        magick      = ImageMagick.new(logger)
+        inkscape    = InkScape.new(logger)
+
+        pathname = commandline.option('watch')
+
+        unless pathname and not pathname.empty? then
+            report('empty watchpath is not supported')
+            exit
+        end
+
+        if pathname == '.' then
+            report("watchpath #{pathname} is not supported")
+            exit
+        end
+
+        if FileTest.directory?(pathname) then
+            if Dir.chdir(pathname) then
+                report("watching path #{pathname}")
+            else
+                report("unable to change to path #{pathname}")
+                exit
+            end
+        else
+            report("invalid path #{pathname}")
+            exit
+        end
+
+        waiting = false
+
+        loop do
+
+            if waiting then
+                report("waiting #{getvariable('delay')}")
+                waiting = false
+                sleep(getvariable('delay').to_i)
+            end
+
+            files = Dir.glob("**/*.*")
+
+            if files and files.length > 0 then
+
+                files.each do |fullname|
+
+                    next unless fullname
+
+                    if FileTest.directory?(fullname) then
+                        debug('skipping path', fullname)
+                        next
+                    end
+
+                    unless magick.supported(fullname) then
+                        debug('not supported', fullname)
+                        next
+                    end
+
+                    if (! FileTest.file?(fullname)) || (FileTest.size(fullname) < 100) then
+                        debug("skipping small crap file #{fullname}")
+                        next
+                    end
+
+                    debug("handling file #{fullname}")
+
+                    begin
+                        next unless File.rename(fullname,fullname) # access trick
+                    rescue
+                        next                                       # being written
+                    end
+
+                    fullname = Tool.cleanfilename(fullname,@commandline)
+
+                    fullname.gsub!(/\\/io, '/')
+
+                    filename = File.basename(fullname)
+                    filepath = File.dirname(fullname)
+
+                    next if filename =~ /gstemp.*/io
+
+                    if filepath !~ /(result|done|raw|crop|bound|bitmap)/io then
+                        begin
+                            File.makedirs(filepath+'/raw')
+                            File.makedirs(filepath+'/bound')
+                            File.makedirs(filepath+'/crop')
+                            File.makedirs(filepath+'/bitmap')
+                            debug("creating prefered input paths on #{filepath}")
+                        rescue
+                            debug("creating input paths on #{filepath} failed")
+                        end
+                    end
+
+                    if filepath =~ /^(.*\/|)(done|result)$/io then
+                        debug("skipping file #{fullname}")
+                    else
+                        report("start processing file #{fullname}")
+                        if filepath =~ /^(.*\/*)(raw|crop|bound)$/io then
+                            donepath = $1 + 'done'
+                            resultpath = $1 + 'result'
+                            case $2
+                                when 'raw'   then method = 1
+                                when 'bound' then method = 2
+                                when 'crop'  then method = 3
+                                else              method = 2
+                            end
+                            report("forcing method #{method}")
+                        else
+                            method = 2
+                            donepath = filepath + '/done'
+                            resultpath = filepath + '/result'
+                            report("default method #{method}")
+                        end
+
+                        begin
+                            File.makedirs(donepath)
+                            File.makedirs(resultpath)
+                        rescue
+                            report('result path creation fails')
+                        end
+
+                        if FileTest.directory?(donepath) && FileTest.directory?(resultpath) then
+
+                            resultname = resultpath + '/' + filename.sub(/\.[^\.]*$/,'') + '.pdf'
+
+                            @commandline.setoption('inputpath',  filepath)
+                            @commandline.setoption('outputpath', resultpath)
+                            @commandline.setoption('method',     method)
+
+                            if ghostscript.psfile?(fullname) then
+                                handle_ghostscript(ghostscript,filename)
+                            else
+                                handle_magick(magick,filename)
+                            end
+
+                            sleep(1) # calm down
+
+                            if FileTest.file?(fullname) then
+                                begin
+                                    File.copy(fullname,donepath + '/' + filename)
+                                    File.delete(fullname)
+                                rescue
+                                    report('cleanup fails')
+                                end
+                            end
+
+                        end
+
+                    end
+
+                end
+
+            end
+
+            waiting = true
+        end
+
+    end
+
+    private
+
+    def handle_whatever(ghostscript,inkscape,magick,filename)
+        if ghostscript.psfile?(filename) then
+            # report("processing ps file #{filename}")
+            ghostscript.setvariable('pipe',false) if @commandline.option('nopipe')
+            # ghostscript.setvariable('pipe',not @commandline.option('nopipe'))
+            ghostscript.setvariable('colormodel',@commandline.option('colormodel'))
+            ghostscript.setvariable('offset',@commandline.option('offset'))
+            handle_ghostscript(ghostscript,filename)
+        elsif ghostscript.pdffile?(filename) && ghostscript.pdfmethod?(@commandline.option('method')) then
+            # report("processing pdf file #{filename}")
+            handle_ghostscript(ghostscript,filename)
+        elsif inkscape.supported?(filename) then
+            # report("processing non ps/pdf file #{filename}")
+            handle_inkscape(inkscape,filename)
+        elsif magick.supported?(filename) then
+            # report("processing non ps/pdf file #{filename}")
+            handle_magick(magick,filename)
+        else
+            report("option not supported for #{filename}")
+        end
+    end
+
+    def handle_magick(magick,filename)
+
+        report("converting non-ps file #{filename} into pdf")
+
+        inppath = @commandline.option('inputpath')
+        outpath = @commandline.option('outputpath')
+
+        inppath = inppath + '/' if not inppath.empty?
+        outpath = outpath + '/' if not outpath.empty?
+
+        prefix = @commandline.option('prefix')
+        suffix = @commandline.option('suffix')
+
+        inpfilename = "#{inppath}#{filename}"
+        outfilename = "#{outpath}#{prefix}#{filename.sub(/\.([^\.]*?)$/, '')}#{suffix}.pdf"
+
+        magick.setvariable('inputfile' , inpfilename)
+        magick.setvariable('outputfile', outfilename)
+
+        magick.autoconvert
+
+    end
+
+    def handle_inkscape(inkscape,filename)
+
+        report("converting svg(z) file #{filename} into pdf")
+
+        inppath = @commandline.option('inputpath')
+        outpath = @commandline.option('outputpath')
+
+        inppath = inppath + '/' if not inppath.empty?
+        outpath = outpath + '/' if not outpath.empty?
+
+        prefix = @commandline.option('prefix')
+        suffix = @commandline.option('suffix')
+
+        inpfilename = "#{inppath}#{filename}"
+        outfilename = "#{outpath}#{prefix}#{filename.sub(/\.([^\.]*?)$/, '')}#{suffix}.pdf"
+
+        inkscape.setvariable('inputfile' , inpfilename)
+        inkscape.setvariable('outputfile', outfilename)
+
+        if @commandline.option('verbose') || @commandline.option('debug') then
+            logname = filename.gsub(/\.[^\.]*?$/, '.log')
+            report("log info saved in #{logname}")
+            inkscape.convert(logname) # logname ook doorgeven
+        else
+            inkscape.convert
+        end
+
+    end
+
+    def handle_ghostscript(ghostscript,filename)
+
+        ghostscript.reset
+
+        method = ghostscript.method(@commandline.option('method'))
+        force = ghostscript.method(@commandline.option('force'))
+
+        ghostscript.setvariable('method', method)
+        ghostscript.setvariable('force', force)
+
+        # report("conversion method #{method}")
+
+        inppath = @commandline.option('inputpath')
+        outpath = @commandline.option('outputpath')
+
+        inppath = inppath + '/' if not inppath.empty?
+        outpath = outpath + '/' if not outpath.empty?
+
+        prefix = @commandline.option('prefix')
+        suffix = @commandline.option('suffix')
+
+        ok = false
+
+        if ghostscript.pdfmethod?(method) then
+
+            report("converting pdf file #{filename} into pdf")
+
+            if prefix.empty? && suffix.empty? && inppath.empty? && outpath.empty? then
+                prefix = ghostscript.pdfprefix(method)
+            end
+
+            if ghostscript.pdffile?(filename) then
+
+                filename = filename.sub(/\.pdf$/, '')
+
+                inpfilename = "#{inppath}#{filename}.pdf"
+                outfilename = "#{outpath}#{prefix}#{filename}#{suffix}.pdf"
+
+                ghostscript.setvariable('inputfile' ,inpfilename)
+                ghostscript.setvariable('outputfile',outfilename)
+
+                if FileTest.file?(inpfilename) then
+                    ok = ghostscript.convert
+                else
+                    report("no file found #{filename}")
+                end
+
+            else
+                report("no pdf file #{filename}")
+            end
+
+        elsif ghostscript.psfile?(filename) then
+
+            if filename =~ /(.*)\.([^\.]*?)$/io then
+                filename, filesuffix = $1, $2
+            else
+                filesuffix = 'eps'
+            end
+
+            report("converting #{filesuffix} (ps) into pdf")
+
+            inpfilename = "#{inppath}#{filename}.#{filesuffix}"
+            outfilename = "#{outpath}#{prefix}#{filename}#{suffix}.pdf"
+
+            ghostscript.setvariable('inputfile' , inpfilename)
+            ghostscript.setvariable('outputfile', outfilename)
+
+            if FileTest.file?(inpfilename) then
+                ok = ghostscript.convert
+                if ! ok && FileTest.file?(outfilename) then
+                    begin
+                        File.delete(outfilename)
+                    rescue
+                    end
+                end
+            else
+                report("no file with name #{filename} found")
+            end
+
+        else
+            report('file must be of type eps/ps/ai/pdf')
+        end
+
+        return ok
+
+    end
+
+end
+
+# ook pdf -> pdf onder optie 0, andere kleurruimte
+
+logger      = Logger.new(banner.shift)
+commandline = CommandLine.new
+
+commandline.registerflag('debug')
+commandline.registerflag('verbose')
+commandline.registerflag('nopipe')
+
+commandline.registervalue('method',2)
+commandline.registervalue('offset',0)
+
+commandline.registervalue('prefix')
+commandline.registervalue('suffix')
+
+commandline.registervalue('inputpath')
+commandline.registervalue('outputpath')
+
+commandline.registerflag('watch')
+commandline.registerflag('force')
+commandline.registerflag('recurse')
+
+commandline.registervalue('delay',2)
+
+commandline.registervalue('colormodel','cmyk')
+commandline.registervalue('pattern','')
+
+commandline.registeraction('help')
+commandline.registeraction('version')
+
+commandline.registeraction('convert', 'convert ps into pdf')
+commandline.registeraction('request', 'handles exa request file')
+commandline.registeraction('watch',   'watch folders for conversions (untested)')
+
+commandline.expand
+
+logger.verbose if (commandline.option('verbose') || commandline.option('debug'))
+
+Commands.new(commandline,logger,banner).send(commandline.action || 'main')
diff --git a/scripts/context/ruby/rlxtools.rb b/scripts/context/ruby/rlxtools.rb
new file mode 100644
index 000000000..6a5c5ca20
--- /dev/null
+++ b/scripts/context/ruby/rlxtools.rb
@@ -0,0 +1,368 @@
+#!/usr/bin/env ruby
+
+# program   : rlxtools
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2004-2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+banner = ['RlxTools', 'version 1.0.1', '2004/2005', 'PRAGMA ADE/POD']
+
+$: << File.expand_path(File.dirname($0)) ; $: << File.join($:.last,'lib') ; $:.uniq!
+
+require 'base/switch'
+require 'base/logger'
+require 'base/system'
+require 'base/kpse'
+
+require 'fileutils'
+# require 'ftools'
+require 'rexml/document'
+
+class Commands
+
+    include CommandBase
+
+    # <?xml version='1.0 standalone='yes'?>
+    # <rl:manipulators>
+    #    <rl:manipulator name='lowres' suffix='pdf'>
+    #         <rl:step>
+    #             texmfstart
+    #             --verbose
+    #             --iftouched=<rl:value name='path'/>/<rl:value name='file'/>,<rl:value name='path'/>/<rl:value name='prefix'/><rl:value name='file'/>
+    #             pstopdf
+    #             --method=5
+    #             --inputpath=<rl:value name='path'/>
+    #             --outputpath=<rl:value name='path'/>/<rl:value name='prefix'/>
+    #             <rl:value name='file'/>
+    #         </rl:step>
+    #     </rl:manipulator>
+    # </rl:manipulators>
+    #
+    # <?xml version='1.0' standalone='yes'?>
+    # <rl:library>
+    #     <rl:usage>
+    #         <rl:type>figure</rl:type>
+    #         <rl:state>found</rl:state>
+    #         <rl:file>cow.pdf</rl:file>
+    #         <rl:suffix>pdf</rl:suffix>
+    #         <rl:path>.</rl:path>
+    #         <rl:conversion>lowres</rl:conversion>
+    #         <rl:prefix>lowres/</rl:prefix>
+    #         <rl:width>276.03125pt</rl:width>
+    #         <rl:height>200.75pt</rl:height>
+    #     </rl:usage>
+    # </r:library>
+
+    def manipulate
+
+        procname = @commandline.argument('first')  || ''
+        filename = @commandline.argument('second') || ''
+
+        procname = Kpse.found(procname)
+
+        if procname.empty? || ! FileTest.file?(procname) then
+            report('provide valid manipulator file')
+        elsif filename.empty? || ! FileTest.file?(filename) then
+            report('provide valid resource log file')
+        else
+            begin
+                data = REXML::Document.new(File.new(filename))
+            rescue
+                report('provide valid resource log file (xml error)')
+                return
+            end
+            begin
+                proc = REXML::Document.new(File.new(procname))
+            rescue
+                report('provide valid manipulator file (xml error)')
+                return
+            end
+            report("manipulator file: #{procname}")
+            report("resourcelog file: #{filename}")
+            begin
+                nofrecords, nofdone = 0, 0
+                REXML::XPath.each(data.root,"/rl:library/rl:usage") do |usage|
+                    nofrecords += 1
+                    variables = Hash.new
+                    usage.elements.each do |e|
+                        variables[e.name] = e.text.to_s
+                    end
+                    report("processing record #{nofrecords} (#{variables['file'] || 'noname'}: #{variables.size} entries)")
+                    if conversion = variables['conversion'] then
+                        report("testing for conversion #{conversion}")
+                        if suffix = variables['suffix'] then
+                            suffix.downcase!
+                            if ! suffix.empty? && variables['file'] && variables['file'] !~ /\.([a-z]+)$/i then
+                                variables['file'] += ".#{suffix}"
+                            end
+                            if file = variables['file'] then
+                                report("conversion #{conversion} for suffix #{suffix} for file #{file}")
+                            else
+                                report("conversion #{conversion} for suffix #{suffix}")
+                            end
+                            pattern = "@name='#{conversion}' and @suffix='#{suffix}'"
+                            if steps = REXML::XPath.first(proc.root,"/rl:manipulators/rl:manipulator[#{pattern}]") then
+                                localsteps = steps.deep_clone
+                                ['rl:old','rl:new'].each do |tag|
+                                    REXML::XPath.each(localsteps,tag) do |extras|
+                                        REXML::XPath.each(extras,"rl:value") do |value|
+                                            if name = value.attributes['name'] then
+                                                substitute(value,variables[name.to_s] || '')
+                                            end
+                                        end
+                                    end
+                                end
+                                old = REXML::XPath.first(localsteps,"rl:old")
+                                new = REXML::XPath.first(localsteps,"rl:new")
+                                if old && new then
+                                    old, new = justtext(old.to_s), justtext(new.to_s)
+                                    variables['old'], variables['new'] = old, new
+                                    begin
+                                        [old,new].each do |d|
+                                            File.makedirs(File.dirname(d))
+                                        end
+                                    rescue
+                                        report("error during path creation")
+                                    end
+                                    report("old file #{old}")
+                                    report("new file #{new}")
+                                    level = if File.needsupdate(old,new) then 2 else 0 end
+                                else
+                                    level = 1
+                                end
+                                if level>0 then
+                                    REXML::XPath.each(localsteps,"rl:step") do |command|
+                                        REXML::XPath.each(command,"rl:old") do |value|
+                                            replace(value,old)
+                                        end
+                                        REXML::XPath.each(command,"rl:new") do |value|
+                                            replace(value,new)
+                                        end
+                                        REXML::XPath.each(command,"rl:value") do |value|
+                                            if name = value.attributes['name'] then
+                                                substitute(value,variables[name.to_s])
+                                            end
+                                        end
+                                        str = justtext(command.to_s)
+                                        # str.gsub!(/(\.\/)+/io, '')
+                                        report("command #{str}")
+                                        System.run(str) unless @commandline.option('test')
+                                        report("synchronizing #{old} and #{new}")
+                                        File.syncmtimes(old,new) if level > 1
+                                        nofdone += 1
+                                    end
+                                else
+                                    report("no need for a manipulation")
+                                end
+                            else
+                                report("no manipulator found")
+                            end
+                        else
+                            report("no suffix specified")
+                        end
+                    else
+                        report("no conversion needed")
+                    end
+                end
+                if nofdone > 0 then
+                    jobname = filename.gsub(/\.(.*?)$/,'') # not 'tuo' here
+                    tuoname = jobname + '.tuo'
+                    if FileTest.file?(tuoname) && (f = File.open(tuoname,'a')) then
+                        f.puts("%\n% number of rlx manipulations: #{nofdone}\n")
+                        f.close
+                    end
+                end
+            rescue
+                report("error in manipulating files: #{$!}")
+            end
+            begin
+                logname = "#{filename}.log"
+                File.delete(logname) if FileTest.file?(logname)
+                File.copy(filename,logname)
+            rescue
+            end
+        end
+
+    end
+
+    private
+
+    def justtext(str)
+        str = str.to_s
+        str.gsub!(/<[^>]*?>/o, '')
+        str.gsub!(/\s+/o, ' ')
+        str.gsub!(/&lt;/o, '<')
+        str.gsub!(/&gt;/o, '>')
+        str.gsub!(/&amp;/o, '&')
+        str.gsub!(/&quot;/o, '"')
+        str.gsub!(/[\/\\]+/o, '/')
+        return str.strip
+    end
+
+    def substitute(value,str='')
+        if str then
+            begin
+                if value.attributes.key?('method') then
+                    str = filtered(str.to_s,value.attributes['method'].to_s)
+                end
+                if str.empty? && value.attributes.key?('default') then
+                    str = value.attributes['default'].to_s
+                end
+                value.insert_after(value,REXML::Text.new(str.to_s))
+            rescue Exception
+            end
+        end
+    end
+
+    def replace(value,str)
+        if str then
+            begin
+                value.insert_after(value,REXML::Text.new(str.to_s))
+            rescue Exception
+            end
+        end
+    end
+
+    def filtered(str,method)
+
+        str = str.to_s # to be sure
+        case method
+            when 'name' then # no path, no suffix
+                case str
+                    when /^.*[\\\/](.+?)\..*?$/o then $1
+                    when /^.*[\\\/](.+?)$/o      then $1
+                    when /^(.*)\..*?$/o          then $1
+                    else                              str
+                end
+            when 'path'     then if str =~ /^(.+)([\\\/])(.*?)$/o then $1 else ''  end
+            when 'suffix'   then if str =~ /^.*\.(.*?)$/o         then $1 else ''  end
+            when 'nosuffix' then if str =~ /^(.*)\..*?$/o         then $1 else str end
+            when 'nopath'   then if str =~ /^.*[\\\/](.*?)$/o     then $1 else str end
+            else                                                               str
+        end
+    end
+
+end
+
+class Commands
+
+    include CommandBase
+
+    @@xmlbanner = "<?xml version='1.0' standalone='yes'?>"
+
+    def identify(resultfile='rlxtools.rli')
+        if @commandline.option('collect') then
+            begin
+                File.open(resultfile,'w') do |f|
+                    f << "#{@@xmlbanner}\n"
+                    f << "<rl:identification>\n"
+                    @commandline.arguments.each do |filename|
+                        if state = do_identify(filename) then
+                            report("#{filename} is identified")
+                            f << state
+                        else
+                            report("unable to identify #{filename}")
+                        end
+                    end
+                    f << "</rl:identification>\n"
+                    report("result saved in #{resultfile}")
+                end
+            rescue
+                report("error in writing result")
+            end
+        else
+            @commandline.arguments.each do |filename|
+                if state = do_identify(filename) then
+                    begin
+                        File.open(filename+'.rli','w') do |f|
+                            f << "#{@@xmlbanner}\n"
+                            f << state
+                        end
+                    rescue
+                        report("error in identifying #{filename}")
+                    else
+                        report("#{filename} is identified")
+                    end
+                else
+                    report("unable to identify #{filename}")
+                end
+            end
+        end
+    end
+
+    private
+
+    def do_identify(filename,centimeters=false)
+        begin
+            str = nil
+            if FileTest.file?(filename) then
+                # todo: use pdfinto for pdf files, identify is bugged
+                if centimeters then
+                    result = `identify -units PixelsPerCentimeter -format \"x=%x,y=%y,w=%w,h=%h,b=%b\" #{filename}`.chomp.split(',')
+                else
+                    result = `identify -units PixelsPerInch       -format \"x=%x,y=%y,w=%w,h=%h,b=%b\" #{filename}`.chomp.split(',')
+                end
+                tags = Hash.new
+                result.each do |r|
+                    if rr = r.split("=") then
+                        tags[rr[0]] = rr[1]
+                    end
+                end
+                size   = (tags['b']||0).to_i
+                width  = unified(tags['w']||0,tags['x']||'1')
+                height = unified(tags['h']||0,tags['y']||'1')
+                if size > 0 then
+                    str = ''
+                    str << "<rl:identify name='#{File.basename(filename)}'>\n"
+                    str << "  <rl:size>#{size}</rl:size>\n"
+                    str << "  <rl:path>#{File.dirname(filename).sub(/\\/o,'/')}</rl:path>\n"
+                    str << "  <rl:width>#{width}</rl:width>\n"
+                    str << "  <rl:height>#{height}</rl:height>\n"
+                    str << "</rl:identify>\n"
+                end
+            else
+                str = nil
+            end
+        rescue
+            str = nil
+        end
+        return str
+    end
+
+    def unified(dim,res)
+        case res
+            when /([\d\.]+)\s*PixelsPerInch/io then
+                sprintf("%.4fin",dim.to_f/$1.to_f)
+            when /([\d\.]+)\s*PixelsPerCentimeter/io then
+                sprintf("%.4fcm",dim.to_f/$1.to_f)
+            when /([\d\.]+)\s*PixelsPerMillimeter/io then
+                sprintf("%.4fmm",dim.to_f/$1.to_f)
+            when /([\d\.]+)\s*PixelsPerPoint/io then
+                sprintf("%.4fbp",dim.to_f/$1.to_f)
+            else
+                sprintf("%.4fbp",dim.to_f)
+        end
+    end
+
+end
+
+logger      = Logger.new(banner.shift)
+commandline = CommandLine.new
+
+commandline.registeraction('manipulate', '[--test] manipulatorfile resourselog')
+commandline.registeraction('identify'  , '[--collect] filename')
+
+commandline.registeraction('help')
+commandline.registeraction('version')
+
+commandline.registerflag('test')
+commandline.registerflag('collect')
+
+commandline.expand
+
+Commands.new(commandline,logger,banner).send(commandline.action || 'help')
diff --git a/scripts/context/ruby/rscortool.rb b/scripts/context/ruby/rscortool.rb
new file mode 100644
index 000000000..c656fed85
--- /dev/null
+++ b/scripts/context/ruby/rscortool.rb
@@ -0,0 +1,63 @@
+# program   : rscortool
+# copyright : PRAGMA Publishing On Demand
+# version   : 1.00 - 2002
+# author    : Hans Hagen
+#
+# project   : eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-pod.com / www.pragma-ade.com
+
+require 'rexml/document.rb'
+
+class Array
+
+    def downcase
+        self.collect { |l| l.to_s.downcase }
+    end
+
+end
+
+class SortedXML
+
+    def initialize (filename)
+        return nil if not filename or filename.empty? or not test(?e,filename)
+        @data = REXML::Document.new(File.new(filename),
+            {:ignore_whitespace_nodes => :all,
+             :compress_whitespace     => :all})
+    end
+
+    def save (filename)
+        # filename += '.xml' unless filename.match(/\..*?$/)
+        filename += '.xml' unless filename =~ /\..*?$/
+        if not filename.empty? and f = open(filename,'w')
+            @data.write(f,0)
+            f.close
+        end
+    end
+
+    def sort
+        keys = REXML::XPath.match(@data.root,"/contacts/contact/@label")
+        return unless keys
+        keys = keys.downcase
+        records = @data.elements.to_a("/contacts/contact")
+        @data.elements.delete_all("/contacts/contact")
+        keys = keys.collect do |l| # prepare numbers
+            l.gsub(/(\d+)/) do |d| sprintf('%05d', d) end
+        end
+        keys.sort.each do |s|
+            @data.root.add_element(records[keys.index(s)])
+        end
+    end
+
+end
+
+def sortfile (filename)
+    c = SortedXML.new(filename)
+    c.sort
+    c.save('test.xml')
+end
+
+exit if ARGV[0] == nil or ARGV[0].empty?
+
+sortfile(ARGV[0])
diff --git a/scripts/context/ruby/rsfiltool.rb b/scripts/context/ruby/rsfiltool.rb
new file mode 100644
index 000000000..6d7c7aba0
--- /dev/null
+++ b/scripts/context/ruby/rsfiltool.rb
@@ -0,0 +1,341 @@
+# program   : rsfiltool
+# copyright : PRAGMA Publishing On Demand
+# version   : 1.01 - 2002
+# author    : Hans Hagen
+#
+# project   : eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-pod.com / www.pragma-ade.com
+
+unless defined? ownpath
+    ownpath = $0.sub(/[\\\/]\w*?\.rb/i,'')
+    $: << ownpath
+end
+
+# --name=a,b,c.xml wordt names [a.xml, b.xml, c.xml]
+# --path=x/y/z/a,b,c.xml wordt [x/y/z/a.xml, x/y/z/b.xml, x/y/z/c.xml]
+
+# todo : split session stuff from xmpl/base into an xmpl/session module and "include xmpl/session" into base and here and ...
+
+require 'fileutils'
+# require 'ftools'
+require 'xmpl/base'
+require 'xmpl/switch'
+require 'xmpl/request'
+
+session = Example.new('rsfiltool', '1.01', 'PRAGMA POD')
+
+filterprefix = 'rsfil-'
+
+commandline = CommandLine.new
+
+commandline.registerflag('submit')
+commandline.registerflag('fetch')
+commandline.registerflag('report')
+#commandline.registerflag('split')
+commandline.registerflag('stamp')
+commandline.registerflag('silent')
+commandline.registerflag('request')
+commandline.registerflag('nobackup')
+
+commandline.registervalue('filter')
+
+commandline.registervalue('root')
+commandline.registervalue('path')
+commandline.registervalue('name')
+
+commandline.expand
+
+session.set('log.silent',true) if commandline.option('silent')
+
+session.inherit(commandline)
+
+session.identify
+
+# session.exit unless session.loadenvironment
+
+def prepare (session)
+
+    # Normally the system provides the file, but a user can provide the rest; in
+    # order to prevent problems with keying in names, we force lowercase names.
+
+    session.set('option.file',session.get('argument.first')) if session.get('option.file').empty?
+
+    root = session.get('option.root').downcase
+    path = session.get('option.path').downcase
+    name = session.get('option.name').downcase
+    file = session.get('option.file').downcase
+
+    session.error('provide file') if file.empty?
+    session.error('provide root') if root.empty?
+
+    filter = session.get('option.filter').downcase
+    trash  = session.get('option.trash').downcase
+
+    trash = '' unless FileTest.directory?(trash)
+
+    if not filter.empty? then
+        begin
+            require filter
+        rescue Exception
+            begin
+                require filterprefix + filter
+            rescue Exception
+                session.error('invalid filter')
+            end
+        end
+        begin
+            if RSFIL::valid?(file) then
+                split = RSFIL::split(file,name)
+                path = if split[0].downcase then split[0] else '' end
+                file = if split[1].downcase then split[1] else '' end
+                name = if split[2].downcase then split[2] else '' end
+                session.report('split result',split.inspect)
+                session.error('unable to split off path') if path.empty?
+                session.error('unable to split off file') if file.empty?
+                session.error('unable to split off name') if name.empty?
+                session.set('option.path',path) if path
+                session.set('option.file',file) if file
+                session.set('option.name',name) if name
+            else
+                session.error('invalid filename', file)
+                unless trash.empty? then
+                    File.copy(file,trash + '/' + file)
+                end
+            end
+        rescue
+            session.error('unable to split',file,'with filter',filter)
+        end
+    end
+
+    session.error('provide path') if path.empty?
+
+    session.error('invalid root') unless test(?d,root)
+
+    exit if session.error?
+
+    session.set('fb.filename',file)
+
+    path.gsub!(/\\/o, '/')
+    path.gsub!(/\s/o, '')
+
+    path = root + '/' + path
+
+    # multiple paths
+
+    if path =~ /^(.*)\/(.*?)$/o then
+        prepath = $1
+        postpath = $2
+        paths = postpath.split(/\,/)
+        paths.collect! do |p|
+            prepath + '/' + p
+        end
+    else
+        paths = Array.new
+        paths.push(path)
+    end
+
+    paths.collect! do |p|
+        p.gsub(/[^a-zA-Z0-9\s\-\_\/\.\:]/o, '-')
+    end
+
+    file.gsub!(/\\/o, '/')
+    file.gsub!(/[^a-zA-Z0-9\s\-\_\/\.\:]/o, '-')
+
+#    if session.get('option.split')
+#        if file =~ /(.*)\.(.*?)$/o
+#            path = path + '/' + $1
+#        else
+#            session.error('nothing to split in filename')
+#        end
+#    end
+
+    paths.each do |p|
+        begin
+            session.report('creating path', p)
+            File.makedirs(p)
+        rescue
+            session.error('unable to create path', p)
+        end
+    end
+
+    name.gsub!(/\s+/,'')
+
+    # can be a,b,c.exa.saved => a.exa.saved,b.exa.saved,c.exa.saved
+
+    if name =~ /(.*?)\.(.*)$/
+        name = $1
+        suffix = $2
+        names = name.split(/\,/)
+        names.collect! do |n|
+            n + '.' + suffix
+        end
+        name = names.join(',')
+    else
+        names = name.split(/\,/)
+    end
+
+    session.set('fb.path',path)
+    session.set('fb.paths',paths)
+    session.set('fb.name',name)
+    session.set('fb.names',names)
+
+end
+
+def thefullname(path,file,name='')
+
+    filename = file.gsub(/.*?\//, '')
+
+    if name.empty?
+        path + '/' + filename
+    else
+        unless name =~ /\..+$/o  # unless name.match(/\..+$/o)
+            if filename =~ /(\..+)$/o  # if file.match(/(\..+)$/o)
+                name = name + $1
+            end
+        end
+        path + '/' + name
+    end
+
+end
+
+def submitfile (session)
+
+    filename = session.get('fb.filename')
+    paths = session.get('fb.paths')
+    names = session.get('fb.names')
+
+    paths.each do |path|
+        session.report('submitting path',path)
+        names.each do |name|
+            session.report('submitting file',filename,'to',name)
+            submit(session,path,filename,name)
+        end
+    end
+
+end
+
+def submitlist (session)
+
+    requestname = session.get('fb.filename')
+    paths = session.get('fb.paths')
+
+    if test(?e,requestname)
+        session.report('loading request file', requestname)
+        if request = ExaRequest.new(requestname)
+            filelist = request.files
+            if filelist && (filelist.size > 0)
+                filelist.each do |filename|
+                    paths.each do |path|
+                        session.report('submitting file from list', filename)
+                        submit(session,path,filename,request.naturalname(filename))
+                    end
+                end
+            else
+                session.warning('no filelist in', requestname)
+            end
+        else
+            session.warning('unable to load', requestname)
+        end
+    else
+        session.warning('no file', requestname)
+    end
+
+end
+
+def submit (session, path, filename, newname)
+
+    fullname = thefullname(path,newname)
+
+    unless test(?e,filename)
+        session.warning('no file to submit', filename)
+        return
+    end
+
+    begin
+        File.copy(fullname,fullname+'.old') if ! session.get('nobackup') && test(?e,fullname)
+        if test(?e,filename)
+            File.copy(filename,fullname)
+            session.report('submit', filename, 'in', fullname)
+            if session.get('option.stamp')
+                f = open(fullname+'.tim','w')
+                f.puts(Time.now.gmtime.strftime("%a %b %d %H:%M:%S %Y"))
+                f.close
+            end
+        else
+            session.error('unable to locate', filename)
+        end
+    rescue
+        session.error('unable to move', filename, 'to', fullname)
+    end
+
+end
+
+def fetch (session)
+
+    filename = session.get('fb.filename')
+    paths = session.get('fb.paths')
+    name = session.get('fb.name')
+
+    begin
+        File.copy(filename,filename+'.old') if ! session.get('nobackup') && test(?e,filename)
+        paths.each do |path|
+            #  fullname = thefullname(path,request.naturalname(filename))
+            # fullname = thefullname(path,filename)
+            fullname = thefullname(path,name)
+            if test(?e,fullname)
+                File.copy(fullname,filename)
+                session.report('fetch', filename, 'from', fullname)
+                return
+            else
+                session.report('file',fullname, 'is not present')
+            end
+        end
+    rescue
+        session.error('unable to fetch file from path')
+    end
+    session.error('no file',filename, 'fetched') unless test(?e,filename)
+
+end
+
+def report (session)
+
+    filename = session.get('fb.filename')
+    paths = session.get('fb.paths')
+
+    paths.each do |path|
+        fullname = thefullname(path,request.naturalname(filename))
+        if test(?e,fullname)
+            begin
+                session.report('file', fullname)
+                session.report('size', test(?s,fullname))
+                if test(?e,fullname+'.tim')
+                    str = IO.readlines(fullname+'.tim')
+                    # str = IO.read(fullname+'.tim')
+                    session.report('time', str)
+                end
+            rescue
+                session.error('unable to report about', fullname)
+            end
+        end
+    end
+
+end
+
+if session.get('option.submit')
+    prepare(session)
+    if session.get('option.request')
+        submitlist(session)
+    else
+        submitfile(session)
+    end
+elsif session.get('option.fetch')
+    prepare(session)
+    fetch(session)
+elsif session.get('option.report')
+    prepare(session)
+    report(session)
+else
+    session.report('provide action')
+end
diff --git a/scripts/context/ruby/rslibtool.rb b/scripts/context/ruby/rslibtool.rb
new file mode 100644
index 000000000..7ae5efb64
--- /dev/null
+++ b/scripts/context/ruby/rslibtool.rb
@@ -0,0 +1,114 @@
+# program   : rslibtool
+# copyright : PRAGMA Publishing On Demand
+# version   : 1.00 - 2002
+# author    : Hans Hagen
+#
+# project   : eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-pod.com / www.pragma-ade.com
+
+# --add      --base=filename --path=directory pattern
+# --remove   --base=filename --path=directory label
+# --sort     --base=filename --path=directory
+# --purge    --base=filename --path=directory
+# --dummy    --base=filename
+# --namespace
+
+# rewrite
+
+unless defined? ownpath
+    ownpath = $0.sub(/[\\\/]\w*?\.rb/i,'')
+    $: << ownpath
+end
+
+require 'rslb/base'
+require 'xmpl/base'
+require 'xmpl/switch'
+
+session = Example.new('rslbtool', '1.0', 'PRAGMA POD')
+
+session.identify
+
+commandline = CommandLine.new
+
+commandline.registerflag('add')
+commandline.registerflag('remove')
+commandline.registerflag('delete')
+commandline.registerflag('sort')
+commandline.registerflag('purge')
+commandline.registerflag('dummy')
+commandline.registerflag('process')
+commandline.registerflag('namespace')
+
+commandline.registervalue('prefix')
+commandline.registervalue('base')
+commandline.registervalue('path')
+commandline.registervalue('result')
+commandline.registervalue('texexec')
+commandline.registervalue('zipalso')
+
+commandline.expand
+
+session.inherit(commandline)
+
+base = session.get('option.base')
+path = session.get('option.path')
+
+base = 'rslbtool.xml' if base.empty?
+
+# when path is given, assume that arg list is list of
+# suffixes, else assume it is a list of globbed filespec
+
+if path.empty?
+	base += '.xml' unless base =~ /\..+$/
+	list = commandline.arguments
+else
+	Dir.chdir(File.dirname(path))
+	list = Dir.glob("*.{#{commandline.arguments.join(',')}}")
+end
+
+begin
+	reslib = Resource.new(base,session.get('option.namespace'))
+	reslib.load(base)
+rescue
+	session.error('problems with loading base')
+	exit
+end
+
+unless session.get('option.texexec').empty?
+    reslib.set_texexec(session.get('option.texexec'))
+end
+
+if session.get('option.add')
+
+	session.report('adding records', list)
+	reslib.add_figures(list,session.get('option.prefix'))
+
+elsif session.get('option.remove') or session.get('option.delete')
+
+	session.report('removing records')
+	reslib.delete_figures(list)
+
+elsif session.get('option.sort')
+
+	session.report('sorting records')
+	reslib.sort_figures()
+
+elsif session.get('option.purge')
+
+	session.report('purging records')
+	reslib.purge_figures()
+
+elsif session.get('option.dummy')
+
+	session.report('creating dummy records')
+    reslib.create_dummies(session.get('option.process'),session.get('option.result'),session.get('option.zipalso'))
+
+else
+
+	session.warning('provide action')
+
+end
+
+reslib.save(base)
diff --git a/scripts/context/ruby/runtools.rb b/scripts/context/ruby/runtools.rb
new file mode 100644
index 000000000..5565748e2
--- /dev/null
+++ b/scripts/context/ruby/runtools.rb
@@ -0,0 +1,506 @@
+require 'timeout'
+require 'fileutils'
+# require 'ftools'
+require 'rbconfig'
+
+class File
+
+    # we don't want a/b//c
+    #
+    # puts File.join('a','b','c')
+    # puts File.join('/a','b','c')
+    # puts File.join('a:','b','c')
+    # puts File.join('a/','/b/','c')
+    # puts File.join('/a','/b/','c')
+    # puts File.join('//a/','/b/','c')
+
+    def File.join(*list)
+        path, prefix = [*list].flatten.join(File::SEPARATOR), ''
+        path.sub!(/^([\/]+)/) do
+            prefix = $1
+            ''
+        end
+        path.gsub!(/([\\\/]+)/) do
+            File::SEPARATOR
+        end
+        prefix + path
+    end
+
+end
+
+
+class Job
+
+    $ownfile, $ownpath = '', ''
+
+    def Job::set_own_path(file)
+        $ownfile, $ownpath = File.basename(file), File.expand_path(File.dirname(file))
+        $: << $ownpath
+    end
+
+    def Job::ownfile
+        $ownfile
+    end
+
+    def Job::ownpath
+        $ownpath
+    end
+
+end
+
+class Job
+
+    def initialize
+        @startuppath = Dir.getwd
+        @log = Array.new
+        @testmode = false
+        @ownpath = $ownpath
+        @paths = Array.new
+    end
+
+    def exit(showlog=false)
+        Dir.chdir(@startuppath)
+        show_log if showlog
+        Kernel::exit
+    end
+
+    def platform
+        case Config::CONFIG['host_os']
+            when /mswin/ then :windows
+                         else :unix
+        end
+    end
+
+    def path(*components)
+        File.join([*components].flatten)
+    end
+
+    def found(name)
+        FileTest.file?(path(name)) || FileTest.directory?(path(name))
+    end
+
+    def binary(name)
+        if platform == :windows then
+            name.sub(/\.[^\/]+$/o,'') + '.exe'
+        else
+            name
+        end
+    end
+
+    def suffixed(name,suffix)
+        if name =~ /\.[^\/]+$/o then
+            name
+        else
+            name + '.' + suffix
+        end
+    end
+
+    def expanded(*name)
+        File.expand_path(File.join(*name))
+    end
+
+    def argument(n,default=nil)
+        ARGV[n] || default
+    end
+
+    def variable(name,default='')
+        ENV[name] || default
+    end
+
+    def change_dir(*dir)
+        dir, old = expanded(path(*dir)), expanded(Dir.getwd)
+        unless old == dir then
+            begin
+                Dir.chdir(dir)
+            rescue
+                error("unable to change to path #{dir}")
+            else
+                if old == dir then
+                    error("error in changing to path #{dir}")
+                else
+                    message("changed to path #{dir}")
+                end
+            end
+        end
+        # return File.expand_path(Dir.getwd)
+    end
+
+    def delete_dir(*dir)
+        begin
+            dir = path(*dir)
+            pattern = "#{dir}/**/*"
+            puts("analyzing dir #{pattern}")
+            files = Dir.glob(pattern).sort.reverse
+            files.each do |f|
+                begin
+                    # FileTest.file?(f) fails on .whatever files
+                    File.delete(f)
+                rescue
+                    # probably directory
+                else
+                    puts("deleting file #{f}")
+                end
+            end
+            files.each do |f|
+                begin
+                    Dir.rmdir(f)
+                rescue
+                    # strange
+                else
+                    message("deleting path #{f}")
+                end
+            end
+            begin
+                Dir.rmdir(dir)
+            rescue
+                # strange
+            else
+                message("deleting parent #{dir}")
+            end
+            Dir.glob(pattern).sort.each do |f|
+                warning("unable to delete #{f}")
+            end
+        rescue
+            warning("unable to delete path #{File.expand_path(dir)} (#{$!})")
+        else
+            message("path #{File.expand_path(dir)} removed")
+        end
+    end
+
+
+    def create_dir(*dir)
+        begin
+            dir = path(*dir)
+            unless FileTest.directory?(dir) then
+                File.makedirs(dir)
+            else
+                return
+            end
+        rescue
+            error("unable to create path #{File.expand_path(dir)}")
+        else
+            message("path #{File.expand_path(dir)} created")
+        end
+    end
+
+    def show_dir(delay=0)
+        _puts_("\n")
+        print Dir.getwd + ' '
+        begin
+            timeout(delay) do
+                loop do
+                    print '.'
+                    sleep(1)
+                end
+            end
+        rescue TimeoutError
+            # ok
+        end
+        _puts_("\n\n")
+    end
+
+    def copy_file(from,to='.',exclude=[])
+        to, ex = path(to), [exclude].flatten
+        Dir.glob(path(from)).each do |file|
+            tofile = to.sub(/[\.\*]$/o) do File.basename(file) end
+            _do_copy_(file,tofile) unless ex.include?(File.extname(file))
+        end
+    end
+
+    def clone_file(from,to)
+        if from and to then
+            to = File.join(File.basename(from),to) if File.basename(to).empty?
+            _do_copy_(from,to)
+        end
+    end
+
+    def copy_dir(from,to,pattern='*',exclude=[]) # recursive
+        pattern = '*' if ! pattern or pattern.empty?
+        if from and to and File.expand_path(from) != File.expand_path(to) then
+            ex = [exclude].flatten
+            Dir.glob("#{from}/**/#{pattern}").each do |file|
+                unless ex.include?(File.extname(file)) then
+                    _do_copy_(file,File.join(to,file.sub(/^#{from}/, '')))
+                end
+            end
+        end
+    end
+
+    def copy_path(from,to,pattern='*',exclude=[]) # non-recursive
+        pattern = '*' if ! pattern or pattern.empty?
+        if from and to and File.expand_path(from) != File.expand_path(to) then
+            ex = [exclude].flatten
+            Dir.glob("#{from}/#{pattern}").each do |file|
+                unless ex.include?(File.extname(file)) then
+                    _do_copy_(file,File.join(to,file.sub(/^#{from}/, '')))
+                end
+            end
+        end
+    end
+
+    def _do_copy_(file,tofile)
+        if FileTest.file?(file) and File.expand_path(file) != File.expand_path(tofile) then
+            begin
+                create_dir(File.dirname(tofile))
+                File.copy(file,tofile)
+            rescue
+                error("unable to copy #{file} to #{tofile}")
+            else
+                message("file #{file} copied to #{tofile}")
+            end
+        else
+            puts("file #{file} is not copied")
+        end
+    end
+
+    def rename_file(from,to)
+        from, to = path(from), path(to)
+        begin
+            File.move(from,to)
+        rescue
+            error("unable to rename #{from} to #{to}")
+        else
+            message("#{from} renamed to #{to}")
+        end
+    end
+
+    def delete_file(pattern)
+        Dir.glob(path(pattern)).each do |file|
+            _do_delete_(file)
+        end
+    end
+
+    def delete_files(*files)
+        [*files].flatten.each do |file|
+            _do_delete_(file)
+        end
+    end
+
+    def _do_delete_(file)
+        if FileTest.file?(file) then
+            begin
+                File.delete(file)
+            rescue
+                error("unable to delete file #{file}")
+            else
+                message("file #{file} deleted")
+            end
+        else
+            message("no file #{File.expand_path(file)}")
+        end
+    end
+
+    def show_log(filename=nil)
+        if filename then
+            begin
+                if f = File.open(filename,'w') then
+                    @log.each do |line|
+                        f.puts(line)
+                    end
+                    f.close
+                end
+                message("log data written to #{filename}")
+            rescue
+                error("unable to write log to #{filename}")
+            end
+        else
+            @log.each do |line|
+                _puts_(line)
+            end
+        end
+    end
+
+    def _puts_(str)
+        begin
+            STDOUT.puts(    str)
+        rescue
+            STDERR.puts("error while writing '#{str}' to terminal")
+        end
+    end
+
+    def puts(message)
+        @log << message
+        _puts_(message)
+    end
+
+    def error(message)
+        puts("! #{message}")
+        exit
+    end
+
+    def warning(message)
+        puts("- #{message}")
+    end
+
+    def message(message)
+        puts("+ #{message}")
+    end
+
+    def export_variable(variable,value)
+        value = path(value) if value.class == Array
+        ENV[variable] = value
+        message("environment variable #{variable} set to #{value}")
+        return value
+    end
+
+    def execute_command(*command)
+        begin
+            command = [*command].flatten.join(' ')
+            message("running '#{command}'")
+            _puts_("\n")
+            ok = system(command)
+            _puts_("\n")
+            if true then # ok then
+                message("finished '#{command}'")
+            else
+                error("error in running #{command}")
+            end
+        rescue
+            error("unable to run #{command}")
+        end
+    end
+
+    def pipe_command(*command)
+        begin
+            command = [*command].flatten.join(' ')
+            message("running '#{command}'")
+            result = `#{command}`
+            _puts_("\n")
+            _puts_(result)
+            _puts_("\n")
+        rescue
+            error("unable to run #{command}")
+        end
+    end
+
+    def execute_script(script)
+        script = suffixed(script,'rb')
+        script = path(script_path,File.basename(script)) unless found(script)
+        if found(script) then
+            begin
+                message("loading script #{script}")
+                load(script)
+            rescue
+                error("error in loading script #{script} (#{$!})")
+            else
+                message("script #{script} finished")
+            end
+        else
+            warning("no script #{script}")
+        end
+    end
+
+    def execute_binary(*command)
+        command = [*command].flatten.join(' ').split(' ')
+        command[0] = binary(command[0])
+        execute_command(command)
+    end
+
+    def extend_path(pth)
+        export_variable('PATH',"#{path(pth)}#{File::PATH_SEPARATOR}#{ENV['PATH']}")
+    end
+
+    def startup_path
+        @startuppath
+    end
+
+    def current_path
+        Dir.getwd
+    end
+
+    def script_path
+        @ownpath
+    end
+
+    def push_path(newpath)
+        newpath = File.expand_path(newpath)
+        @paths.push(newpath)
+        change_dir(newpath)
+    end
+
+    def pop_path
+        change_dir(if @paths.length > 0 then @paths.pop else @startuppath end)
+    end
+
+    # runner = Runner.new
+    # runner.texmfstart('texexec','--help')
+
+    def texmfstart(name,args,verbose=false)
+        command = ['texmfstart',"#{'--verbose' if verbose}",name,args].flatten.join(' ')
+        system(command)
+    end
+
+end
+
+class Job
+
+    # copied from texmfstart and patched (message/error), different name
+
+    def use_tree(tree)
+        unless tree.empty? then
+            begin
+                setuptex = File.join(tree,'setuptex.tmf')
+                if FileTest.file?(setuptex) then
+                    message("tex tree : #{setuptex}")
+                    ENV['TEXPATH'] = tree.sub(/\/+$/,'') #  + '/'
+                    ENV['TMP'] = ENV['TMP'] || ENV['TEMP'] || ENV['TMPDIR'] || ENV['HOME']
+                    case RUBY_PLATFORM
+                        when /(mswin|bccwin|mingw|cygwin)/i then ENV['TEXOS'] = ENV['TEXOS'] || 'texmf-mswin'
+                        when /(linux)/i                     then ENV['TEXOS'] = ENV['TEXOS'] || 'texmf-linux'
+                        when /(darwin|rhapsody|nextstep)/i  then ENV['TEXOS'] = ENV['TEXOS'] || 'texmf-macosx'
+                    #   when /(netbsd|unix)/i               then # todo
+                        else                                     # todo
+                    end
+                    ENV['TEXMFOS'] = "#{ENV['TEXPATH']}/#{ENV['TEXOS']}"
+                    message("preset   : TEXPATH => #{ENV['TEXPATH']}")
+                    message("preset   : TEXOS   => #{ENV['TEXOS']}")
+                    message("preset   : TEXMFOS => #{ENV['TEXMFOS']}")
+                    message("preset   : TMP => #{ENV['TMP']}")
+                    IO.readlines(File.join(tree,'setuptex.tmf')).each do |line|
+                        case line
+                            when /^[\#\%]/ then
+                                # comment
+                            when /^(.*?)\s+\=\s+(.*)\s*$/ then
+                                k, v = $1, $2
+                                ENV[k] = v.gsub(/\%(.*?)\%/) do
+                                    ENV[$1] || ''
+                                end
+                                message("user set : #{k} => #{ENV[k]}")
+                        end
+                    end
+                else
+                    warning("no setup file '#{setuptex}', tree not initialized") # no error
+                end
+            rescue
+                warning("error in setup: #{$!}")
+            end
+        end
+    end
+
+end
+
+Job::set_own_path($0)
+
+if Job::ownfile == 'runtools.rb' then
+
+    begin
+        script = ARGV.shift
+        if script then
+            script += '.rb' if File.extname(script).empty?
+            fullname = File.expand_path(script)
+            fullname = File.join(Job::ownpath,script) unless FileTest.file?(fullname)
+            if FileTest.file?(fullname) then
+                puts("loading script #{fullname}")
+                Job::set_own_path(fullname)
+                load(fullname)
+            else
+                puts("unknown script #{fullname}")
+            end
+        else
+            puts("provide script name")
+        end
+    rescue
+        puts("fatal error: #{$!}")
+    end
+
+end
diff --git a/scripts/context/ruby/texexec.rb b/scripts/context/ruby/texexec.rb
new file mode 100644
index 000000000..747d76b68
--- /dev/null
+++ b/scripts/context/ruby/texexec.rb
@@ -0,0 +1,792 @@
+#!/usr/bin/env ruby
+#encoding: ASCII-8BIT
+
+banner = ['TeXExec', 'version 6.2.1', '1997-2009', 'PRAGMA ADE/POD']
+
+$: << File.expand_path(File.dirname($0)) ; $: << File.join($:.last,'lib') ; $:.uniq!
+
+require 'fileutils'
+# require 'ftools'     # needed ?
+
+require 'base/switch'
+require 'base/logger'
+require 'base/variables'
+require 'base/system'
+
+require 'base/state' # needed ?
+require 'base/file'  # needed ?
+
+require 'base/tex'
+require 'base/texutil'
+require 'base/kpse'
+
+class Commands
+
+    include CommandBase
+
+    def make
+        if job = TEX.new(logger) then
+            prepare(job)
+            # bonus, overloads language switch !
+            job.setvariable('language','all') if @commandline.option('all')
+            if @commandline.arguments.length > 0 then
+                if @commandline.arguments.first == 'all' then
+                    job.setvariable('texformats',job.defaulttexformats)
+                    job.setvariable('mpsformats',job.defaultmpsformats)
+                else
+                    job.setvariable('texformats',@commandline.arguments)
+                    job.setvariable('mpsformats',@commandline.arguments)
+                end
+            end
+            job.makeformats
+            job.inspect && Kpse.inspect if @commandline.option('verbose')
+            seterror if job.error?
+        end
+    end
+
+    def check
+        if job = TEX.new(logger) then
+            job.checkcontext
+            job.inspect && Kpse.inspect  if @commandline.option('verbose')
+        end
+    end
+
+    def main
+        if @commandline.arguments.length>0 then
+            process
+        else
+            help
+        end
+    end
+
+    def process
+        if job = TEX.new(logger) then
+            job.setvariable('files',@commandline.arguments)
+            prepare(job)
+            job.processtex
+            job.inspect && Kpse.inspect if @commandline.option('verbose')
+            seterror if job.error?
+        end
+    end
+
+    def mptex
+        if job = TEX.new(logger) then
+            job.setvariable('files',@commandline.arguments)
+            prepare(job)
+            job.processmptex
+            job.inspect && Kpse.inspect  if @commandline.option('verbose')
+            seterror if job.error?
+        end
+    end
+
+    def mpxtex
+        if job = TEX.new(logger) then
+            job.setvariable('files',@commandline.arguments)
+            prepare(job)
+            job.processmpxtex
+            job.inspect && Kpse.inspect  if @commandline.option('verbose')
+            seterror if job.error?
+        end
+    end
+
+    def mpgraphic
+        if job = TEX.new(logger) then
+            job.setvariable('files',@commandline.arguments)
+            prepare(job)
+            job.processmpgraphic
+            job.inspect && Kpse.inspect  if @commandline.option('verbose')
+            seterror if job.error?
+        end
+    end
+
+    def mpstatic
+        if job = TEX.new(logger) then
+            job.setvariable('filename',@commandline.arguments.first)
+            prepare(job)
+            job.processmpstatic
+            job.inspect && Kpse.inspect  if @commandline.option('verbose')
+            seterror if job.error?
+        end
+    end
+
+    # hard coded goodies # to be redone as s-ctx-.. with vars passed as such
+
+    def listing
+        if job = TEX.new(logger) then
+            prepare(job)
+            job.cleanuptemprunfiles
+            files =  if @commandline.option('sort') then @commandline.arguments.sort else @commandline.arguments end
+            if files.length > 0 then
+                if f = File.open(job.tempfilename('tex'),'w') then
+                    backspace = @commandline.checkedoption('backspace', '1.5cm')
+                    topspace  = @commandline.checkedoption('topspace', '1.5cm')
+                    pretty    = @commandline.option('pretty')
+                    f << "% interface=english\n"
+                    f << "\\setupbodyfont[11pt,tt]\n"
+                    f << "\\setuplayout\n"
+                    f << "  [topspace=#{topspace},backspace=#{backspace},\n"
+                    f << "   header=0cm,footer=1.5cm,\n"
+                    f << "   width=middle,height=middle]\n"
+                    f << "\\setuptyping[lines=yes]\n"
+                    f << "\\setuptyping[option=color]\n" if pretty
+                    f << "\\starttext\n";
+                    files.each do |filename|
+                        report("list file: #{filename}")
+                        cleanname = cleantexfilename(filename).downcase
+                        f << "\\page\n"
+                        f << "\\setupfootertexts[\\tttf #{cleanname}][\\tttf \\pagenumber]\n"
+                        f << "\\typefile{#{filename}}\n"
+                    end
+                    f << "\\stoptext\n"
+                    f.close
+                    job.setvariable('interface','english')
+                    job.setvariable('simplerun',true)
+                    # job.setvariable('nooptionfile',true)
+                    job.setvariable('files',[job.tempfilename])
+                    job.processtex
+                else
+                    report('no files to list')
+                end
+            else
+                report('no files to list')
+            end
+            job.cleanuptemprunfiles
+        end
+    end
+
+    def figures
+        # we replaced "texutil --figures ..."
+        if job = TEX.new(logger) then
+            prepare(job)
+            job.cleanuptemprunfiles
+            fast = @commandline.option('fast')
+            files =  if @commandline.option('sort') then @commandline.arguments.sort else @commandline.arguments end
+            if fast or (files.length > 0) then
+                if f = File.open(job.tempfilename('tex'),'w') then
+                    files.delete("texexec.pdf")
+                    # Kpse.runscript('rlxtools', ['--identify','--collect'], files.join(' ')) unless fast
+                    system("texmfstart rlxtools --identify --collect #{files.join(' ')}")
+                    figures     = @commandline.checkedoption('method', 'a').downcase
+                    paperoffset = @commandline.checkedoption('paperoffset', '0pt')
+                    backspace   = @commandline.checkedoption('backspace', '1.5cm')
+                    topspace    = @commandline.checkedoption('topspace', '1.5cm')
+                    boxtype     = @commandline.checkedoption('boxtype','')
+                    f << "% format=english\n";
+                    f << "\\usemodule[res-20]\n"
+                    f << "\\setuplayout\n";
+                    f << "  [topspace=#{topspace},backspace=#{backspace},\n"
+                    f << "   header=1.5cm,footer=0pt,\n";
+                    f << "   width=middle,height=middle]\n";
+                    if @commandline.option('fullscreen') then
+                        f << "\\setupinteraction\n";
+                        f << "  [state=start]\n";
+                        f << "\\setupinteractionscreen\n";
+                        f << "  [option=max]\n";
+                    end
+                    boxtype += "box" unless boxtype.empty? || (boxtype =~ /box$/io)
+                    f << "\\starttext\n";
+                    f << "\\showexternalfigures[alternative=#{figures},offset=#{paperoffset},size=#{boxtype}]\n";
+                    f << "\\stoptext\n";
+                    f.close
+                    job.setvariable('interface','english')
+                    job.setvariable('simplerun',true)
+                    job.setvariable('files',[job.tempfilename])
+                    job.processtex
+                    # File.silentdelete('rlxtools.rli') unless job.getvariable('keep')
+                else
+                    report('no figures to show')
+                end
+            else
+                report('no figures to show')
+            end
+            job.cleanuptemprunfiles
+        end
+    end
+
+    def modules
+        if job = TEX.new(logger) then
+            prepare(job)
+            job.cleanuptemprunfiles
+            files =  if @commandline.option('sort') then @commandline.arguments.sort else @commandline.arguments end
+            msuffixes = ['tex','mkii','mkiv','mp','pl','pm','rb']
+            if files.length > 0 then
+                files.each do |fname|
+                    fnames = Array.new
+                    if FileTest.file?(fname) then
+                        fnames << fname
+                    else
+                        msuffixes.each do |fsuffix|
+                            fnames << File.suffixed(fname,fsuffix)
+                        end
+                    end
+                    fnames.each do |ffname|
+                        if msuffixes.include?(File.splitname(ffname)[1]) && FileTest.file?(ffname) then
+                            if mod = File.open(job.tempfilename('tex'),'w') then
+                                if File.suffix(ffname) =~ /^(mkii|mkiv)$/o then
+                                    markfile = $1
+                                else
+                                    markfile = nil
+                                end
+                                # Kpse.runscript('ctxtools',['--document'],ffname)
+                                system("texmfstart ctxtools --document #{ffname}")
+                                if ted = File.silentopen(File.suffixed(ffname,'ted')) then
+                                    firstline = ted.gets
+                                    if firstline =~ /interface=/o then
+                                        mod << firstline
+                                    else
+                                        mod << "% interface=en\n"
+                                    end
+                                    ted.close
+                                else
+                                    mod << "% interface=en\n"
+                                end
+                                mod << "\\usemodule[mod-01]\n"
+                                mod << "\\def\\ModuleNumber{1}\n"
+                                mod << "\\starttext\n"
+                                # todo: global file too
+                                mod << "\\readlocfile{#{File.suffixed(ffname,'ted')}}{}{}\n"
+                                mod << "\\stoptext\n"
+                                mod.close
+                                job.setvariable('interface','english') # redundant
+                                # job.setvariable('simplerun',true)
+                                # job.setvariable('nooptionfile',true)
+                                job.setvariable('files',[job.tempfilename])
+                                result = File.unsuffixed(File.basename(ffname))
+                                if markfile then
+                                    result = result+'-'+markfile
+                                end
+                                job.setvariable('result',result)
+                                job.processtex
+                                # ["dvi", "pdf","ps"].each do |s|
+                                    # File.silentrename(job.tempfilename(s),File.suffixed(ffname,s));
+                                # end
+                            end
+                        end
+                    end
+                end
+            else
+                report('no modules to process')
+            end
+            job.cleanuptemprunfiles
+        end
+    end
+
+    def pdfsplit
+        if job = TEX.new(logger) then
+            prepare(job)
+            job.cleanuptemprunfiles
+            filename = File.expand_path(@commandline.arguments.first)
+            if FileTest.file?(filename) then
+                basename = filename.sub(/\..*?$/,'')
+                tempfile = File.suffixed(job.tempfilename,'tex')
+                if basename != filename then
+                    info = `pdfinfo #{filename}`
+                    if info =~ /Pages:\s*(\d+)/ then
+                        nofpages = $1.to_i
+                        result = @commandline.checkedoption('result','texexec')
+                        nofpages.times do |i|
+                            if f = File.open(tempfile,"w") then
+                                n = i + 1
+                                report("extracting page #{n}")
+                                f << "\\starttext\\startTEXpage\n"
+                                f << "\\externalfigure[#{filename}][object=no,page=#{n}]\n"
+                                f << "\\stopTEXpage\\stoptext\n"
+                                f.close
+                                job.setvariable('result',"#{result}-#{n}")
+                                job.setvariable('interface','english') # redundant
+                                job.setvariable('simplerun',true)
+                                job.setvariable('purge',true)
+                                job.setvariable('files',[tempfile])
+                                job.processtex
+                            end
+                        end
+                    end
+                end
+            end
+            job.cleanuptemprunfiles
+        end
+    end
+
+    def arrangeoutput
+        if job = TEX.new(logger) then
+            prepare(job)
+            job.cleanuptemprunfiles
+            files =  if @commandline.option('sort') then @commandline.arguments.sort else @commandline.arguments end
+            if files.length > 0 then
+                if f = File.open(job.tempfilename('tex'),'w') then
+                    emptypages  = @commandline.checkedoption('addempty', '')
+                    paperoffset = @commandline.checkedoption('paperoffset', '0cm')
+                    textwidth   = @commandline.checkedoption('textwidth', '0cm')
+                    backspace   = @commandline.checkedoption('backspace', '0cm')
+                    topspace    = @commandline.checkedoption('topspace', '0cm')
+                    f << "\\definepapersize\n"
+                    f << "  [offset=#{paperoffset}]\n"
+                    f << "\\setuplayout\n"
+                    f << "  [backspace=#{backspace},\n"
+                    f << "    topspace=#{topspace},\n"
+                    f << "     marking=on,\n" if @commandline.option('marking')
+                    f << "       width=middle,\n"
+                    f << "      height=middle,\n"
+                    f << "    location=middle,\n"
+                    f << "      header=0pt,\n"
+                    f << "      footer=0pt]\n"
+                    unless @commandline.option('noduplex') then
+                        f << "\\setuppagenumbering\n"
+                        f << "  [alternative=doublesided]\n"
+                    end
+                    f << "\\starttext\n"
+                    files.each do |filename|
+                        report("arranging file #{filename}")
+                        f << "\\insertpages\n"
+                        f << "  [#{filename}]\n"
+                        f << "  [#{emptypages}]\n" unless emptypages.empty?
+                        f << "  [width=#{textwidth}]\n"
+                    end
+                    f << "\\stoptext\n"
+                    f.close
+                    job.setvariable('interface','english')
+                    job.setvariable('simplerun',true)
+                    job.setvariable('arrange',true)
+                    # job.setvariable('nooptionfile',true)
+                    job.setvariable('files',[job.tempfilename])
+                    job.processtex
+                else
+                    report('no files to arrange')
+                end
+            else
+                report('no files to arrange')
+            end
+            job.cleanuptemprunfiles
+        end
+    end
+
+    def selectoutput
+        if job = TEX.new(logger) then
+            prepare(job)
+            job.cleanuptemprunfiles
+            files =  if @commandline.option('sort') then @commandline.arguments.sort else @commandline.arguments end
+            if files.length > 0 then
+                if f = File.open(job.tempfilename('tex'),'w') then
+                    selection   = @commandline.checkedoption('selection', '')
+                    paperoffset = @commandline.checkedoption('paperoffset', '0cm')
+                    textwidth   = @commandline.checkedoption('textwidth', '0cm')
+                    backspace   = @commandline.checkedoption('backspace', '0cm')
+                    topspace    = @commandline.checkedoption('topspace', '0cm')
+                    paperformat = @commandline.checkedoption('paperformat', 'A4*A4').split(/[\*x]/o)
+                    from, to = paperformat[0] || 'A4', paperformat[1] || paperformat[0] || 'A4'
+                    if from == 'fit' or to == 'fit' then
+                        f << "\\getfiguredimensions[#{files.first}]\n"
+                        if from == 'fit' then
+                            f << "\\expanded{\\definepapersize[from-fit][width=\\figurewidth,height=\\figureheight]}\n"
+                            from = 'from-fit'
+                        end
+                        if to == 'fit' then
+                            f << "\\expanded{\\definepapersize[to-fit][width=\\figurewidth,height=\\figureheight]}\n"
+                            to = 'to-fit'
+                        end
+                    end
+                    job.setvariable('paperformat','') # else overloaded later on
+                    f << "\\setuppapersize[#{from}][#{to}]\n"
+                    f << "\\definepapersize\n";
+                    f << "  [offset=#{paperoffset}]\n";
+                    f << "\\setuplayout\n";
+                    f << "  [backspace=#{backspace},\n";
+                    f << "    topspace=#{topspace},\n";
+                    f << "     marking=on,\n" if @commandline.option('marking')
+                    f << "       width=middle,\n";
+                    f << "      height=middle,\n";
+                    f << "    location=middle,\n";
+                    f << "      header=0pt,\n";
+                    f << "      footer=0pt]\n";
+                    f << "\\setupexternalfigures\n";
+                    f << "  [directory=]\n";
+                    f << "\\starttext\n";
+                    unless selection.empty? then
+                        f << "\\filterpages\n"
+                        f << "  [#{files.first}][#{selection}][width=#{textwidth}]\n"
+                    end
+                    f << "\\stoptext\n"
+                    f.close
+                    job.setvariable('interface','english')
+                    job.setvariable('simplerun',true)
+                    # job.setvariable('nooptionfile',true)
+                    job.setvariable('files',[job.tempfilename])
+                    job.processtex
+                else
+                    report('no files to selectt')
+                end
+            else
+                report('no files to select')
+            end
+            job.cleanuptemprunfiles
+        end
+    end
+
+    def copyoutput
+        copyortrim(false,'copy')
+    end
+
+    def trimoutput
+        copyortrim(true,'trim')
+    end
+
+    def copyortrim(trim=false,what='unknown')
+        if job = TEX.new(logger) then
+            prepare(job)
+            job.cleanuptemprunfiles
+            files =  if @commandline.option('sort') then @commandline.arguments.sort else @commandline.arguments end
+            if files.length > 0 then
+                if f = File.open(job.tempfilename('tex'),'w') then
+                    scale = @commandline.checkedoption('scale')
+                    begin
+                        scale = (scale.to_f * 1000.0).to_i if scale.to_i < 10
+                    rescue
+                        scale = 1000
+                    end
+                    scale = scale.to_i
+                    paperoffset = @commandline.checkedoption('paperoffset', '0cm')
+                    f << "\\starttext\n"
+                    files.each do |filename|
+                        result = @commandline.checkedoption('result','texexec')
+                        begin
+                            if (filename !~ /^texexec/io) && (filename !~ /^#{result}/) then
+                                report("copying file: #{filename}")
+                                f <<  "\\getfiguredimensions\n"
+                                f <<  "  [#{filename}]\n"
+                                f <<  "  [scale=#{scale},\n"
+                                f <<  "   page=1,\n"
+                                f <<  "   size=trimbox\n" if trim
+                                f <<  "]\n"
+                                f <<  "\\definepapersize\n"
+                                f <<  "  [copy]\n"
+                                f <<  "  [width=\\figurewidth,\n"
+                                f <<  "   height=\\figureheight]\n"
+                                f <<  "\\setuppapersize\n"
+                                f <<  "  [copy][copy]\n"
+                                f <<  "\\setuplayout\n"
+                                f <<  "  [page]\n"
+                                f <<  "\\setupexternalfigures\n"
+                                f <<  "  [directory=]\n"
+                                f <<  "\\copypages\n"
+                                f <<  "  [#{filename}]\n"
+                                f <<  "  [scale=#{scale},\n"
+                                f <<  "   marking=on,\n" if @commandline.option('markings')
+                                f <<  "   size=trimbox,\n" if trim
+                                f <<  "   offset=#{paperoffset}]\n"
+                            end
+                        rescue
+                            report("wrong specification")
+                        end
+                    end
+                    f << "\\stoptext\n"
+                    f.close
+                    job.setvariable('interface','english')
+                    job.setvariable('simplerun',true)
+                    # job.setvariable('nooptionfile',true)
+                    job.setvariable('files',[job.tempfilename])
+                    job.processtex
+                else
+                    report("no files to #{what}")
+                end
+            else
+                report("no files to #{what}")
+            end
+            job.cleanuptemprunfiles
+        end
+    end
+
+    # todo: make this styles
+
+    def combineoutput
+        if job = TEX.new(logger) then
+            prepare(job)
+            job.cleanuptemprunfiles
+            files =  if @commandline.option('sort') then @commandline.arguments.sort else @commandline.arguments end
+            if files.length > 0 then
+                if f = File.open(job.tempfilename('tex'),'w') then
+                    paperoffset = @commandline.checkedoption('paperoffset', '0cm')
+                    combination = @commandline.checkedoption('combination','2*2').split(/[\*x]/o)
+                    paperformat = @commandline.checkedoption('paperformat', 'A4*A4').split(/[\*x]/o)
+                    bannerheight = @commandline.checkedoption('bannerheight', '')
+                    nx, ny = combination[0] || '2', combination[1] || combination[0] || '2'
+                    from, to = paperformat[0] || 'A4', paperformat[1] || paperformat[0] || 'A4'
+                    f << "\\setuppapersize[#{from}][#{to}]\n"
+                    f << "\\setuplayout\n"
+                    f << "  [topspace=#{paperoffset},backspace=#{paperoffset},\n"
+                    f << "   header=0pt,footer=0pt,\n"
+                    f << "   width=middle,height=middle]\n"
+                    if bannerheight.empty? then
+                        f << "\\setuplayout[footer=1cm]\n"
+                    else
+                        f << "\\definelayer[page][width=\\paperwidth,height=\\paperheight]\n"
+                        f << "\\setupbackgrounds[page][background=page]\n"
+                    end
+                    if @commandline.option('nobanner') then
+                        f << "\\setuplayout[footer=0cm]\n"
+                        f << "\\setupbackgrounds[page][background=]\n"
+                    end
+                    f << "\\setupexternalfigures\n"
+                    f << "  [directory=]\n"
+                    f << "\\starttext\n"
+                    files.each do |filename|
+                        result = @commandline.checkedoption('result','texexec')
+                        if (filename !~ /^texexec/io) && (filename !~ /^#{result}/) then
+                            report("combination file: #{filename}")
+                            cleanname = cleantexfilename(filename).downcase
+                            bannerstring = "\\tttf #{cleanname}\\quad\\quad\\currentdate\\quad\\quad\\pagenumber"
+                            if bannerheight.empty? then
+                                f << "\\setupfootertexts\n"
+                                f << "  [#{bannerstring}]\n"
+                            else
+                                # for the moment we lack a better hook
+                                f << "\\setuptexttexts\n"
+                                f << "  [{\\setlayerframed[page][preset=middlebottom][frame=off,height=#{bannerheight}]{#{bannerstring}}}]\n"
+                            end
+                            f << "\\combinepages[#{filename}][nx=#{nx},ny=#{ny}]\n"
+                            f << "\\page\n"
+                        end
+                    end
+                    f << "\\stoptext\n"
+                    f.close
+                    job.setvariable('interface','english')
+                    job.setvariable('simplerun',true)
+                    # job.setvariable('nooptionfile',true)
+                    job.setvariable('files',[job.tempfilename])
+                    job.processtex
+                else
+                    report('no files to list')
+                end
+            else
+                report('no files to list')
+            end
+            job.cleanuptemprunfiles
+        end
+    end
+
+    private
+
+    def prepare(job)
+
+        job.booleanvars.each do |k|
+            job.setvariable(k,@commandline.option(k))
+        end
+        job.stringvars.each do |k|
+            job.setvariable(k,@commandline.option(k)) unless @commandline.option(k).empty?
+        end
+        job.standardvars.each do |k|
+            job.setvariable(k,@commandline.option(k)) unless @commandline.option(k).empty?
+        end
+        job.knownvars.each do |k|
+            job.setvariable(k,@commandline.option(k)) unless @commandline.option(k).empty?
+        end
+
+job.setvariable('given.backend',job.getvariable('backend'))
+
+        if (str = @commandline.option('engine')) && ! str.standard? && ! str.empty? then
+            job.setvariable('texengine',str)
+        elsif @commandline.oneof('luatex') then
+            job.setvariable('texengine','luatex')
+        elsif @commandline.oneof('pdfetex','pdftex','pdf') then
+            job.setvariable('texengine','pdftex')
+        elsif @commandline.oneof('xetex','xtx') then
+            job.setvariable('texengine','xetex')
+        elsif @commandline.oneof('aleph') then
+            job.setvariable('texengine','aleph')
+        elsif @commandline.oneof('petex') then
+            job.setvariable('texengine','petex')
+        else
+            job.setvariable('texengine','standard')
+        end
+
+        if (str = @commandline.option('backend')) && ! str.standard? && ! str.empty? then
+            job.setvariable('backend',str)
+        elsif @commandline.oneof('pdfetex','pdftex','pdf','luatex') then
+            job.setvariable('backend','pdftex')
+        elsif @commandline.oneof('dvipdfmx','dvipdfm','dpx','dpm') then
+            job.setvariable('backend','dvipdfmx')
+        elsif @commandline.oneof('xetex','xtx') then
+            job.setvariable('backend','xetex')
+        elsif @commandline.oneof('aleph') then
+            job.setvariable('backend','dvipdfmx')
+        elsif @commandline.oneof('petex') then
+            job.setvariable('backend','dvipdfmx')
+        elsif @commandline.oneof('dvips','ps') then
+            job.setvariable('backend','dvips')
+        elsif @commandline.oneof('xdv') then
+            job.setvariable('backend','xdv')
+        else
+            case job.getvariable('texengine')
+                when 'pdfetex'  then job.setvariable('backend','pdftex')
+                when 'pdftex'   then job.setvariable('backend','pdftex')
+                when 'luatex'   then job.setvariable('backend','pdftex')
+                when 'xetex'    then job.setvariable('backend','xetex')
+                when 'petex'    then job.setvariable('backend','dvipdfmx')
+                when 'aleph'    then job.setvariable('backend','dvipdfmx')
+            else
+                job.setvariable('backend','standard')
+            end
+        end
+
+        if (str = @commandline.option('engine')) && ! str.standard? && ! str.empty? then
+            job.setvariable('mpsengine',@commandline.option('engine'))
+        else
+            job.setvariable('mpsengine','standard')
+        end
+
+    end
+
+    def cleantexfilename(filename)
+        filename.gsub(/([\$\_\#])/) do "\\$1" end.gsub(/([\~])/) do "\\string$1" end
+    end
+
+end
+
+# we will make this pluggable, i.e. load plugins from base/tex that
+# extend the class and may even add switches
+#
+# commandline.load_plugins('base/tex')
+#
+# maybe it's too slow so for a while keep the --pdf* in here
+
+logger      = Logger.new(banner.shift)
+commandline = CommandLine.new
+
+commandline.registeraction('make',      'make formats')
+commandline.registeraction('check',     'check versions')
+commandline.registeraction('process',   'process file')
+commandline.registeraction('mptex',     'process mp file')
+commandline.registeraction('mpxtex',    'process mpx file')
+commandline.registeraction('mpgraphic', 'process mp file to stand-alone graphics')
+commandline.registeraction('mpstatic',  'process mp/ctx file to stand-alone graphics')
+
+commandline.registeraction('listing',    'list of file content')
+commandline.registeraction('figures',    'generate overview of figures')
+commandline.registeraction('modules',    'generate module documentation')
+commandline.registeraction('pdfarrange', 'impose pages (booklets)')
+commandline.registeraction('pdfselect',  'select pages from file(s)')
+commandline.registeraction('pdfcopy',    'copy pages from file(s)')
+commandline.registeraction('pdftrim',    'trim pages from file(s)')
+commandline.registeraction('pdfcombine', 'combine multiple pages')
+commandline.registeraction('pdfsplit',   'split file in pages')
+
+# compatibility switch
+
+class Commands
+
+    include CommandBase
+
+    alias pdfarrange :arrangeoutput
+    alias pdfselect  :selectoutput
+    alias pdfcopy    :copyoutput
+    alias pdftrim    :trimoutput
+    alias pdfcombine :combineoutput
+
+end
+
+# so far for compatibility, will move to tex
+
+@@extrastringvars = [
+    'pages', 'background', 'backspace', 'topspace', 'boxtype', 'tempdir','bannerheight',
+    'printformat', 'method', 'scale', 'selection',
+    'combination', 'textwidth', 'addempty', 'logfile',
+    'startline', 'endline', 'startcolumn', 'endcolumn', 'scale'
+]
+
+@@extrabooleanvars = [
+    'centerpage', 'noduplex', 'color', 'pretty',
+    'fullscreen', 'screensaver', 'markings'
+]
+
+if job = TEX.new(logger) then
+
+    job.setextrastringvars(@@extrastringvars)
+    job.setextrabooleanvars(@@extrabooleanvars)
+
+    job.booleanvars.each do |k|
+        commandline.registerflag(k)
+    end
+    job.stringvars.each do |k|
+        commandline.registervalue(k,'')
+    end
+    job.standardvars.each do |k|
+        commandline.registervalue(k,'standard')
+    end
+    job.knownvars.each do |k|
+        commandline.registervalue(k,'')
+    end
+
+end
+
+class Commands
+
+    alias saved_help help
+
+    def wrap_help(title, vars)
+        report("")
+        report(title)
+        report("")
+        r, n = '', 0
+        vars.sort.each do |s|
+            if n == 5 then
+                report(r)
+                r, n = '', 1
+            else
+                n += 1
+            end
+            r << '  ' + s
+        end
+        report(r) unless r.empty?
+    end
+
+    def help
+        saved_help
+        if @commandline.option('all') then
+            if job = TEX.new(logger) then
+                wrap_help("boolean switches:", job.allbooleanvars)
+                wrap_help("string switches:", job.allstringvars)
+            end
+        else
+            report('')
+            report('--help --all   shows all switches')
+        end
+    end
+
+end
+
+# todo: register flags -> first one true
+
+commandline.registerflag('pdf')
+commandline.registerflag('pdftex')
+commandline.registerflag('pdfetex')
+commandline.registerflag('luatex')
+
+commandline.registerflag('dvipdfmx')
+commandline.registerflag('dvipdfm')
+commandline.registerflag('dpx')
+commandline.registerflag('dpm')
+
+commandline.registerflag('dvips')
+commandline.registerflag('ps')
+
+commandline.registerflag('xetex')
+commandline.registerflag('xtx')
+commandline.registerflag('xdv')
+
+commandline.registerflag('aleph')
+
+commandline.registerflag('petex')
+
+commandline.registerflag('all')
+commandline.registerflag('fast')
+commandline.registerflag('sort')
+
+# generic
+
+commandline.registeraction('help')
+commandline.registeraction('version')
+
+commandline.registerflag('verbose')
+
+commandline.expand
+
+Commands.new(commandline,logger,banner).execute(commandline.action || 'main') # or just execute()
diff --git a/scripts/context/ruby/texmfstart.rb b/scripts/context/ruby/texmfstart.rb
new file mode 100644
index 000000000..97087c3ae
--- /dev/null
+++ b/scripts/context/ruby/texmfstart.rb
@@ -0,0 +1,1277 @@
+#!/usr/bin/env ruby
+#encoding: ASCII-8BIT
+
+# We have removed the fast, server and client variants and no longer
+# provide the distributed 'serve trees' option. After all, we're now
+# using luatex.
+
+# program   : texmfstart
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 1.9.0 - 2003/2006
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-pod.com / www.pragma-ade.com
+
+# no special requirements, i.e. no exa modules/classes used
+
+# texmfstart [switches] filename [optional arguments]
+#
+# ruby2exe texmfstart --help -> avoids stub test
+#
+# Of couse I can make this into a nice class, which i'll undoubtely will
+# do when I feel the need. In that case it will be part of a bigger game.
+
+# --locate        => provides location
+# --exec          => exec instead of system
+# --iftouched=a,b => only if timestamp a<>b
+# --ifchanged=a,b => only if checksum changed
+#
+# file: path: bin:
+
+# texmfstart --exec bin:scite *.tex
+
+# we don't depend on other libs
+
+$: << File.expand_path(File.dirname($0)) ; $: << File.join($:.last,'lib') ; $:.uniq!
+
+require "rbconfig"
+require "fileutils"
+
+require "digest/md5"
+
+# kpse_merge_start
+
+class File
+    def File::makedirs(*x)
+        FileUtils.makedirs(x)
+    end
+end
+
+class String
+
+    def split_path
+        if self =~ /\;/o || self =~ /^[a-z]\:/io then
+            self.split(";")
+        else
+            self.split(":")
+        end
+    end
+
+end
+
+class Array
+
+    def join_path
+        self.join(File::PATH_SEPARATOR)
+    end
+
+end
+
+class File
+
+    def File.locate_file(path,name)
+        begin
+            files = Dir.entries(path)
+            if files.include?(name) then
+                fullname = File.join(path,name)
+                return fullname if FileTest.file?(fullname)
+            end
+            files.each do |p|
+                fullname = File.join(path,p)
+                if p != '.' and p != '..' and FileTest.directory?(fullname) and result = locate_file(fullname,name) then
+                    return result
+                end
+            end
+        rescue
+            # bad path
+        end
+        return nil
+    end
+
+    def File.glob_file(pattern)
+        return Dir.glob(pattern).first
+    end
+
+end
+
+# kpse_merge_file: 't:/ruby/base/kpsedirect.rb'
+
+class KpseDirect
+
+    attr_accessor :progname, :format, :engine
+
+    def initialize
+        @progname, @format, @engine = '', '', ''
+    end
+
+    def expand_path(str)
+        clean_name(`kpsewhich -expand-path=#{str}`.chomp)
+    end
+
+    def expand_var(str)
+        clean_name(`kpsewhich -expand-var=#{str}`.chomp)
+    end
+
+    def find_file(str)
+        clean_name(`kpsewhich #{_progname_} #{_format_} #{str}`.chomp)
+    end
+
+    def _progname_
+        if @progname.empty? then '' else "-progname=#{@progname}" end
+    end
+    def _format_
+        if @format.empty?   then '' else "-format=\"#{@format}\"" end
+    end
+
+    private
+
+    def clean_name(str)
+        str.gsub(/\\/,'/')
+    end
+
+end
+
+# kpse_merge_stop
+
+$mswindows = Config::CONFIG['host_os'] =~ /mswin/
+$separator = File::PATH_SEPARATOR
+$version   = "2.1.0"
+$ownpath   = File.dirname($0)
+
+if $mswindows then
+    require "win32ole"
+    require "Win32API"
+end
+
+# exit if defined?(REQUIRE2LIB)
+
+$stdout.sync = true
+$stderr.sync = true
+
+$applications = Hash.new
+$suffixinputs = Hash.new
+$predefined   = Hash.new
+$runners      = Hash.new
+
+$suffixinputs['pl']  = 'PERLINPUTS'
+$suffixinputs['rb']  = 'RUBYINPUTS'
+$suffixinputs['py']  = 'PYTHONINPUTS'
+$suffixinputs['lua'] = 'LUAINPUTS'
+$suffixinputs['jar'] = 'JAVAINPUTS'
+$suffixinputs['pdf'] = 'PDFINPUTS'
+
+$predefined['texexec']  = 'texexec.rb'
+$predefined['texutil']  = 'texutil.rb'
+$predefined['texfont']  = 'texfont.pl'
+$predefined['texshow']  = 'texshow.pl'
+
+$predefined['makempy']  = 'makempy.pl'
+$predefined['mptopdf']  = 'mptopdf.pl'
+$predefined['pstopdf']  = 'pstopdf.rb'
+
+$predefined['examplex'] = 'examplex.rb'
+$predefined['concheck'] = 'concheck.rb'
+
+$predefined['runtools'] = 'runtools.rb'
+$predefined['textools'] = 'textools.rb'
+$predefined['tmftools'] = 'tmftools.rb'
+$predefined['ctxtools'] = 'ctxtools.rb'
+$predefined['rlxtools'] = 'rlxtools.rb'
+$predefined['pdftools'] = 'pdftools.rb'
+$predefined['mpstools'] = 'mpstools.rb'
+# $predefined['exatools'] = 'exatools.rb'
+$predefined['xmltools'] = 'xmltools.rb'
+# $predefined['luatools'] = 'luatools.lua'
+# $predefined['mtxtools'] = 'mtxtools.rb'
+
+$predefined['newpstopdf']   = 'pstopdf.rb'
+$predefined['newtexexec']   = 'texexec.rb'
+$predefined['pdftrimwhite'] = 'pdftrimwhite.pl'
+
+$makelist = [
+    # context
+    'texexec',
+    'texutil',
+    'texfont',
+    # mp/ps
+    'pstopdf',
+    'mptopdf',
+    'makempy',
+    # misc
+    'ctxtools',
+    'pdftools',
+    'xmltools',
+    'textools',
+    'mpstools',
+    'tmftools',
+    'exatools',
+    'runtools',
+    'rlxtools',
+    'pdftrimwhite',
+    'texfind',
+    'texshow'
+    #
+    # no 'luatools',
+    # no 'mtxtools',
+    # no, 'texmfstart'
+]
+
+$scriptlist   = 'rb|pl|py|lua|jar'
+$documentlist = 'pdf|ps|eps|htm|html'
+
+$editor       = ENV['TEXMFSTART_EDITOR'] || ENV['EDITOR'] || ENV['editor'] || 'scite'
+
+$crossover    = true # to other tex tools, else only local
+$kpse         = nil
+
+def set_applications(page=1)
+
+    $applications['unknown']  = ''
+    $applications['ruby']     = $applications['rb']  = 'ruby'
+    $applications['lua']      = $applications['lua'] = 'lua'
+    $applications['perl']     = $applications['pl']  = 'perl'
+    $applications['python']   = $applications['py']  = 'python'
+    $applications['java']     = $applications['jar'] = 'java'
+
+    if $mswindows then
+        $applications['pdf']  = ['',"pdfopen --page #{page} --file",'acroread']
+        $applications['html'] = ['','netscape','mozilla','opera','iexplore']
+        $applications['ps']   = ['','gview32','gv','gswin32','gs']
+    else
+        $applications['pdf']  = ["pdfopen --page #{page} --file",'acroread']
+        $applications['html'] = ['netscape','mozilla','opera']
+        $applications['ps']   = ['gview','gv','gs']
+    end
+
+    $applications['htm']      = $applications['html']
+    $applications['eps']      = $applications['ps']
+
+    $runners['lua']           = "texlua"
+
+end
+
+set_applications()
+
+def check_kpse
+    if $kpse then
+        # already done
+    else
+        $kpse = KpseDirect.new
+    end
+end
+
+if $mswindows then
+
+    GetShortPathName = Win32API.new('kernel32', 'GetShortPathName', ['P','P','N'], 'N')
+    GetLongPathName  = Win32API.new('kernel32', 'GetLongPathName',  ['P','P','N'], 'N')
+
+    def dowith_pathname (filename,filemethod)
+        filename = filename.gsub(/\\/o,'/') # no gsub! because filename can be frozen
+        case filename
+            when /\;/o then
+                # could be a path spec
+                return filename
+            when /\s+/o then
+                # danger lurking
+                buffer = ' ' * 260
+                length = filemethod.call(filename,buffer,buffer.size)
+                if length>0 then
+                    return buffer.slice(0..length-1)
+                else
+                    # when the path or file does not exist, nothing is returned
+                    # so we try to handle the path separately from the basename
+                    basename = File.basename(filename)
+                    pathname = File.dirname(filename)
+                    length = filemethod.call(pathname,buffer,260)
+                    if length>0 then
+                        return buffer.slice(0..length-1) + '/' + basename
+                    else
+                        return filename
+                    end
+                end
+            else
+                # no danger
+                return filename
+        end
+    end
+
+    def longpathname (filename)
+        dowith_pathname(filename,GetLongPathName)
+    end
+
+    def shortpathname (filename)
+        dowith_pathname(filename,GetShortPathName)
+    end
+
+else
+
+    def longpathname (filename)
+        filename
+    end
+
+    def shortpathname (filename)
+        filename
+    end
+
+end
+
+class File
+
+    @@update_eps = 1
+
+    def File.needsupdate(oldname,newname)
+        begin
+            oldtime = File.stat(oldname).mtime.to_i
+            newtime = File.stat(newname).mtime.to_i
+            if newtime >= oldtime then
+                return false
+            elsif oldtime-newtime < @@update_eps then
+                return false
+            else
+                return true
+            end
+        rescue
+            return true
+        end
+    end
+
+    def File.syncmtimes(oldname,newname)
+        return
+        begin
+            if $mswindows then
+                # does not work (yet) / gives future timestamp
+                # t = File.mtime(oldname) # i'm not sure if the time is frozen, so we do it here
+                # File.utime(0,t,oldname,newname)
+            else
+                t = File.mtime(oldname) # i'm not sure if the time is frozen, so we do it here
+                File.utime(0,t,oldname,newname)
+            end
+        rescue
+        end
+    end
+
+    def File.timestamp(name)
+        begin
+            "#{File.stat(name).mtime}"
+        rescue
+            return 'unknown'
+        end
+    end
+
+end
+
+def hashed (arr=[])
+    arg = if arr.class == String then arr.split(' ') else arr.dup end
+    hsh = Hash.new
+    if arg.length > 0
+        hsh['arguments'] = ''
+        done = false
+        arg.each do |s|
+            if done then
+                if s =~ /\s/ then
+                    kvl = s.split('=')
+                    if kvl[1] and kvl[1] !~ /^[\"\']/ then
+                        hsh['arguments'] += ' ' + kvl[0] + "=" + '"' + kvl[1] + '"'
+                    elsif s =~ /\s/ then
+                        hsh['arguments'] += ' "' + s + '"'
+                    else
+                        hsh['arguments'] += ' ' + s
+                    end
+                else
+                    hsh['arguments'] += ' ' + s
+                end
+            else
+                kvl = s.split('=')
+                if kvl[0].sub!(/^\-+/,'') then
+                    hsh[kvl[0]] = if kvl.length > 1 then kvl[1] else true end
+                else
+                    hsh['file'] = s
+                    done = true
+                end
+            end
+        end
+    end
+    return hsh
+end
+
+
+def launch(filename)
+    if $browser && $mswindows then
+        filename = filename.gsub(/\.[\/\\]/) do
+            Dir.getwd + '/'
+        end
+        report("launching #{filename}")
+        ie = WIN32OLE.new("InternetExplorer.Application")
+        ie.visible = true
+        ie.navigate(filename)
+        return true
+    else
+        return false
+    end
+end
+
+# env|environment
+# rel|relative
+# loc|locate|kpse|path|file
+
+def quoted(str)
+    if str =~ /^\"/ then
+        return str
+    elsif str =~ / / then
+        return "\"#{str}\""
+    else
+        return str
+    end
+end
+
+def expanded(arg) # no "other text files", too restricted
+    arg.gsub(/(env|environment)\:([a-zA-Z\-\_\.0-9]+)/o) do
+        method, original, resolved = $1, $2, ''
+        if resolved = ENV[original] then
+            report("environment variable #{original} expands to #{resolved}") unless $report
+            quoted(resolved)
+        else
+            report("environment variable #{original} cannot be resolved") unless $report
+            quoted(original)
+        end
+    end . gsub(/(rel|relative)\:([a-zA-Z\-\_\.0-9]+)/o) do
+        method, original, resolved = $1, $2, ''
+        ['.','..','../..'].each do |r|
+            if FileTest.file?(File.join(r,original)) then
+                resolved = File.join(r,original)
+                break
+            end
+        end
+        if resolved.empty? then
+            quoted(original)
+        else
+            quoted(resolved)
+        end
+    end . gsub(/(kpse|loc|locate|file|path)\:([a-zA-Z\-\_\.0-9]+)/o) do
+        method, original, resolved = $1, $2, ''
+        if $program && ! $program.empty? then
+            # pstrings = ["-progname=#{$program}"]
+            pstrings = [$program]
+        else
+            # pstrings = ['','-progname=context']
+            pstrings = ['','context']
+        end
+        # auto suffix with texinputs as fall back
+        if ENV["_CTX_K_V_#{original}_"] then
+            resolved = ENV["_CTX_K_V_#{original}_"]
+            report("environment provides #{original} as #{resolved}") unless $report
+            quoted(resolved)
+        else
+            check_kpse
+            pstrings.each do |pstr|
+                if resolved.empty? then
+                    # command = "kpsewhich #{pstr} #{original}"
+                    # report("running #{command}")
+                    report("locating '#{original}' in program space '#{pstr}'")
+                    begin
+                        # resolved = `#{command}`.chomp
+                        $kpse.progname = pstr
+                        $kpse.format = ''
+                        resolved = $kpse.find_file(original).gsub(/\\/,'/')
+                    rescue
+                        resolved = ''
+                    end
+                end
+                # elsewhere in the tree
+                if resolved.empty? then
+                    # command = "kpsewhich #{pstr} -format=\"other text files\" #{original}"
+                    # report("running #{command}")
+                    report("locating '#{original}' in program space '#{pstr}' using format 'other text files'")
+                    begin
+                        # resolved = `#{command}`.chomp
+                        $kpse.progname = pstr
+                        $kpse.format = 'other text files'
+                        resolved = $kpse.find_file(original).gsub(/\\/,'/')
+                    rescue
+                        resolved = ''
+                    end
+                end
+            end
+            if resolved.empty? then
+                original = File.dirname(original) if method =~ /path/
+                report("#{original} is not resolved") unless $report
+                ENV["_CTX_K_V_#{original}_"] = original if $crossover
+                quoted(original)
+            else
+                resolved = File.dirname(resolved) if method =~ /path/
+                report("#{original} is resolved to #{resolved}") unless $report
+                ENV["_CTX_K_V_#{original}_"] = resolved if $crossover
+                quoted(resolved)
+            end
+        end
+    end
+end
+
+def changeddir?(path)
+    if path.empty? then
+        return true
+    else
+        oldpath = File.expand_path(path)
+        begin
+            Dir.chdir(path) if not path.empty?
+        rescue
+            report("unable to change to directory: #{path}")
+        else
+            report("changed to directory: #{path}")
+        end
+        newpath = File.expand_path(Dir.getwd)
+        return oldpath == newpath
+    end
+end
+
+def runcommand(command)
+    if $locate then
+        command = command.split(' ').collect do |c|
+            if c =~ /\//o then
+                begin
+                    cc = File.expand_path(c)
+                    c = cc if FileTest.file?(cc)
+                rescue
+                end
+            end
+            c
+        end . join(' ')
+        print command # to stdout and no newline
+    elsif $execute then
+        report("using 'exec' instead of 'system' call: #{command}")
+        exec(command) if changeddir?($path)
+    else
+        report("using 'system' call: #{command}")
+        system(command) if changeddir?($path)
+    end
+end
+
+def join_command(args)
+    args[0] = $runners[args[0]] || args[0]
+    [args].join(' ')
+end
+
+def runoneof(application,fullname,browserpermitted)
+    if browserpermitted && launch(fullname) then
+        return true
+    else
+        fullname = quoted(fullname) # added because MM ran into problems
+        report("starting #{$filename}") unless $report
+        output("\n") if $report && $verbose
+        applications = $applications[application.downcase]
+        if ! applications then
+            output("problems with determining application type")
+            return true
+        elsif applications.class == Array then
+            if $report then
+                output(join_command([fullname,expanded($arguments)]))
+                return true
+            else
+                applications.each do |a|
+                    return true if runcommand(join_command([a,fullname,expanded($arguments)]))
+                end
+            end
+        elsif applications.empty? then
+            if $report then
+                output(join_command([fullname,expanded($arguments)]))
+                return true
+            else
+                return runcommand(join_command([fullname,expanded($arguments)]))
+            end
+        else
+            if $report then
+                output(join_command([applications,fullname,expanded($arguments)]))
+                return true
+            else
+                return runcommand(join_command([applications,fullname,expanded($arguments)]))
+            end
+        end
+        return false
+    end
+end
+
+def report(str)
+    $stdout.puts(str) if $verbose
+end
+
+def output(str)
+    $stdout.puts(str)
+end
+
+def usage
+    print "version  : #{$version} - 2003/2006 - www.pragma-ade.com\n"
+    print("\n")
+    print("usage    : texmfstart [switches] filename [optional arguments]\n")
+    print("\n")
+    print("switches : --verbose --report --browser --direct --execute --locate --iftouched --ifchanged\n")
+    print("           --program --file --page --arguments --batch --edit --report --clear\n")
+    print("           --make --lmake --wmake --path --stubpath --indirect --before --after\n")
+    print("           --tree --autotree --environment --showenv\n")
+    print("\n")
+    print("example  : texmfstart pstopdf.rb cow.eps\n")
+    print("           texmfstart --locate examplex.rb\n")
+    print("           texmfstart --execute examplex.rb\n")
+    print("           texmfstart --browser examplap.pdf\n")
+    print("           texmfstart showcase.pdf\n")
+    print("           texmfstart --page=2 --file=showcase.pdf\n")
+    print("           texmfstart --program=yourtex yourscript.rb arg-1 arg-2\n")
+    print("           texmfstart --direct xsltproc kpse:somefile.xsl somefile.xml\n")
+    print("           texmfstart --direct ruby rel:wn-cleanup-1.rb oldfile.xml newfile.xml\n")
+    print("           texmfstart bin:xsltproc env:somepreset path:somefile.xsl somefile.xml\n")
+    print("           texmfstart --iftouched=normal,lowres downsample.rb normal lowres\n")
+    print("           texmfstart --ifchanged=somefile.dat --direct processit somefile.dat\n")
+    print("           texmfstart bin:scite kpse:texmf.cnf\n")
+    print("           texmfstart --exec bin:scite *.tex\n")
+    print("           texmfstart --edit texmf.cnf\n")
+    print("           texmfstart --edit kpse:texmf.cnf\n")
+    print("           texmfstart --serve\n")
+    print("\n")
+    print("           texmfstart --stubpath=/usr/local/bin [--make --remove] --verbose all\n")
+    print("           texmfstart --stubpath=auto [--make --remove] all\n")
+    print("\n")
+    check_kpse
+end
+
+# somehow registration does not work out (at least not under windows)
+# the . is also not accepted by unix as seperator
+
+def tag(name)
+    if $crossover then "_CTX_K_S_#{name}_" else "TEXMFSTART.#{name}" end
+end
+
+def registered?(filename)
+    return ENV[tag(filename)] != nil
+end
+
+def registered(filename)
+    return ENV[tag(filename)] || 'unknown'
+end
+
+def register(filename,fullname)
+    if fullname && ! fullname.empty? then # && FileTest.file?(fullname)
+        ENV[tag(filename)] = fullname
+        report("registering '#{filename}' as '#{fullname}'")
+        return true
+    else
+        return false
+    end
+end
+
+def find(filename,program)
+    begin
+        filename = filename.sub(/script:/o, '') # so we have bin: and script: and nothing
+        if $predefined.key?(filename) then
+            report("expanding '#{filename}' to '#{$predefined[filename]}'")
+            filename = $predefined[filename]
+        end
+        if registered?(filename) then
+            report("already located '#{filename}'")
+            return registered(filename)
+        end
+        # create suffix list
+        if filename =~ /^(.*)\.(.+)$/ then
+            filename = $1
+            suffixlist = [$2]
+        else
+            suffixlist = [$scriptlist.split('|'),$documentlist.split('|')].flatten
+        end
+        # first we honor a given path
+        if filename =~ /[\\\/]/ then
+            report("trying to honor '#{filename}'")
+            suffixlist.each do |suffix|
+                fullname = filename+'.'+suffix
+                if FileTest.file?(fullname) && register(filename,fullname)
+                    return shortpathname(fullname)
+                end
+            end
+        end
+        filename.sub!(/^.*[\\\/]/, '')
+        # next we look at the current path and the callerpath
+        pathlist = [ ]
+        progpath = $applications[suffixlist[0]]
+        threadok = registered("THREAD") !~ /unknown/
+        pathlist << ['.','current']
+        pathlist << [$ownpath,'caller']                                 if $ownpath != '.'
+        pathlist << ["#{$ownpath}/../#{progpath}",'caller']             if progpath
+        pathlist << [registered("THREAD"),'thread']                     if threadok
+        pathlist << ["#{registered("THREAD")}/../#{progpath}",'thread'] if progpath && threadok
+        pathlist.each do |p|
+            if p && ! p.empty? && ! (p[0] == 'unknown') then
+                suffixlist.each do |suffix|
+                    fname = "#{filename}.#{suffix}"
+                    fullname = File.expand_path(File.join(p[0],fname))
+                    report("locating '#{fname}' in #{p[1]} path '#{p[0]}'")
+                    if FileTest.file?(fullname) && register(filename,fullname) then
+                        report("'#{fname}' located in #{p[1]} path")
+                        return shortpathname(fullname)
+                    end
+                end
+            end
+        end
+        # now we consult environment settings
+        fullname = nil
+        check_kpse
+        $kpse.progname = program
+        suffixlist.each do |suffix|
+            begin
+                break unless $suffixinputs[suffix]
+                environment = ENV[$suffixinputs[suffix]] || ENV[$suffixinputs[suffix]+".#{$program}"]
+                if ! environment || environment.empty? then
+                    begin
+                        # environment = `kpsewhich -expand-path=\$#{$suffixinputs[suffix]}`.chomp
+                        environment = $kpse.expand_path("\$#{$suffixinputs[suffix]}")
+                    rescue
+                        environment = nil
+                    else
+                        if environment && ! environment.empty? then
+                            report("using kpsewhich variable #{$suffixinputs[suffix]}")
+                        end
+                    end
+                elsif environment && ! environment.empty? then
+                    report("using environment variable #{$suffixinputs[suffix]}")
+                end
+                if environment && ! environment.empty? then
+                    environment.split($separator).each do |e|
+                        e.strip!
+                        e = '.' if e == '\.' # somehow . gets escaped
+                        e += '/' unless e =~ /[\\\/]$/
+                        fullname = e + filename + '.' + suffix
+                        report("testing '#{fullname}'")
+                        if FileTest.file?(fullname) then
+                            break
+                        else
+                            fullname = nil
+                        end
+                    end
+                end
+            rescue
+                report("environment string '#{$suffixinputs[suffix]}' cannot be used to locate '#{filename}'")
+                fullname = nil
+            else
+                return shortpathname(fullname) if register(filename,fullname)
+            end
+        end
+        return shortpathname(fullname) if register(filename,fullname)
+        # then we fall back on kpsewhich
+        suffixlist.each do |suffix|
+            # TDS script scripts location as per 2004
+            if suffix =~ /(#{$scriptlist})/ then
+                begin
+                    report("using 'kpsewhich' to locate '#{filename}' in suffix space '#{suffix}' (1)")
+                    # fullname = `kpsewhich -progname=#{program} -format=texmfscripts #{filename}.#{suffix}`.chomp
+                    $kpse.format = 'texmfscripts'
+                    fullname = $kpse.find_file("#{filename}.#{suffix}").gsub(/\\/,'/')
+                rescue
+                    report("kpsewhich cannot locate '#{filename}' in suffix space '#{suffix}' (1)")
+                    fullname = nil
+                else
+                    return shortpathname(fullname) if register(filename,fullname)
+                end
+            end
+            # old TDS location: .../texmf/context/...
+            begin
+                report("using 'kpsewhich' to locate '#{filename}' in suffix space '#{suffix}' (2)")
+                # fullname = `kpsewhich -progname=#{program} -format="other text files" #{filename}.#{suffix}`.chomp
+                $kpse.format = 'other text files'
+                fullname = $kpse.find_file("#{filename}.#{suffix}").gsub(/\\/,'/')
+            rescue
+                report("kpsewhich cannot locate '#{filename}' in suffix space '#{suffix}' (2)")
+                fullname = nil
+            else
+                return shortpathname(fullname) if register(filename,fullname)
+            end
+        end
+        return shortpathname(fullname) if register(filename,fullname)
+        # let's take a look at the path
+        paths = ENV['PATH'].split($separator)
+        suffixlist.each do |s|
+            paths.each do |p|
+                suffixedname = "#{filename}.#{s}"
+                report("checking #{p} for #{filename}")
+                if FileTest.file?(File.join(p,suffixedname)) then
+                    fullname = File.join(p,suffixedname)
+                    return  shortpathname(fullname) if register(filename,fullname)
+                end
+            end
+        end
+        # bad luck, we need to search the tree ourselves
+        if (suffixlist.length == 1) && (suffixlist.first =~ /(#{$documentlist})/) then
+            report("aggressively locating '#{filename}' in document trees")
+            begin
+                # texroot = `kpsewhich -expand-var=$SELFAUTOPARENT`.chomp
+                texroot = $kpse.expand_var("$SELFAUTOPARENT")
+            rescue
+                texroot = ''
+            else
+                texroot.sub!(/[\\\/][^\\\/]*?$/, '')
+            end
+            if not texroot.empty? then
+                sffxlst = suffixlist.join(',')
+                begin
+                    report("locating '#{filename}' in document tree '#{texroot}/doc*'")
+                    if (result = Dir.glob("#{texroot}/doc*/**/#{filename}.{#{sffxlst}}")) && result && result[0] && FileTest.file?(result[0]) then
+                        fullname = result[0]
+                    end
+                rescue
+                    report("locating '#{filename}.#{suffixlist.join('|')}' in tree '#{texroot}' aborted")
+                end
+            end
+            return shortpathname(fullname) if register(filename,fullname)
+        end
+        report("aggressively locating '#{filename}' in tex trees")
+        begin
+            # textrees = `kpsewhich -expand-var=$TEXMF`.chomp
+            textrees = $kpse.expand_var("$TEXMF")
+        rescue
+            textrees = ''
+        end
+        if not textrees.empty? then
+            textrees.gsub!(/[\{\}\!]/, '')
+            textrees = textrees.split(',')
+            if (suffixlist.length == 1) && (suffixlist.first =~ /(#{$documentlist})/) then
+                speedup = ['doc**','**']
+            else
+                speedup = ['**']
+            end
+            sffxlst = suffixlist.join(',')
+            speedup.each do |speed|
+                textrees.each do |tt|
+                    tt.gsub!(/[\\\/]$/, '')
+                    if FileTest.directory?(tt) then
+                        begin
+                            report("locating '#{filename}' in tree '#{tt}/#{speed}/#{filename}.{#{sffxlst}}'")
+                            if (result = Dir.glob("#{tt}/#{speed}/#{filename}.{#{sffxlst}}")) && result && result[0] && FileTest.file?(result[0]) then
+                                fullname = result[0]
+                                break
+                            end
+                        rescue
+                            report("locating '#{filename}' in tree '#{tt}' aborted")
+                            next
+                        end
+                    end
+                end
+                break if fullname && ! fullname.empty?
+            end
+        end
+        if register(filename,fullname) then
+            return shortpathname(fullname)
+        else
+            return ''
+        end
+    rescue
+        error, trace = $!, $@.join("\n")
+        report("fatal error: #{error}\n#{trace}")
+        # report("fatal error")
+    end
+end
+
+def run(fullname)
+    if ! fullname || fullname.empty? then
+        output("the file '#{$filename}' is not found")
+    elsif FileTest.file?(fullname) then
+        begin
+            case fullname
+                when /\.(#{$scriptlist})$/i then
+                    return runoneof($1,fullname,false)
+                when /\.(#{$documentlist})$/i then
+                    return runoneof($1,fullname,true)
+                else
+                    return runoneof('unknown',fullname,false)
+            end
+        rescue
+            report("starting '#{$filename}' in program space '#{$program}' fails (#{$!})")
+        end
+    else
+        report("the file '#{$filename}' in program space '#{$program}' is not accessible")
+    end
+    return false
+end
+
+def direct(fullname)
+    begin
+        return runcommand([fullname.sub(/^(bin|binary)\:/, ''),expanded($arguments)].join(' '))
+    rescue
+        return false
+    end
+end
+
+def edit(filename)
+    begin
+        return runcommand([$editor,expanded(filename),expanded($arguments)].join(' '))
+    rescue
+        return false
+    end
+end
+
+def make(filename,windows=false,linux=false,remove=false)
+    basename = File.basename(filename).gsub(/\.[^.]+?$/, '')
+    if $stubpath == 'auto' then
+        basename = File.dirname($0) + '/' + basename
+    else
+        basename = $stubpath + '/' + basename unless $stubpath.empty?
+    end
+    if filename == 'texmfstart' then
+        program = 'ruby'
+        command = 'kpsewhich --format=texmfscripts --progname=context texmfstart.rb'
+        filename = `#{command}`.chomp.gsub(/\\/, '/')
+        if filename.empty? then
+            report("failure: #{command}")
+            return
+        elsif not remove then
+            if windows then
+                ['bat','cmd','exe'].each do |suffix|
+                    if FileTest.file?("#{basename}.#{suffix}") then
+                        report("windows stub '#{basename}.#{suffix}' skipped (already present)")
+                        return
+                    end
+                end
+            elsif linux && FileTest.file?(basename) then
+                report("unix stub '#{basename}' skipped (already present)")
+                return
+            end
+        end
+    else
+        program = nil
+        if filename =~ /[\\\/]/ && filename =~ /\.(#{$scriptlist})$/ then
+            program = $applications[$1]
+        end
+        filename = "\"#{filename}\"" if filename =~ /\s/
+        program = 'texmfstart' if $indirect || ! program || program.empty?
+    end
+    begin
+        callname = $predefined[filename.sub(/\.*?$/,'')] || filename
+        if remove then
+            if windows && (File.delete(basename+'.bat') rescue false) then
+                report("windows stub '#{basename}.bat' removed (calls #{callname})")
+            elsif linux && (File.delete(basename) rescue false) then
+                report("unix stub '#{basename}' removed (calls #{callname})")
+            end
+        else
+            if windows && f = open(basename+'.bat','w') then
+                f.binmode
+                f.write("@echo off\015\012")
+                f.write("#{program} #{callname} %*\015\012")
+                f.close
+                report("windows stub '#{basename}.bat' made (calls #{callname})")
+            elsif linux && f = open(basename,'w') then
+                f.binmode
+                f.write("#!/bin/sh\012")
+                f.write("#{program} #{callname} \"$@\"\012")
+                f.close
+                report("unix stub '#{basename}' made (calls #{callname})")
+            end
+        end
+    rescue
+        report("failed to make stub '#{basename}' #{$!}")
+        return false
+    else
+        return true
+    end
+end
+
+def process(&block)
+    if $iftouched then
+        files = $directives['iftouched'].split(',')
+        oldname, newname = files[0], files[1]
+        if oldname && newname && File.needsupdate(oldname,newname) then
+            report("file #{oldname}: #{File.timestamp(oldname)}")
+            report("file #{newname}: #{File.timestamp(newname)}")
+            report("file is touched, processing started")
+            yield
+            File.syncmtimes(oldname,newname)
+        else
+            report("file #{oldname} is untouched")
+        end
+    elsif $ifchanged then
+        filename = $directives['ifchanged']
+        checkname = filename + ".md5"
+        oldchecksum, newchecksum = "old", "new"
+        begin
+            newchecksum = Digest::MD5.hexdigest(IO.read(filename)).upcase
+        rescue
+            newchecksum = "new"
+        else
+            begin
+                oldchecksum = IO.read(checkname).chomp
+            rescue
+                oldchecksum = "old"
+            end
+        end
+        if $verbose then
+            report("old checksum #{filename}: #{oldchecksum}")
+            report("new checksum #{filename}: #{newchecksum}")
+        end
+        if oldchecksum != newchecksum then
+            report("file is changed, processing started")
+            begin
+                File.open(checkname,'w') do |f|
+                    f << newchecksum
+                end
+            rescue
+            end
+            yield
+        else
+            report("file #{filename} is unchanged")
+        end
+    else
+        yield
+    end
+end
+
+def checkenvironment(tree)
+    report('')
+    ENV['TMP'] = ENV['TMP'] || ENV['TEMP'] || ENV['TMPDIR'] || ENV['HOME']
+    case RUBY_PLATFORM
+        when /(mswin|bccwin|mingw|cygwin)/i then ENV['TEXOS'] = ENV['TEXOS'] || 'texmf-mswin'
+        when /(linux)/i                     then ENV['TEXOS'] = ENV['TEXOS'] || 'texmf-linux'
+        when /(darwin|rhapsody|nextstep)/i  then ENV['TEXOS'] = ENV['TEXOS'] || 'texmf-macosx'
+    #   when /(netbsd|unix)/i               then # todo
+        else                                     # todo
+    end
+    ENV['TEXOS']   = "#{ENV['TEXOS'].sub(/^[\\\/]*/, '').sub(/[\\\/]*$/, '')}"
+    ENV['TEXPATH'] = tree.sub(/\/+$/,'') # + '/'
+    ENV['TEXMFOS'] = "#{ENV['TEXPATH']}/#{ENV['TEXOS']}"
+    report('')
+    report("preset : TEXPATH => #{ENV['TEXPATH']}")
+    report("preset : TEXOS   => #{ENV['TEXOS']}")
+    report("preset : TEXMFOS => #{ENV['TEXMFOS']}")
+    report("preset : TMP => #{ENV['TMP']}")
+    report('')
+end
+
+def loadfile(filename)
+    begin
+        IO.readlines(filename).each do |line|
+            case line.chomp
+                when /^[\#\%]/ then
+                    # comment
+                when /^(.*?)\s*(\>|\=|\<)\s*(.*)\s*$/ then
+                    # = assign | > prepend | < append
+                    key, how, value = $1, $2, $3
+                    begin
+                        # $SAFE = 0
+                        value.gsub!(/\%(.*?)\%/) do
+                            ENV[$1] || ''
+                        end
+                        # value.gsub!(/\;/,$separator) if key =~ /PATH/i then
+                        case how
+                            when '=', '<<' then ENV[key] = value
+                            when '?', '??' then ENV[key] = ENV[key] || value
+                            when '<', '+=' then ENV[key] = (ENV[key] || '') + $separator + value
+                            when '>', '=+' then ENV[key] = value + $separator + (ENV[key] ||'')
+                        end
+                    rescue
+                        report("user set failed : #{key} (#{$!})")
+                    else
+                        report("user set : #{key} => #{ENV[key]}")
+                    end
+            end
+        end
+    rescue
+        report("error in reading file '#{filename}'")
+    end
+end
+
+def loadtree(tree)
+    begin
+        unless tree.empty? then
+            if File.directory?(tree) then
+                setuptex = File.join(tree,'setuptex.tmf')
+            else
+                setuptex = tree.dup
+            end
+            if FileTest.file?(setuptex) then
+                report("tex tree definition: #{setuptex}")
+                checkenvironment(File.dirname(setuptex))
+                loadfile(setuptex)
+            else
+                report("no setup file '#{setuptex}'")
+            end
+        end
+    rescue
+        # maybe tree is empty or boolean (no arg given)
+    end
+end
+
+def loadenvironment(environment)
+    begin
+        unless environment.empty? then
+            filename = if $path.empty? then environment else File.expand_path(File.join($path,environment)) end
+            if FileTest.file?(filename) then
+                report("environment : #{environment}")
+                loadfile(filename)
+            else
+                report("no environment file '#{environment}'")
+            end
+        end
+    rescue
+        report("problem while loading '#{environment}'")
+    end
+end
+
+def show_environment
+    if $showenv then
+        keys = ENV.keys.sort
+        size = 0
+        keys.each do |k|
+            size = k.size if k.size > size
+        end
+        report('')
+        keys.each do |k|
+            report("#{k.rjust(size)} => #{ENV[k]}")
+        end
+        report('')
+    end
+end
+
+def execute(arguments) # br global
+
+    arguments = arguments.split(/\s+/) if arguments.class == String
+    $directives = hashed(arguments)
+
+    $help        = $directives['help']        || false
+    $batch       = $directives['batch']       || false
+    $filename    = $directives['file']        || ''
+    $program     = $directives['program']     || 'context'
+    $direct      = $directives['direct']      || false
+    $edit        = $directives['edit']        || false
+    $page        = $directives['page']        || 1
+    $browser     = $directives['browser']     || false
+    $report      = $directives['report']      || false
+    $verbose     = $directives['verbose']     || false
+    $arguments   = $directives['arguments']   || ''
+    $execute     = $directives['execute']     || $directives['exec'] || false
+    $locate      = $directives['locate']      || false
+
+    $autotree    = if $directives['autotree'] then (ENV['TEXMFSTART_TREE'] || ENV['TEXMFSTARTTREE'] || '') else '' end
+
+    $path        = $directives['path']        || ''
+    $tree        = $directives['tree']        || $autotree || ''
+    $environment = $directives['environment'] || ''
+
+    $make        = $directives['make']        || false
+    $remove      = $directives['remove']      || $directives['delete'] || false
+    $unix        = $directives['unix']        || false
+    $windows     = $directives['windows']     || $directives['mswin'] || false
+    $stubpath    = $directives['stubpath']    || ''
+    $indirect    = $directives['indirect']    || false
+
+    $before      = $directives['before']      || ''
+    $after       = $directives['after']       || ''
+
+    $iftouched   = $directives['iftouched']   || false
+    $ifchanged   = $directives['ifchanged']   || false
+
+    $openoffice  = $directives['oo']          || false
+
+    $crossover   = false if $directives['clear']
+
+    $showenv     = $directives['showenv']     || false
+    $verbose     = true if $showenv
+
+    $serve       = $directives['serve']       || false
+
+    $verbose = true if (ENV['_CTX_VERBOSE_'] =~ /(y|yes|t|true|on)/io) && ! $locate && ! $report
+
+    set_applications($page)
+
+    # private:
+
+    $selfmerge   = $directives['selfmerge'] || false
+    $selfcleanup = $directives['selfclean'] || $directives['selfcleanup'] || false
+
+    ENV['_CTX_VERBOSE_'] = 'yes' if $verbose
+
+    if $openoffice then
+        if ENV['OOPATH'] then
+            if FileTest.directory?(ENV['OOPATH']) then
+                report("using open office python")
+                if $mswindows then
+                    $applications['python'] = $applications['py']  = "\"#{File.join(ENV['OOPATH'],'program','python.bat')}\""
+                else
+                    $applications['python'] = $applications['py']  = File.join(ENV['OOPATH'],'python')
+                end
+                report("python path #{$applications['python']}")
+            else
+                report("environment variable 'OOPATH' does not exist")
+            end
+        else
+            report("environment variable 'OOPATH' is not set")
+        end
+    end
+
+    if $selfmerge then
+        output("ruby libraries are cleaned up") if SelfMerge::cleanup
+        output("ruby libraries are merged")     if SelfMerge::merge
+        return true
+    elsif $selfcleanup then
+        output("ruby libraries are cleaned up") if SelfMerge::cleanup
+        return true
+    elsif $help || ! $filename || $filename.empty? then
+        usage
+        loadtree($tree)
+        loadenvironment($environment)
+        show_environment()
+        return true
+    elsif $batch && $filename && ! $filename.empty? then
+        # todo, take commands from file and avoid multiple starts and checks
+        return false
+    else
+        report("texmfstart version #{$version}")
+        loadtree($tree)
+        loadenvironment($environment)
+        show_environment()
+        if $make || $remove then
+            if $filename == 'all' then
+                makelist = $makelist
+            else
+                makelist = [$filename]
+            end
+            makelist.each do |filename|
+                if $windows then
+                    make(filename,true,false,$remove)
+                elsif $unix then
+                    make(filename,false,true,$remove)
+                else
+                    make(filename,$mswindows,!$mswindows,$remove)
+                end
+            end
+            return true # guess
+        elsif $browser && $filename =~ /^http\:\/\// then
+            return launch($filename)
+        else
+            begin
+                process do
+                    if $direct || $filename =~ /^bin\:/ then
+                        return direct($filename)
+                    elsif $edit && ! $editor.empty? then
+                        return edit($filename)
+                    else # script: or no prefix
+                        command = find(shortpathname($filename),$program)
+                        if command then
+                            register("THREAD",File.dirname(File.expand_path(command)))
+                            return run(command)
+                        else
+                            report('unable to locate program')
+                            return false
+                        end
+                    end
+                end
+            rescue
+                report('fatal error in starting process')
+                return false
+            end
+        end
+    end
+
+end
+
+if execute(ARGV) then
+    report("\nexecution was successful") if $verbose
+    exit(0)
+else
+    report("\nexecution failed") if $verbose
+    exit(1)
+end
diff --git a/scripts/context/ruby/texsync.rb b/scripts/context/ruby/texsync.rb
new file mode 100644
index 000000000..004610399
--- /dev/null
+++ b/scripts/context/ruby/texsync.rb
@@ -0,0 +1,206 @@
+#!/usr/bin/env ruby
+
+# program   : texsync
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2003-2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+# For the moment this script only handles the 'minimal' context
+# distribution. In due time I will add a few more options, like
+# synchronization of the iso image.
+
+# taco's sync: rsync -au -v rsync://www.pragma-ade.com/all ./htdocs
+
+banner = ['TeXSync', 'version 1.1.1', '2002/2004', 'PRAGMA ADE/POD']
+
+$: << File.expand_path(File.dirname($0)) ; $: << File.join($:.last,'lib') ; $:.uniq!
+
+require 'base/switch'
+require 'base/logger'
+# require 'base/tool'
+
+require 'rbconfig'
+
+class Commands
+
+    include CommandBase
+
+    @@formats = ['en','nl','de','cz','it','ro', 'fr']
+    @@always  = ['metafun','mptopdf','en','nl']
+    @@rsync   = 'rsync -r -z -c --progress --stats  "--exclude=*.fmt" "--exclude=*.efmt" "--exclude=*.mem"'
+
+    @@kpsewhich  = Hash.new
+
+    @@kpsewhich['minimal']       = 'SELFAUTOPARENT'
+    @@kpsewhich['context']       = 'TEXMFLOCAL'
+    @@kpsewhich['documentation'] = 'TEXMFLOCAL'
+    @@kpsewhich['unknown']       = 'SELFAUTOPARENT'
+
+    def update
+
+        report
+
+        return unless destination = getdestination
+
+        texpaths = gettexpaths
+        address  = option('address')
+        user     = option('user')
+        tree     = option('tree')
+        force    = option('force')
+
+        ok = true
+        begin
+            report("synchronizing '#{tree}' from '#{address}' to '#{destination}'")
+            report
+            if texpaths then
+                texpaths.each do |path|
+                    report("synchronizing path '#{path}' of '#{tree}' from '#{address}' to '#{destination}'")
+                    command = "#{rsync} #{user}@#{address}::#{tree}/#{path} #{destination}/{path}"
+                    ok = ok && system(command) if force
+                end
+            else
+                command = "#{@@rsync} #{user}@#{address}::#{tree} #{destination}"
+                ok = system(command) if force
+            end
+        rescue
+            report("error in running rsync")
+            ok = false
+        ensure
+            if force then
+                if ok then
+                    if option('make') then
+                        report("generating tex and metapost formats")
+                        report
+                        @@formats.delete_if do |f|
+                            begin
+                                `kpsewhich cont-#{f}`.chomp.empty?
+                            rescue
+                            end
+                        end
+                        str = [@@formats,@@always].flatten.uniq.join(' ')
+                        begin
+                            system("texexec --make --alone #{str}")
+                        rescue
+                            report("unable to generate formats '#{str}'")
+                        else
+                            report
+                        end
+                    else
+                        report("regenerate the formats files if needed")
+                    end
+                else
+                    report("error in synchronizing '#{tree}'")
+                end
+            else
+                report("provide --force to execute '#{command}'") unless force
+            end
+        end
+
+    end
+
+    def list
+
+        report
+
+        address = option('address')
+        user    = option('user')
+        result  = nil
+
+        begin
+            report("fetching list of trees from '#{address}'")
+            command = "#{@@rsync} #{user}@#{address}::"
+            if option('force') then
+                result = `#{command}`.chomp
+            else
+                report("provide --force to execute '#{command}'")
+            end
+        rescue
+            result = nil
+        else
+            if result then
+                report("available trees:")
+                report
+                reportlines(result)
+            end
+        ensure
+            report("unable to fetch list") unless result
+        end
+
+    end
+
+    private
+
+    def gettexpaths
+        if option('full') then
+            texpaths = ['texmf','texmf-local','texmf-fonts','texmf-mswin','texmf-linux','texmf-macos']
+        elsif option('terse') then
+            texpaths = ['texmf','texmf-local','texmf-fonts']
+            case Config::CONFIG['host_os'] # or: Tool.ruby_platform
+                when /mswin/  then texpaths.push('texmf-mswin')
+                when /linux/  then texpaths.push('texmf-linux')
+                when /darwin/ then texpaths.push('texmf-macosx')
+            end
+        else
+            texpaths = nil
+        end
+        texpaths
+    end
+
+    def getdestination
+       if (destination = option('destination')) && ! destination.empty? then
+            begin
+                if @@kpsewhich.key?(destination) then
+                    destination = @@kpsewhich[option('tree')] || @@kpsewhich['unknown']
+                    destination = `kpsewhich --expand-var=$#{destination}`.chomp
+                elsif ! FileTest.directory?(destination) then
+                    destination = nil
+                end
+            rescue
+                report("unable to determine destination tex root")
+            else
+                if ! destination || destination.empty? then
+                    report("no destination is specified")
+                elsif not FileTest.directory?(destination) then
+                    report("invalid destination '#{destination}'")
+                elsif not FileTest.writable?(destination) then
+                    report("destination '#{destination}' is not writable")
+                else
+                    report("using destination '#{destination}'")
+                    return destination
+                end
+            end
+       else
+           report("unknown destination")
+       end
+        return nil
+    end
+
+end
+
+logger      = Logger.new(banner.shift)
+commandline = CommandLine.new
+
+commandline.registeraction('update', 'update installed tree')
+commandline.registeraction('list', 'list available trees')
+
+commandline.registerflag('terse', 'download as less as possible (esp binaries)')
+commandline.registerflag('full', 'download everything (all binaries)')
+commandline.registerflag('force', 'confirm action')
+commandline.registerflag('make', 'remake formats')
+
+commandline.registervalue('address', 'www.pragma-ade.com', 'adress of repository (www.pragma-ade)')
+commandline.registervalue('user', 'guest', 'user account (guest)')
+commandline.registervalue('tree', 'tex', 'tree to synchronize (tex)')
+commandline.registervalue('destination', nil, 'destination of tree (kpsewhich)')
+
+commandline.registeraction('help')
+commandline.registeraction('version')
+
+commandline.expand
+
+Commands.new(commandline,logger,banner).send(commandline.action || 'help')
diff --git a/scripts/context/ruby/textools.rb b/scripts/context/ruby/textools.rb
new file mode 100644
index 000000000..a5858c5ca
--- /dev/null
+++ b/scripts/context/ruby/textools.rb
@@ -0,0 +1,1033 @@
+#!/usr/bin/env ruby
+
+# program   : textools
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2002-2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+# This script will harbor some handy manipulations on tex
+# related files.
+
+banner = ['TeXTools', 'version 1.3.1', '2002/2006', 'PRAGMA ADE/POD']
+
+$: << File.expand_path(File.dirname($0)) ; $: << File.join($:.last,'lib') ; $:.uniq!
+
+require 'base/switch'
+require 'base/logger'
+
+require 'fileutils'
+# require 'ftools'
+
+# Remark
+#
+# The fixtexmftrees feature does not realy belong in textools, but
+# since it looks like no measures will be taken to make texlive (and
+# tetex) downward compatible with respect to fonts installed by
+# users, we provide this fixer. This option also moves script files
+# to their new location (only for context) in the TDS. Beware: when
+# locating  scripts, the --format switch in kpsewhich should now use
+# 'texmfscripts' instead of  'other text files' (texmfstart is already
+# aware of this). Files will only be moved when --force is given. Let
+# me know if more fixes need to be made.
+
+class Commands
+
+    include CommandBase
+
+    def tpmmake
+        if filename = @commandline.argument('first') then
+            filename = File.join('tpm',filename) unless filename =~ /^tpm[\/\\]/
+            filename += '.tpm' unless filename =~ /\.tpm$/
+            if FileTest.file?(filename) then
+                data = IO.read(filename) rescue ''
+                data, fn, n = calculate_tpm(data,"TPM:RunFiles")
+                data, fm, m = calculate_tpm(data,"TPM:DocFiles")
+                data = replace_tpm(data,"TPM:Size",n+m)
+                report("total size #{n+m}")
+                begin
+                    File.open(filename, 'w') do |f|
+                        f << data
+                    end
+                rescue
+                    report("unable to save '#{filename}'")
+                else
+                    report("file '#{filename}' is updated")
+                    filename = File.basename(filename).sub(/\..*$/,'')
+                    zipname = sprintf("%s-%04i.%02i.%02i%s",filename,Time.now.year,Time.now.month,Time.now.day,'.zip')
+                    File.delete(zipname) rescue true
+                    report("zipping file '#{zipname}'")
+                    system("zip -r -9 -q #{zipname} #{[fn,fm].flatten.join(' ')}")
+                end
+            else
+                report("no file '#{filename}'")
+            end
+        end
+    end
+
+    def calculate_tpm(data, tag='')
+        size, ok = 0, Array.new
+        data.gsub!(/<#{tag}.*>(.*?)<\/#{tag}>/m) do
+            content = $1
+            files = content.split(/\s+/)
+            files.each do |file|
+                unless file =~ /^\s*$/ then
+                    if FileTest.file?(file) then
+                        report("found file #{file}")
+                        size += FileTest.size(file) rescue 0
+                        ok << file
+                    else
+                        report("missing file #{file}")
+                    end
+                end
+            end
+            "<#{tag} size=\"#{size}\">#{content}</#{tag}>"
+        end
+        [data, ok, size]
+    end
+
+    def replace_tpm(data, tag='', txt='')
+        data.gsub(/(<#{tag}.*>)(.*?)(<\/#{tag}>)/m) do
+            $1 + txt.to_s + $3
+        end
+    end
+
+end
+
+class Commands
+
+    include CommandBase
+
+    def hidemapnames
+        report('hiding FontNames in map files')
+        xidemapnames(true)
+    end
+
+    def videmapnames
+        report('unhiding FontNames in map files')
+        xidemapnames(false)
+    end
+
+    def removemapnames
+
+        report('removing FontNames from map files')
+
+        if files = findfiles('map') then
+            report
+            files.sort.each do |fn|
+                gn = fn # + '.nonames'
+                hn = fn + '.original'
+                begin
+                    if FileTest.file?(fn) && ! FileTest.file?(hn) then
+                        if File.rename(fn,hn) then
+                            if (fh = File.open(hn,'r')) && (gh = File.open(gn,'w')) then
+                                report("processing #{fn}")
+                                while str = fh.gets do
+                                    str.sub!(/^([^\%]+?)(\s+)([^\"\<\s]*?)(\s)/) do
+                                        $1 + $2 + " "*$3.length + $4
+                                    end
+                                    gh.puts(str)
+                                end
+                                fh.close
+                                gh.close
+                            else
+                                report("no permissions to handle #{fn}")
+                            end
+                        else
+                            report("unable to rename #{fn} to #{hn}")
+                        end
+                    else
+                        report("not processing #{fn} due to presence of #{hn}")
+                    end
+                rescue
+                    report("error in handling #{fn}")
+                end
+            end
+        end
+
+    end
+
+    def restoremapnames
+
+        report('restoring FontNames in map files')
+
+        if files = findfiles('map') then
+            report
+            files.sort.each do |fn|
+                hn = fn + '.original'
+                begin
+                    if FileTest.file?(hn) then
+                        File.delete(fn) if FileTest.file?(fn)
+                        report("#{fn} restored") if File.rename(hn,fn)
+                    else
+                        report("no original found for #{fn}")
+                    end
+                rescue
+                    report("error in restoring #{fn}")
+                end
+            end
+        end
+
+    end
+
+    def findfile
+
+        report('locating file in texmf tree')
+
+        # ! not in tree
+        # ? fuzzy
+        # . in tree
+        # > in tree and used
+
+        if filename = @commandline.argument('first') then
+            if filename && ! filename.empty? then
+                report
+                used = kpsefile(filename) || pathfile(filename)
+                if paths = texmfroots then
+                    found, prefered = false, false
+                    paths.each do |p|
+                        if files = texmffiles(p,filename) then
+                            found = true
+                            files.each do |f|
+                                # unreadable: report("#{if f == used then '>' else '.' end} #{f}")
+                                if f == used then
+                                    prefered = true
+                                    report("> #{f}")
+                                else
+                                    report(". #{f}")
+                                end
+                            end
+                        end
+                    end
+                    if prefered then
+                        report("! #{used}") unless found
+                    else
+                        report("> #{used}")
+                    end
+                elsif used then
+                    report("? #{used}")
+                else
+                    report('no file found')
+                end
+            else
+                report('no file specified')
+            end
+        else
+            report('no file specified')
+        end
+
+    end
+
+    def unzipfiles
+
+        report('g-unzipping files')
+
+        if files = findfiles('gz') then
+            report
+            files.each do |f|
+                begin
+                    system("gunzip -d #{f}")
+                rescue
+                    report("unable to unzip file #{f}")
+                else
+                    report("file #{f} is unzipped")
+                end
+            end
+        end
+
+    end
+
+    def fixafmfiles
+
+        report('fixing afm files')
+
+        if files = findfiles('afm') then
+            report
+            ok = false
+            files.each do |filename|
+                if filename =~ /\.afm$/io then
+                    if f = File.open(filename) then
+                        result = ''
+                        done = false
+                        while str = f.gets do
+                            str.chomp!
+                            str.strip!
+                            if str.empty? then
+                                # skip
+                            elsif (str.length > 200) && (str =~ /^(comment|notice)\s(.*)\s*$/io) then
+                                done = true
+                                tag, words, len = $1, $2.split(' '), 0
+                                result += tag
+                                while words.size > 0 do
+                                    str = words.shift
+                                    len += str.length + 1
+                                    result += ' ' + str
+                                    if len > (70 - tag.length) then
+                                        result += "\n"
+                                        result += tag if words.size > 0
+                                        len = 0
+                                    end
+                                end
+                                result += "\n" if len>0
+                            else
+                                result += str + "\n"
+                            end
+                        end
+                        f.close
+                        if done then
+                            ok = true
+                            begin
+                                if File.rename(filename,filename+'.original') then
+                                    if FileTest.file?(filename) then
+                                        report("something to fix in #{filename} but error in renaming (3)")
+                                    elsif f = File.open(filename,'w') then
+                                        f.puts(result)
+                                        f.close
+                                        report('file', filename, 'has been fixed')
+                                    else
+                                        report("something to fix in #{filename} but error in opening (4)")
+                                        File.rename(filename+'.original',filename) # gamble
+                                    end
+                                else
+                                    report("something to fix in #{filename} but error in renaming (2)")
+                                end
+                            rescue
+                                report("something to fix in #{filename} but error in renaming (1)")
+                            end
+                        else
+                            report("nothing to fix in #{filename}")
+                        end
+                    else
+                        report("error in opening #{filename}")
+                    end
+                end
+            end
+            report('no files match the pattern') unless ok
+        end
+
+    end
+
+    def mactodos
+
+        report('fixing mac newlines')
+
+        if files = findfiles('tex') then
+            report
+            files.each do |filename|
+                begin
+                    report("converting file #{filename}")
+                    tmpfilename = filename + '.tmp'
+                    if f = File.open(filename) then
+                        if g = File.open(tmpfilename, 'w')
+                            while str = f.gets do
+                                g.puts(str.gsub(/\r/,"\n"))
+                            end
+                            if f.close && g.close && FileTest.file?(tmpfilename) then
+                                File.delete(filename)
+                                File.rename(tmpfilename,filename)
+                            end
+                        else
+                            report("unable to open temporary file #{tmpfilename}")
+                        end
+                    else
+                        report("unable to open #{filename}")
+                    end
+                rescue
+                    report("problems with fixing #{filename}")
+                end
+            end
+        end
+
+    end
+
+    def fixtexmftrees
+
+        if paths = @commandline.argument('first') then
+            paths = [paths] if ! paths.empty?
+        end
+        paths = texmfroots if paths.empty?
+
+        if paths then
+
+            moved = 0
+            force = @commandline.option('force')
+
+            report
+            report("checking TDS 2003 => TDS 2004 : map files")
+            # report
+
+            # move [map,enc]  files from /texmf/[dvips,pdftex,dvipdfmx] -> /texmf/fonts/[*]
+
+            ['map','enc'].each do |suffix|
+                paths.each do |path|
+                    ['dvips','pdftex','dvipdfmx'].each do |program|
+                        report
+                        report("checking #{suffix} files for #{program} on #{path}")
+                        report
+                        moved += movefiles("#{path}/#{program}","#{path}/fonts/#{suffix}/#{program}",suffix) do
+                            # nothing
+                        end
+                    end
+                end
+            end
+
+            report
+            report("checking TDS 2003 => TDS 2004 : scripts")
+            # report
+
+            # move [rb,pl,py] files from /texmf/someplace -> /texmf/scripts/someplace
+
+            ['rb','pl','py'].each do |suffix|
+                paths.each do |path|
+                    ['context'].each do |program|
+                        report
+                        report("checking #{suffix} files for #{program} on #{path}")
+                        report
+                        moved += movefiles("#{path}/#{program}","#{path}/scripts/#{program}",suffix) do |f|
+                            f.gsub!(/\/(perl|ruby|python)tk\//o) do
+                                "/#{$1}/"
+                            end
+                        end
+                    end
+                end
+            end
+
+            begin
+                if moved>0 then
+                    report
+                    if force then
+                        system('mktexlsr')
+                        report
+                        report("#{moved} files moved")
+                    else
+                        report("#{moved} files will be moved")
+                    end
+                else
+                    report('no files need to be moved')
+                end
+            rescue
+                report('you need to run mktexlsr')
+            end
+
+        end
+
+    end
+
+    def replacefile
+
+        report('replace file')
+
+        if newname = @commandline.argument('first') then
+            if newname && ! newname.empty? then
+                report
+                report("replacing #{newname}")
+                report
+                oldname = kpsefile(File.basename(newname))
+                force = @commandline.option('force')
+                if oldname && ! oldname.empty? then
+                    oldname = File.expand_path(oldname)
+                    newname = File.expand_path(newname)
+                    report("old: #{oldname}")
+                    report("new: #{newname}")
+                    report
+                    if newname == oldname then
+                        report('unable to replace itself')
+                    elsif force then
+                        begin
+                            File.copy(newname,oldname)
+                        rescue
+                            report('error in replacing the old file')
+                        end
+                    else
+                        report('the old file will be replaced (use --force)')
+                    end
+                else
+                    report('nothing to replace')
+                end
+            else
+                report('no file specified')
+            end
+        else
+            report('no file specified')
+        end
+
+    end
+
+    private # general
+
+    def texmfroots
+        begin
+            paths = `kpsewhich -expand-path=\$TEXMF`.chomp
+        rescue
+        else
+            return paths.split(/#{File::PATH_SEPARATOR}/) if paths && ! paths.empty?
+        end
+        return nil
+    end
+
+    def texmffiles(root, filename)
+        begin
+            files = Dir.glob("#{root}/**/#{filename}")
+        rescue
+        else
+            return files if files && files.length>0
+        end
+        return nil
+    end
+
+    def pathfile(filename)
+        used = nil
+        begin
+            if ! filename || filename.empty? then
+                return nil
+            else
+                ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
+                    if FileTest.file?(File.join(path,filename)) then
+                        used = File.join(path,filename)
+                        break
+                    end
+                end
+            end
+        rescue
+            used = nil
+        else
+            used = nil if used && used.empty?
+        end
+        return used
+    end
+
+    def kpsefile(filename)
+        used = nil
+        begin
+            if ! filename || filename.empty? then
+                return nil
+            else
+                used = `kpsewhich #{filename}`.chomp
+            end
+            if used && used.empty? then
+                used = `kpsewhich -progname=context #{filename}`.chomp
+            end
+            if used && used.empty? then
+                used = `kpsewhich -format=texmfscripts #{filename}`.chomp
+            end
+            if used && used.empty? then
+                used = `kpsewhich -progname=context -format=texmfscripts #{filename}`.chomp
+            end
+            if used && used.empty? then
+                used = `kpsewhich -format="other text files" #{filename}`.chomp
+            end
+            if used && used.empty? then
+                used = `kpsewhich -progname=context -format="other text files" #{filename}`.chomp
+            end
+        rescue
+            used = nil
+        else
+            used = nil if used && used.empty?
+        end
+        return used
+    end
+
+    def downcasefilenames
+
+        report('downcase filenames')
+
+        force = @commandline.option('force')
+
+        # if @commandline.option('recurse') then
+            # files = Dir.glob('**/*')
+        # else
+            # files = Dir.glob('*')
+        # end
+        # if files && files.length>0 then
+
+        if files = findfiles() then
+            files.each do |oldname|
+                if FileTest.file?(oldname) then
+                    newname = oldname.downcase
+                    if oldname != newname then
+                        if force then
+                            begin
+                                File.rename(oldname,newname)
+                            rescue
+                                report("#{oldname} == #{oldname}\n")
+                            else
+                                report("#{oldname} => #{newname}\n")
+                            end
+                        else
+                            report("(#{oldname} => #{newname})\n")
+                        end
+                    end
+                end
+            end
+        end
+    end
+
+    def stripformfeeds
+
+        report('strip formfeeds')
+
+        force = @commandline.option('force')
+
+        if files = findfiles() then
+            files.each do |filename|
+                if FileTest.file?(filename) then
+                    begin
+                        data = IO.readlines(filename).join('')
+                    rescue
+                    else
+                        if data.gsub!(/\n*\f\n*/io,"\n\n") then
+                            if force then
+                                if f = open(filename,'w') then
+                                    report("#{filename} is stripped\n")
+                                    f.puts(data)
+                                    f.close
+                                else
+                                    report("#{filename} cannot be stripped\n")
+                                end
+                            else
+                                report("#{filename} will be stripped\n")
+                            end
+                        end
+                    end
+                end
+            end
+        end
+    end
+
+    public
+
+    def showfont
+
+        file = @commandline.argument('first')
+
+        if file.empty? then
+            report('provide filename')
+        else
+            file.sub!(/\.afm$/,'')
+            begin
+                report("analyzing afm file #{file}.afm")
+                file = `kpsewhich #{file}.afm`.chomp
+            rescue
+                report('unable to run kpsewhich')
+                return
+            end
+
+            names = Array.new
+
+            if FileTest.file?(file) then
+                File.new(file).each do |line|
+                    if line.match(/^C\s*([\-\d]+)\s*\;.*?\s*N\s*(.+?)\s*\;/o) then
+                        names.push($2)
+                    end
+                end
+                ranges = names.size
+                report("number of glyphs: #{ranges}")
+                ranges = ranges/256 + 1
+                report("number of subsets: #{ranges}")
+                file = File.basename(file).sub(/\.afm$/,'')
+                tex = File.open("textools.tex",'w')
+                map = File.open("textools.map",'w')
+                tex.puts("\\starttext\n")
+                tex.puts("\\loadmapfile[textools.map]\n")
+                for i in 1..ranges do
+                    rfile = "#{file}-range-#{i}"
+                    report("generating enc file #{rfile}.enc")
+                    flushencoding("#{rfile}", (i-1)*256, i*256-1, names)
+                    # catch console output
+                    report("generating tfm file #{rfile}.tfm")
+                    mapline = `afm2tfm #{file}.afm -T #{rfile}.enc #{rfile}.tfm`
+                    # more robust replacement
+                    mapline = "#{rfile} <#{rfile}.enc <#{file}.pfb"
+                    # final entry in map file
+                    mapline = "#{mapline} <#{file}.pfb"
+                    map.puts("#{mapline}\n")
+                    tex.puts("\\showfont[#{rfile}][unknown]\n")
+                end
+                tex.puts("\\stoptext\n")
+                report("generating map file textools.map")
+                report("generating tex file textools.tex")
+                map.close
+                tex.close
+            else
+                report("invalid file #{file}")
+            end
+        end
+
+    end
+
+    @@knownchars = Hash.new
+
+    @@knownchars['ae'] = 'aeligature' ; @@knownchars['oe'] = 'oeligature'
+    @@knownchars['AE'] = 'AEligature' ; @@knownchars['OE'] = 'OEligature'
+
+    @@knownchars['acute'       ] = 'textacute'
+    @@knownchars['breve'       ] = 'textbreve'
+    @@knownchars['caron'       ] = 'textcaron'
+    @@knownchars['cedilla'     ] = 'textcedilla'
+    @@knownchars['circumflex'  ] = 'textcircumflex'
+    @@knownchars['diaeresis'   ] = 'textdiaeresis'
+    @@knownchars['dotaccent'   ] = 'textdotaccent'
+    @@knownchars['grave'       ] = 'textgrave'
+    @@knownchars['hungarumlaut'] = 'texthungarumlaut'
+    @@knownchars['macron'      ] = 'textmacron'
+    @@knownchars['ogonek'      ] = 'textogonek'
+    @@knownchars['ring'        ] = 'textring'
+    @@knownchars['tilde'       ] = 'texttilde'
+
+    @@knownchars['cent'    ] = 'textcent'
+    @@knownchars['currency'] = 'textcurrency'
+    @@knownchars['euro'    ] = 'texteuro'
+    @@knownchars['florin'  ] = 'textflorin'
+    @@knownchars['sterling'] = 'textsterling'
+    @@knownchars['yen'     ] = 'textyen'
+
+    @@knownchars['brokenbar'] = 'textbrokenbar'
+    @@knownchars['bullet'   ] = 'textbullet'
+    @@knownchars['dag'      ] = 'textdag'
+    @@knownchars['ddag'     ] = 'textddag'
+    @@knownchars['degree'   ] = 'textdegree'
+    @@knownchars['div'      ] = 'textdiv'
+    @@knownchars['ellipsis' ] = 'textellipsis'
+    @@knownchars['fraction' ] = 'textfraction'
+    @@knownchars['lognot'   ] = 'textlognot'
+    @@knownchars['minus'    ] = 'textminus'
+    @@knownchars['mu'       ] = 'textmu'
+    @@knownchars['multiply' ] = 'textmultiply'
+    @@knownchars['pm'       ] = 'textpm'
+
+    def encmake
+        afmfile  = @commandline.argument('first')
+        encoding = @commandline.argument('second') || 'dummy'
+        if afmfile && FileTest.file?(afmfile) then
+            chars = Array.new
+            IO.readlines(afmfile).each do |line|
+                if line =~ /C\s+(\d+).*?N\s+([a-zA-Z\-\.]+?)\s*;/ then
+                    chars[$1.to_i] = $2
+                end
+            end
+            if f = File.open(encoding+'.enc','w') then
+                f << "% Encoding file, generated by textools.rb from #{afmfile}\n"
+                f << "\n"
+                f << "/#{encoding.gsub(/[^a-zA-Z]/,'')}encoding [\n"
+                256.times do |i|
+                    f << "  /#{chars[i] || '.notdef'} % #{i}\n"
+                end
+                f << "] def\n"
+                f.close
+            end
+            if f = File.open('enco-'+encoding+'.tex','w') then
+                f << "% ConTeXt file, generated by textools.rb from #{afmfile}\n"
+                f << "\n"
+                f << "\\startencoding[#{encoding}]\n\n"
+                256.times do |i|
+                    if str = chars[i] then
+                        tmp = str.gsub(/dieresis/,'diaeresis')
+                        if chr = @@knownchars[tmp] then
+                            f << "  \\definecharacter #{chr} #{i}\n"
+                        elsif tmp.length > 5 then
+                            f << "  \\definecharacter #{tmp} #{i}\n"
+                        end
+                    end
+                end
+                f << "\n\\stopencoding\n"
+                f << "\n\\endinput\n"
+                f.close
+            end
+        end
+    end
+
+    private
+
+    def flushencoding (file, from, to, names)
+        n = 0
+        out = File.open("#{file}.enc",'w')
+        out.puts("/#{file.gsub(/\-/,'')} [\n")
+        for i in from..to do
+            if names[i] then
+                n += 1
+                out.puts("/#{names[i]}\n")
+            else
+                out.puts("/.notdef\n")
+            end
+        end
+        out.puts("] def\n")
+        out.close
+        return n
+    end
+
+    private # specific
+
+    def movefiles(from_path,to_path,suffix,&block)
+        obsolete = 'obsolete'
+        force = @commandline.option('force')
+        moved = 0
+        if files = texmffiles(from_path, "*.#{suffix}") then
+            files.each do |filename|
+                newfilename = filename.sub(/^#{from_path}/, to_path)
+                yield(newfilename) if block
+                if FileTest.file?(newfilename) then
+                    begin
+                        File.rename(filename,filename+'.obsolete') if force
+                    rescue
+                        report("#{filename} cannot be made obsolete") if force
+                    else
+                        if force then
+                            report("#{filename} is made obsolete")
+                        else
+                            report("#{filename} will become obsolete")
+                        end
+                    end
+                else
+                    begin
+                        File.makedirs(File.dirname(newfilename)) if force
+                    rescue
+                    end
+                    begin
+                        File.copy(filename,newfilename) if force
+                    rescue
+                        report("#{filename} cannot be copied to #{newfilename}")
+                    else
+                        begin
+                            File.delete(filename) if force
+                        rescue
+                            report("#{filename} cannot be deleted") if force
+                        else
+                            if force then
+                                report("#{filename} is moved to #{newfilename}")
+                                moved += 1
+                            else
+                                report("#{filename} will be moved to #{newfilename}")
+                            end
+                        end
+                    end
+                end
+            end
+        else
+            report('no matches found')
+        end
+        return moved
+    end
+
+    def xidemapnames(hide)
+
+        filter  = /^([^\%]+?)(\s+)([^\"\<\s]*?)(\s)/
+        banner  = '% textools:nn '
+
+        if files = findfiles('map') then
+            report
+            files.sort.each do |fn|
+                if fn.has_suffix?('map') then
+                    begin
+                        lines = IO.read(fn)
+                        report("processing #{fn}")
+                        if f = File.open(fn,'w') then
+                            skip = false
+                            if hide then
+                                lines.each do |str|
+                                    if skip then
+                                        skip = false
+                                    elsif str =~ /#{banner}/ then
+                                        skip = true
+                                    elsif str =~ filter then
+                                        f.puts(banner+str)
+                                        str.sub!(filter) do
+                                            $1 + $2 + " "*$3.length + $4
+                                        end
+                                    end
+                                    f.puts(str)
+                                end
+                            else
+                                lines.each do |str|
+                                    if skip then
+                                        skip = false
+                                    elsif str.sub!(/#{banner}/, '') then
+                                        f.puts(str)
+                                        skip = true
+                                    else
+                                        f.puts(str)
+                                    end
+                                end
+                            end
+                            f.close
+                        end
+                    rescue
+                        report("error in handling #{fn}")
+                    end
+                end
+            end
+        end
+
+    end
+
+    public
+
+    def updatetree
+
+        nocheck  = @commandline.option('nocheck')
+        merge    = @commandline.option('merge')
+        delete   = @commandline.option('delete')
+        force    = @commandline.option('force')
+        root     = @commandline.argument('first').gsub(/\\/,'/')
+        path     = @commandline.argument('second').gsub(/\\/,'/')
+
+        if FileTest.directory?(root) then
+            report("scanning #{root}")
+            rootfiles = Dir.glob("#{root}/**/*")
+        else
+            report("provide source root")
+            return
+        end
+        if rootfiles.size > 0 then
+            report("#{rootfiles.size} files")
+        else
+            report("no files")
+            return
+        end
+        rootfiles.collect! do |rf|
+            rf.gsub(/\\/o, '/').sub(/#{root}\//o, '')
+        end
+        rootfiles = rootfiles.delete_if do |rf|
+            FileTest.directory?(File.join(root,rf))
+        end
+
+        if FileTest.directory?(path) then
+            report("scanning #{path}")
+            pathfiles = Dir.glob("#{path}/**/*")
+        else
+            report("provide destination root")
+            return
+        end
+        if pathfiles.size > 0 then
+            report("#{pathfiles.size} files")
+        else
+            report("no files")
+            return
+        end
+        pathfiles.collect! do |pf|
+            pf.gsub(/\\/o, '/').sub(/#{path}\//o, '')
+        end
+        pathfiles = pathfiles.delete_if do |pf|
+            FileTest.directory?(File.join(path,pf))
+        end
+
+        root = File.expand_path(root)
+        path = File.expand_path(path)
+
+        donepaths   = Hash.new
+        copiedfiles = Hash.new
+
+        # update existing files, assume similar paths
+
+        report("")
+        pathfiles.each do |f| # destination
+            p = File.join(path,f)
+            if rootfiles.include?(f) then
+                r = File.join(root,f)
+                if p != r then
+                    if nocheck or File.mtime(p) < File.mtime(r) then
+                        copiedfiles[File.expand_path(p)] = true
+                        report("updating '#{r}' to '#{p}'")
+                        begin
+                            begin File.makedirs(File.dirname(p)) if force ; rescue ; end
+                            File.copy(r,p) if force
+                        rescue
+                            report("updating failed")
+                        end
+                    else
+                        report("not updating '#{r}'")
+                    end
+                end
+            end
+        end
+
+        # merging non existing files
+
+        report("")
+        rootfiles.each do |f|
+            donepaths[File.dirname(f)] = true
+            r = File.join(root,f)
+            if not pathfiles.include?(f) then
+                p = File.join(path,f)
+                if p != r then
+                    if merge then
+                        copiedfiles[File.expand_path(p)] = true
+                        report("merging '#{r}' to '#{p}'")
+                        begin
+                            begin File.makedirs(File.dirname(p)) if force ; rescue ; end
+                            File.copy(r,p) if force
+                        rescue
+                            report("merging failed")
+                        end
+                    else
+                        report("not merging '#{r}'")
+                    end
+                end
+            end
+        end
+
+        # deleting obsolete files
+
+        report("")
+        donepaths.keys.sort.each do |d|
+            pathfiles = Dir.glob("#{path}/#{d}/**/*")
+            pathfiles.each do |p|
+# puts(File.dirname(p))
+# if donepaths[File.dirname(p)] then
+                r = File.join(root,d,File.basename(p))
+                if FileTest.file?(p) and not FileTest.file?(r) and not copiedfiles.key?(File.expand_path(p)) then
+                    if delete then
+                        report("deleting '#{p}'")
+                        begin
+                            File.delete(p) if force
+                        rescue
+                            report("deleting failed")
+                        end
+                    else
+                        report("not deleting '#{p}'")
+                    end
+                end
+            end
+# end
+        end
+
+    end
+
+end
+
+logger      = Logger.new(banner.shift)
+commandline = CommandLine.new
+
+commandline.registeraction('removemapnames'   , '[pattern]   [--recurse]')
+commandline.registeraction('restoremapnames'  , '[pattern]   [--recurse]')
+commandline.registeraction('hidemapnames'     , '[pattern]   [--recurse]')
+commandline.registeraction('videmapnames'     , '[pattern]   [--recurse]')
+commandline.registeraction('findfile'         , 'filename    [--recurse]')
+commandline.registeraction('unzipfiles'       , '[pattern]   [--recurse]')
+commandline.registeraction('fixafmfiles'      , '[pattern]   [--recurse]')
+commandline.registeraction('mactodos'         , '[pattern]   [--recurse]')
+commandline.registeraction('fixtexmftrees'    , '[texmfroot] [--force]')
+commandline.registeraction('replacefile'      , 'filename    [--force]')
+commandline.registeraction('updatetree'       , 'fromroot toroot [--force --nocheck --merge --delete]')
+commandline.registeraction('downcasefilenames', '[--recurse] [--force]') # not yet documented
+commandline.registeraction('stripformfeeds'   , '[--recurse] [--force]') # not yet documented
+commandline.registeraction('showfont'         , 'filename')
+commandline.registeraction('encmake'          , 'afmfile encodingname')
+
+commandline.registeraction('tpmmake'          , 'tpm file (run in texmf root)')
+
+commandline.registeraction('help')
+commandline.registeraction('version')
+
+commandline.registerflag('recurse')
+commandline.registerflag('force')
+commandline.registerflag('merge')
+commandline.registerflag('delete')
+commandline.registerflag('nocheck')
+
+commandline.expand
+
+Commands.new(commandline,logger,banner).send(commandline.action || 'help')
diff --git a/scripts/context/ruby/texutil.rb b/scripts/context/ruby/texutil.rb
new file mode 100644
index 000000000..ee0fc1e5e
--- /dev/null
+++ b/scripts/context/ruby/texutil.rb
@@ -0,0 +1,93 @@
+banner = ['TeXUtil  ', 'version 9.1.0', '1997-2005', 'PRAGMA ADE/POD']
+
+$: << File.expand_path(File.dirname($0)) ; $: << File.join($:.last,'lib') ; $:.uniq!
+
+require 'base/switch'
+require 'base/logger'
+require 'base/file'
+require 'base/texutil'
+
+class Commands
+
+    include CommandBase
+
+    def references
+        filename = @commandline.argument('first')
+        if not filename.empty? and FileTest.file?(File.suffixed(filename,'tuo')) then
+            if tu = TeXUtil::Converter.new(logger) and tu.loaded(filename) then
+                tu.saved if tu.processed
+            end
+        end
+    end
+
+    def main
+        if @commandline.arguments.length>0 then
+            references
+        else
+            help
+        end
+    end
+
+    def purgefiles
+        system("texmfstart ctxtools --purge #{@commandline.arguments.join(' ')}")
+    end
+
+    def purgeallfiles
+        system("texmfstart ctxtools --purge --all #{@commandline.arguments.join(' ')}")
+    end
+
+    def documentation
+        system("texmfstart ctxtools --document #{@commandline.arguments.join(' ')}")
+    end
+
+    def analyzefile
+        system("texmfstart pdftools --analyze #{@commandline.arguments.join(' ')}")
+    end
+
+    def filterpages # obsolete
+        system("texmfstart ctxtools --purge #{@commandline.arguments.join(' ')}")
+    end
+
+    def figures
+        report("this code is not yet converted from perl to ruby")
+    end
+
+    def logfile
+        report("this code is not yet converted from perl to ruby")
+    end
+
+end
+
+logger      = Logger.new(banner.shift)
+commandline = CommandLine.new
+
+# main feature
+
+commandline.registeraction('references', 'convert tui file into tuo file')
+
+# todo features
+
+commandline.registeraction('figures', 'generate figure dimensions file')
+commandline.registeraction('logfile', 'filter essential log messages')
+
+# backward compatibility features
+
+commandline.registeraction('purgefiles', 'remove most temporary files')
+commandline.registeraction('purgeallfiles', 'remove all temporary files')
+commandline.registeraction('documentation', 'generate documentation file from source')
+commandline.registeraction('analyzefile', 'analyze pdf file')
+
+# old feature, not needed any longer due to extension of pdftex
+
+commandline.registeraction('filterpages')
+
+# generic features
+
+commandline.registeraction('help')
+commandline.registeraction('version')
+
+commandline.registerflag('verbose')
+
+commandline.expand
+
+Commands.new(commandline,logger,banner).send(commandline.action || 'main')
diff --git a/scripts/context/ruby/tmftools.rb b/scripts/context/ruby/tmftools.rb
new file mode 100644
index 000000000..626ef1f4a
--- /dev/null
+++ b/scripts/context/ruby/tmftools.rb
@@ -0,0 +1,165 @@
+#!/usr/bin/env ruby
+
+# program   : tmftools
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+# The script based alternative is not slower than the kpse one.
+# Loading is a bit faster when the log file is used.
+
+# todo: create database
+
+# tmftools [some of the kpsewhich switches]
+
+# tmftools --analyze
+# tmftools --analyze > kpsewhat.log
+# tmftools --analyze --strict > kpsewhat.log
+# tmftools --analyze --delete --force "texmf-local/fonts/.*/somename"
+# tmftools --serve
+
+# the real thing
+
+banner = ['TMFTools', 'version 1.1.0 (experimental, no help yet)', '2005/2006', 'PRAGMA ADE/POD']
+
+$: << File.expand_path(File.dirname($0)) ; $: << File.join($:.last,'lib') ; $:.uniq!
+
+require 'base/switch'
+require 'base/logger'
+
+class Commands
+
+    include CommandBase
+
+    def init_kpse
+        # require 'base/kpseremote'
+        # if KpseRemote::available? then
+        if ENV['KPSEMETHOD'] && ENV['KPSEPORT'] then
+            require 'base/kpseremote'
+            k = KpseRemote.new
+        else
+            k = nil
+        end
+        if k && k.okay? then
+            k.progname = @commandline.option('progname')
+            k.engine   = @commandline.option('engine')
+            k.format   = @commandline.option('format')
+        else
+            require 'base/kpsefast'
+            k = KpseFast.new
+            k.rootpath   = @commandline.option('rootpath')
+            k.treepath   = @commandline.option('treepath')
+            k.progname   = @commandline.option('progname')
+            k.engine     = @commandline.option('engine')
+            k.format     = @commandline.option('format')
+            k.diskcache  = @commandline.option('diskcache')
+            k.renewcache = @commandline.option('renewcache')
+            k.load_cnf
+            k.expand_variables
+            k.load_lsr
+        end
+        return k
+    end
+
+    def serve
+        if ENV['KPSEMETHOD'] && ENV['KPSEPORT'] then
+            require 'base/kpseremote'
+            begin
+                KpseRemote::start_server
+            rescue
+            end
+        end
+    end
+
+    def reload
+        begin
+            init_kpse.load
+        rescue
+        end
+    end
+
+    def main
+        if    option = @commandline.option('expand-braces') and not option.empty? then
+            puts init_kpse.expand_braces(option)
+        elsif option = @commandline.option('expand-path')   and not option.empty? then
+            puts init_kpse.expand_path(option)
+        elsif option = @commandline.option('expand-var')    and not option.empty? then
+            if option == '*' then
+                init_kpse.list_expansions()
+            else
+                puts init_kpse.expand_var(option)
+            end
+        elsif option = @commandline.option('show-path')     and not option.empty? then
+            puts init_kpse.show_path(option)
+        elsif option = @commandline.option('var-value')     and not option.empty? then
+            if option == '*' then
+                init_kpse.list_variables()
+            else
+                puts init_kpse.expand_var(option)
+            end
+        elsif @commandline.arguments.size > 0 then
+            kpse = init_kpse
+            @commandline.arguments.each do |option|
+                puts kpse.find_file(option)
+            end
+        else
+            help
+        end
+    end
+
+    def analyze
+        pattern = @commandline.argument('first')
+        strict  = @commandline.option('strict')
+        sort    = @commandline.option('sort')
+        delete  = @commandline.option('delete') and @commandline.option('force')
+        init_kpse.analyze_files(pattern, strict, sort, delete)
+    end
+
+end
+
+logger      = Logger.new(banner.shift)
+commandline = CommandLine.new
+
+# kpsewhich compatible options
+
+commandline.registervalue('expand-braces','')
+commandline.registervalue('expand-path','')
+commandline.registervalue('expand-var','')
+commandline.registervalue('show-path','')
+commandline.registervalue('var-value','')
+
+commandline.registervalue('engine','')
+commandline.registervalue('progname','')
+commandline.registervalue('format','')
+
+# additional goodies
+
+commandline.registervalue('rootpath','')
+commandline.registervalue('treepath','')
+commandline.registervalue('sort','')
+
+commandline.registerflag('diskcache')
+commandline.registerflag('renewcache')
+commandline.registerflag('strict')
+commandline.registerflag('delete')
+commandline.registerflag('force')
+
+commandline.registeraction('analyze', "[--strict --sort --rootpath --treepath]\n[--delete [--force]] [pattern]")
+
+# general purpose options
+
+commandline.registerflag('verbose')
+commandline.registeraction('help')
+commandline.registeraction('version')
+
+commandline.registeraction('reload', 'reload file database')
+commandline.registeraction('serve', 'act as kpse server')
+
+commandline.expand
+
+Commands.new(commandline,logger,banner).send(commandline.action || 'main')
diff --git a/scripts/context/ruby/xmltools.rb b/scripts/context/ruby/xmltools.rb
new file mode 100644
index 000000000..c28df200d
--- /dev/null
+++ b/scripts/context/ruby/xmltools.rb
@@ -0,0 +1,656 @@
+#!/usr/bin/env ruby
+
+# program   : xmltools
+# copyright : PRAGMA Advanced Document Engineering
+# version   : 2002-2005
+# author    : Hans Hagen
+#
+# project   : ConTeXt / eXaMpLe
+# concept   : Hans Hagen
+# info      : j.hagen@xs4all.nl
+# www       : www.pragma-ade.com
+
+# todo : use kpse lib
+
+# This script will harbor some handy manipulations on tex
+# related files.
+
+banner = ['XMLTools', 'version 1.2.2', '2002/2007', 'PRAGMA ADE/POD']
+
+$: << File.expand_path(File.dirname($0)) ; $: << File.join($:.last,'lib') ; $:.uniq!
+
+require 'base/switch'
+require 'base/logger'
+
+class String
+
+    def astring(n=10)
+        gsub(/(\d+)/o) do $1.to_s.rjust(n) end.gsub(/ /o, '0')
+    end
+
+    def xstring
+        if self =~ /\'/o then
+            "\"#{self.gsub(/\"/, '&quot;')}\""
+        else
+            "\'#{self}\'"
+        end
+    end
+
+end
+
+class Array
+
+    def asort(n=10)
+        sort {|x,y| x.astring(n) <=> y.astring(n)}
+    end
+
+end
+
+class Commands
+
+    include CommandBase
+
+    def dir
+
+        @xmlns     = "xmlns='http://www.pragma-ade.com/rlg/xmldir.rng'"
+
+        pattern    = @commandline.option('pattern')
+        recurse    = @commandline.option('recurse')
+        stripname  = @commandline.option('stripname')
+        longname   = @commandline.option('longname')
+        url        = @commandline.option('url')
+        outputfile = @commandline.option('output')
+        root       = @commandline.option('root')
+
+        def generate(output,files,url,root,longname)
+
+            class << output
+                def xputs(str,n=0)
+                    puts("#{' '*n}#{str}")
+                end
+            end
+
+            dirname = ''
+            output.xputs("<?xml version='1.0'?>\n\n")
+            if ! root || root.empty? then
+                rootatt = @xmlns
+            else
+                rootatt = " #{@xmlns} root='#{root}'"
+            end
+            rootatt += " timestamp='#{Time.now}'"
+            if url.empty? then
+                output.xputs("<files #{rootatt}>\n")
+            else
+                output.xputs("<files url='#{url}'#{rootatt}>\n")
+            end
+            files.each do |f|
+                bn, dn = File.basename(f), File.dirname(f)
+                if dirname != dn then
+                    output.xputs("</directory>\n", 2) if dirname != ''
+                    output.xputs("<directory name='#{dn}'>\n", 2)
+                    dirname = dn
+                end
+                if longname && dn != '.' then
+                    output.xputs("<file name='#{dn}/#{bn}'>\n", 4)
+                else
+                    output.xputs("<file name='#{bn}'>\n", 4)
+                end
+                output.xputs("<base>#{bn.sub(/\..*$/,'')}</base>\n", 6)
+                if File.stat(f).file? then
+                    bt = bn.sub(/^.*\./,'')
+                    if bt != bn then
+                        output.xputs("<type>#{bt}</type>\n", 6)
+                    end
+                    output.xputs("<size>#{File.stat(f).size}</size>\n", 6)
+                    permissions = ''
+                    permissions << 'r' if File.readable?(f)
+                    permissions << 'w' if File.writable?(f)
+                    permissions << 'x' if File.executable?(f)
+                    output.xputs("<permissions>#{permissions}</permissions>\n", 6) unless permissions.empty?
+                end
+                output.xputs("<date>#{File.stat(f).mtime.strftime("%Y-%m-%d %H:%M")}</date>\n", 6)
+                output.xputs("</file>\n", 4)
+            end
+            output.xputs("</directory>\n", 2) if dirname != ''
+            output.xputs("</files>\n")
+
+        end
+
+        if pattern.empty? then
+            report('provide --pattern=')
+            return
+        end
+
+        unless outputfile.empty? then
+            begin
+                output = File.open(outputfile,'w')
+            rescue
+                report("unable to open #{outputfile}")
+                return
+            end
+        else
+            report('provide --output')
+            return
+        end
+
+        if stripname && pattern.class == String && ! pattern.empty? then
+            pattern = File.dirname(pattern)
+        end
+
+        pattern = '*' if pattern.empty?
+
+        unless root.empty? then
+            unless FileTest.directory?(root) then
+                report("unknown root #{root}")
+                return
+            end
+            begin
+                Dir.chdir(root)
+            rescue
+                report("unable to change to root #{root}")
+                return
+            end
+        end
+
+        generate(output, globbed(pattern, recurse), url, root, longname)
+
+        output.close if output
+
+    end
+
+    alias ls :dir
+
+    def mmlpages
+
+        file  = @commandline.argument('first')
+        eps   = @commandline.option('eps')
+        jpg   = @commandline.option('jpg')
+        png   = @commandline.option('png')
+        style = @commandline.option('style')
+        modes = @commandline.option('modes')
+
+        file = file.sub(/\.xml/io, '')
+        long = "#{file}-mmlpages"
+        if FileTest.file?(file+'.xml') then
+            style = "--arg=\"style=#{style}\"" unless style.empty?
+            modes = "--mode=#{modes}" unless modes.empty?
+            if system("texmfstart texexec --batch --pdf --once --result=#{long} --use=mmlpag #{style} #{modes} #{file}.xml") then
+                if eps then
+                    if f = open("#{file}-mmlpages.txt") then
+                        while line = f.gets do
+                            data = Hash.new
+                            if fields = line.split then
+                                fields.each do |fld|
+                                    key, value = fld.split('=')
+                                    data[key] = value if key && value
+                                end
+                                if data.key?('p') then
+                                    page = data['p']
+                                    name = "#{long}-#{page.to_i-1}"
+                                    if eps then
+                                        report("generating eps file #{name}")
+                                        if system("pdftops -eps -f #{page} -l #{page} #{long}.pdf #{name}.eps") then
+                                            if data.key?('d') then
+                                                if epsfile = IO.read("#{name}.eps") then
+                                                    epsfile.sub!(/^(\%\%BoundingBox:.*?$)/i) do
+                                                        newline = $1 + "\n%%Baseline: #{data['d']}\n"
+                                                        if data.key?('w') && data.key?('h') then
+                                                            newline += "%%PositionWidth: #{data['w']}\n"
+                                                            newline += "%%PositionHeight: #{data['h']}\n"
+                                                            newline += "%%PositionDepth: #{data['d']}"
+                                                        end
+                                                        newline
+                                                    end
+                                                    if g = File.open("#{name}.eps",'wb') then
+                                                        g.write(epsfile)
+                                                        g.close
+                                                    end
+                                                end
+                                            end
+                                        else
+                                            report("error in generating eps from #{name}")
+                                        end
+                                    end
+                                end
+                            end
+                        end
+                        f.close
+                    else
+                        report("missing data log file #{file}")
+                    end
+                end
+                if png then
+                    report("generating png file for #{long}")
+                    system("imagemagick #{long}.pdf #{long}-%d.png")
+                end
+                if jpg then
+                    report("generating jpg files for #{long}")
+                    system("imagemagick #{long}.pdf #{long}-%d.jpg")
+                end
+            else
+                report("error in processing file #{file}")
+            end
+            system("texmfstart ctxtools --purge")
+        else
+            report("error in processing file #{file}")
+        end
+
+    end
+
+    def analyze
+
+        file    = @commandline.argument('first')
+        result  = @commandline.option('output')
+        utf     = @commandline.option('utf')
+        process = @commandline.option('process')
+
+        if FileTest.file?(file) then
+            if data = IO.read(file) then
+                if data =~ /<?xml.*?version\=/ then
+                    report("xml file #{file} loaded")
+                    elements   = Hash.new
+                    attributes = Hash.new
+                    entities   = Hash.new
+                    chars      = Hash.new
+                    unicodes   = Hash.new
+                    names      = Hash.new
+                    data.scan(/<([^>\s\/\!\?]+)([^>]*?)>/o) do
+                        element, attributelist = $1, $2
+                        if elements.key?(element) then
+                            elements[element] += 1
+                        else
+                            elements[element] = 1
+                        end
+                        attributelist.scan(/\s*([^\=]+)\=([\"\'])(.*?)(\2)/) do
+                            key, value = $1, $3
+                            attributes[element] = Hash.new unless attributes.key?(element)
+                            attributes[element][key] = Hash.new unless attributes[element].key?(key)
+                            if attributes[element][key].key?(value) then
+                                attributes[element][key][value] += 1
+                            else
+                                attributes[element][key][value] = 1
+                            end
+                        end
+                    end
+                    data.scan(/\&([^\;]+)\;/o) do
+                        entity = $1
+                        if entities.key?(entity) then
+                            entities[entity] += 1
+                        else
+                            entities[entity] = 1
+                        end
+                    end
+                    if utf then
+                        data.scan(/(\w)/u) do
+                            chars[$1] = (chars[$1] || 0) + 1
+                        end
+                        if chars.size > 0 then
+                            begin
+                                # todo : use kpse lib
+                                filename, ownpath, foundpath = 'contextnames.txt', File.dirname($0), ''
+                                begin
+                                    foundpath = File.dirname(`kpsewhich -progname=context -format=\"other text files\" #{filename}`.chomp)
+                                rescue
+                                    foundpath = '.'
+                                else
+                                    foundpath = '.' if foundpath.empty?
+                                end
+                                [foundpath,ownpath,File.join(ownpath,'../../../context/data')].each do |path|
+                                    fullname = File.join(path,filename)
+                                    if FileTest.file?(fullname) then
+                                        report("loading '#{fullname}'")
+                                        # rough scan, we assume no valid lines after comments
+                                        IO.read(fullname).scan(/^([0-9A-F][0-9A-F][0-9A-F][0-9A-F])\s*\;\s*(.*?)\s*\;\s*(.*?)\s*\;\s*(.*?)\s*$/) do
+                                            names[$1.hex.to_i.to_s] = [$2,$3,$4]
+                                        end
+                                        break
+                                    end
+                                end
+                            rescue
+                            end
+                        end
+                    end
+                    result = file.gsub(/\..*?$/, '') + '.xlg' if result.empty?
+                    if f = File.open(result,'w') then
+                        report("saving report in #{result}")
+                        f.puts "<?xml version='1.0'?>\n"
+                        f.puts "<document>\n"
+                        if entities.length>0 then
+                            total = 0
+                            entities.each do |k,v|
+                                total += v
+                            end
+                            f.puts "  <entities n=#{total.to_s.xstring}>\n"
+                            entities.keys.asort.each do |entity|
+                                f.puts "    <entity name=#{entity.xstring} n=#{entities[entity].to_s.xstring}/>\n"
+                            end
+                            f.puts "  </entities>\n"
+                        end
+                        if utf && (chars.size > 0) then
+                            total = 0
+                            chars.each do |k,v|
+                                total += v
+                            end
+                            f.puts "  <characters n=#{total.to_s.xstring}>\n"
+                            chars.each do |k,v|
+                                if k.length > 1 then
+                                    begin
+                                        u = k.unpack('U')
+                                        unicodes[u] = (unicodes[u] || 0) + v
+                                    rescue
+                                        report("invalid utf codes")
+                                    end
+                                end
+                            end
+                            unicodes.keys.sort.each do |u|
+                                ustr = u.to_s
+                                if names[ustr] then
+                                    f.puts "    <character number=#{ustr.xstring} pname=#{names[ustr][0].xstring} cname=#{names[ustr][1].xstring} uname=#{names[ustr][2].xstring} n=#{unicodes[u].to_s.xstring}/>\n"
+                                else
+                                    f.puts "    <character number=#{ustr.xstring} n=#{unicodes[u].to_s.xstring}/>\n"
+                                end
+                            end
+                            f.puts "  </characters>\n"
+                        end
+                        if elements.length>0 then
+                            f.puts "  <elements>\n"
+                            elements.keys.sort.each do |element|
+                                if attributes.key?(element) then
+                                    f.puts "    <element name=#{element.xstring} n=#{elements[element].to_s.xstring}>\n"
+                                    if attributes.key?(element) then
+                                        attributes[element].keys.asort.each do |attribute|
+                                            f.puts "      <attribute name=#{attribute.xstring}>\n"
+                                            if attribute =~ /id$/o then
+                                                nn = 0
+                                                attributes[element][attribute].keys.asort.each do |value|
+                                                    nn += attributes[element][attribute][value].to_i
+                                                end
+                                                f.puts "        <instance value=#{"*".xstring} n=#{nn.to_s.xstring}/>\n"
+                                            else
+                                                attributes[element][attribute].keys.asort.each do |value|
+                                                    f.puts "        <instance value=#{value.xstring} n=#{attributes[element][attribute][value].to_s.xstring}/>\n"
+                                                end
+                                            end
+                                            f.puts "      </attribute>\n"
+                                        end
+                                    end
+                                    f.puts "    </element>\n"
+                                else
+                                    f.puts "    <element name=#{element.xstring} n=#{elements[element].to_s.xstring}/>\n"
+                                end
+                            end
+                            f.puts "  </elements>\n"
+                        end
+                        f.puts "</document>\n"
+                        f.close
+                        if process then
+                            system("texmfstart texexec --purge --pdf --use=xml-analyze #{result}")
+                        end
+                    else
+                        report("unable to open file '#{result}'")
+                    end
+                else
+                    report("invalid xml file '#{file}'")
+                end
+            else
+                report("unable to load file '#{file}'")
+            end
+        else
+            report("unknown file '#{file}'")
+        end
+    end
+
+    def filter
+
+        require "rexml/document"
+
+        element = @commandline.option('element')
+        files   = @commandline.arguments
+        result  = "xmltools.xlg"
+
+        if element.empty? then
+            report("provide element using --element")
+        elsif files.length == 0 then
+            report("provide filename(s)")
+        else
+            begin
+                File.open(result,'w') do |f|
+                    f << "<?xml version='1.0'?>\n\n"
+                    f << "<xlg:document>\n\n"
+                    total = 0
+                    files.sort.each do |file|
+                        begin
+                            report("loading: #{file}")
+                            data = REXML::Document.new(IO.read(file))
+                        rescue
+                            report("error: invalid xml")
+                        else
+                            found = 0
+                            report("filtering: #{element}")
+                            REXML::XPath.each(data,"//#{element}") do |table|
+                                str = table.to_s
+                                if str.length > 0 then
+                                    total += 1
+                                    found += 1
+                                    report("found: #{total} / #{found} / #{str.length} bytes")
+                                    f << "<xlg:file name='#{file}'>\n\n" unless found > 1
+                                    f << "<xlg:filtered n='#{total}' m='#{found}'>"
+                                    f << "#{str.gsub(/^\s*/m,'').gsub(/\s*$/m,'')}"
+                                    f << "</xlg:filtered>\n\n"
+                                end
+                            end
+                            f << "</xlg:file>\n\n" if found > 0
+                        end
+                    end
+                    f << "</xlg:document>\n"
+                end
+                report("result: #{result}")
+            rescue
+                report("error in opening #{result}")
+            end
+        end
+
+    end
+
+    def enhance
+        oldname = @commandline.argument('first')
+        newname = @commandline.argument('second')
+        verbose = @commandline.option('verbose')
+        # todo: options, maybe a config file
+        if ! newname || newname.empty? then
+            newname = oldname + ".prep"
+        end
+        if FileTest.file?(oldname) then
+            report("") if verbose
+            data     = IO.read(oldname)
+            elements = Array.new
+            preamble = ""
+            done     = false
+            data.sub!(/^(.*?)\s*(<[a-z])/moi) do
+                preamble = $1
+                $2
+            end
+            # hide elements
+            data.gsub!(/<([^>]*?)>/moi) do
+                elements << $1
+                "<#{elements.length}>"
+            end
+            # abc[-/]def
+            data.gsub!(/([a-z]{3,})([\/\-\(\)]+)([a-z]{3,})/moi) do
+                done = true
+                report("compound: #{$1}#{$2}#{$3}") if verbose
+                "#{$1}<compound token='#{$2}'/>#{$3}"
+            end
+            # (abcd
+            # data.gsub!(/(\()([a-z]{4,})/moi) do
+                # done = true
+                # report("compound: #{$1}#{$2}") if verbose
+                # "<compound token='#{$1}'/>#{$2}"
+            # end
+            # abcd)
+            # data.gsub!(/(\()([a-z]{4,})/moi) do
+                # done = true
+                # report("compound: #{$1}#{$2}") if verbose
+                # "#{$2}<compound token='#{$2}'/>"
+            # end
+            # roll back elements
+            data.gsub!(/<(\d+)>/moi) do
+                "<#{elements.shift}>"
+            end
+            File.open(newname,'wb') do |f|
+                f << preamble
+                f << "\n"
+                f << data
+            end
+            if verbose then
+                if done then
+                    report("")
+                    report(oldname," converted to ",newname)
+                else
+                    report(oldname," copied to ",newname)
+                end
+            end
+        end
+    end
+
+    def cleanup # todo, share loading/saving with previous
+
+        file    = @commandline.argument('first')
+        force   = @commandline.option('force')
+        verbose = @commandline.option('verbose')
+
+        if FileTest.file?(file) then
+            if data = IO.read(file) then
+                if data =~ /<?xml.*?version\=/ then
+                    data = doxmlcleanup(data,verbose)
+                    result = if force then file else file.gsub(/\..*?$/, '') + '.xlg' end
+                    begin
+                        if f = File.open(result,'w') then
+                            f << data
+                            f.close
+                        end
+                    rescue
+                        report("unable to open file '#{result}'")
+                    end
+                else
+                    report("invalid xml file '#{file}'")
+                end
+            else
+                report("unable to load file '#{file}'")
+            end
+        else
+            report("unknown file '#{file}'")
+        end
+
+    end
+
+    def doxmlreport(str,verbose=false)
+        if verbose then
+            result = str
+            report(result)
+            return result
+        else
+            return str
+        end
+    end
+
+    def doxmlcleanup(data="",verbose=false)
+
+        # remove funny spaces (looks cleaner)
+        #
+        # data = "<whatever ></whatever ><whatever />"
+
+        data.gsub!(/\<(\/*\w+)\s*(\/*)>/o) do
+            "<#{$1}#{$2}>"
+        end
+
+        # remove funny ampersands
+        #
+        # data = "<x> B&W </x>"
+
+        data.gsub!(/\&([^\<\>\&]*?)\;/mo) do
+            "<entity name='#{$1}'/>"
+        end
+        data.gsub!(/\&/o) do
+            doxmlreport("&amp;",verbose)
+        end
+        data.gsub!(/\<entity name=\'(.*?)\'\/\>/o) do
+            doxmlreport("&#{$1};",verbose)
+        end
+
+        # remove funny < >
+        #
+        # data = "<x> < 5% </x>"
+
+        data.gsub!(/<([^>].*?)>/o) do
+            tag = $1
+            case tag
+                when /^\//o then
+                    "<#{tag}>" # funny tag but ok
+                when /\/$/o then
+                    "<#{tag}>" # funny tag but ok
+                when /</o then
+                    doxmlreport("&lt;#{tag}>",verbose)
+                else
+                    "<#{tag}>"
+            end
+        end
+
+        # remove funny < >
+        #
+        # data = "<x> > 5% </x>"
+
+        data.gsub!(/<([^>].*?)>([^\>\<]*?)>/o) do
+            doxmlreport("<#{$1}>#{$2}&gt;",verbose)
+        end
+
+        return data
+    end
+
+    # puts doxmlcleanup("<whatever ></whatever ><whatever />")
+    # puts doxmlcleanup("<x> B&W </x>")
+    # puts doxmlcleanup("<x> < 5% </x>")
+    # puts doxmlcleanup("<x> > 5% </x>")
+
+end
+
+logger      = Logger.new(banner.shift)
+commandline = CommandLine.new
+
+commandline.registeraction('dir',     'generate directory listing')
+commandline.registeraction('mmlpages','generate graphic from mathml')
+commandline.registeraction('analyze', 'report entities and elements [--utf --process]')
+commandline.registeraction('cleanup', 'cleanup xml file [--force]')
+commandline.registeraction('enhance', 'enhance xml file (partial)')
+commandline.registeraction('filter', 'filter elements from xml file [element=]')
+
+# commandline.registeraction('dir',     'filename --pattern= --output= [--recurse --stripname --longname --url --root]')
+# commandline.registeraction('mmlpages','filename [--eps --jpg --png --style= --mode=]')
+
+commandline.registeraction('ls')
+
+commandline.registeraction('help')
+commandline.registeraction('version')
+
+commandline.registerflag('stripname')
+commandline.registerflag('longname')
+commandline.registerflag('recurse')
+commandline.registerflag('verbose')
+
+commandline.registervalue('pattern')
+commandline.registervalue('element')
+commandline.registervalue('url')
+commandline.registervalue('output')
+commandline.registervalue('root')
+
+commandline.registerflag('eps')
+commandline.registerflag('png')
+commandline.registerflag('jpg')
+commandline.registerflag('utf')
+commandline.registerflag('process')
+commandline.registervalue('style')
+commandline.registervalue('modes')
+
+commandline.expand
+
+Commands.new(commandline,logger,banner).send(commandline.action || 'help')
diff --git a/scripts/context/stubs/mswin/context.exe b/scripts/context/stubs/mswin/context.exe
new file mode 100644
index 000000000..2d45f2749
Binary files /dev/null and b/scripts/context/stubs/mswin/context.exe differ
diff --git a/scripts/context/stubs/mswin/luatools.exe b/scripts/context/stubs/mswin/luatools.exe
new file mode 100644
index 000000000..2d45f2749
Binary files /dev/null and b/scripts/context/stubs/mswin/luatools.exe differ
diff --git a/scripts/context/stubs/mswin/luatools.lua b/scripts/context/stubs/mswin/luatools.lua
new file mode 100644
index 000000000..1d87322c1
--- /dev/null
+++ b/scripts/context/stubs/mswin/luatools.lua
@@ -0,0 +1,8185 @@
+#!/usr/bin/env texlua
+
+if not modules then modules = { } end modules ['luatools'] = {
+    version   = 1.001,
+    comment   = "companion to context.tex",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format = string.format
+
+-- one can make a stub:
+--
+-- #!/bin/sh
+-- env LUATEXDIR=/....../texmf/scripts/context/lua texlua luatools.lua "$@"
+
+-- Although this script is part of the ConTeXt distribution it is
+-- relatively indepent of ConTeXt.  The same is true for some of
+-- the luat files. We may may make them even less dependent in
+-- the future. As long as Luatex is under development the
+-- interfaces and names of functions may change.
+
+-- For the sake of independence we optionally can merge the library
+-- code here. It's too much code, but that does not harm. Much of the
+-- library code is used elsewhere. We don't want dependencies on
+-- Lua library paths simply because these scripts are located in the
+-- texmf tree and not in some Lua path. Normally this merge is not
+-- needed when texmfstart is used, or when the proper stub is used or
+-- when (windows) suffix binding is active.
+
+texlua = true
+
+-- begin library merge
+
+
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-string'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local sub, gsub, find, match, gmatch, format, char, byte, rep, lower = string.sub, string.gsub, string.find, string.match, string.gmatch, string.format, string.char, string.byte, string.rep, string.lower
+local lpegmatch = lpeg.match
+
+-- some functions may disappear as they are not used anywhere
+
+if not string.split then
+
+    -- this will be overloaded by a faster lpeg variant
+
+    function string:split(pattern)
+        if #self > 0 then
+            local t = { }
+            for s in gmatch(self..pattern,"(.-)"..pattern) do
+                t[#t+1] = s
+            end
+            return t
+        else
+            return { }
+        end
+    end
+
+end
+
+local chr_to_esc = {
+    ["%"] = "%%",
+    ["."] = "%.",
+    ["+"] = "%+", ["-"] = "%-", ["*"] = "%*",
+    ["^"] = "%^", ["$"] = "%$",
+    ["["] = "%[", ["]"] = "%]",
+    ["("] = "%(", [")"] = "%)",
+    ["{"] = "%{", ["}"] = "%}"
+}
+
+string.chr_to_esc = chr_to_esc
+
+function string:esc() -- variant 2
+    return (gsub(self,"(.)",chr_to_esc))
+end
+
+function string:unquote()
+    return (gsub(self,"^([\"\'])(.*)%1$","%2"))
+end
+
+--~ function string:unquote()
+--~     if find(self,"^[\'\"]") then
+--~         return sub(self,2,-2)
+--~     else
+--~         return self
+--~     end
+--~ end
+
+function string:quote() -- we could use format("%q")
+    return format("%q",self)
+end
+
+function string:count(pattern) -- variant 3
+    local n = 0
+    for _ in gmatch(self,pattern) do
+        n = n + 1
+    end
+    return n
+end
+
+function string:limit(n,sentinel)
+    if #self > n then
+        sentinel = sentinel or " ..."
+        return sub(self,1,(n-#sentinel)) .. sentinel
+    else
+        return self
+    end
+end
+
+--~ function string:strip() -- the .- is quite efficient
+--~  -- return match(self,"^%s*(.-)%s*$") or ""
+--~  -- return match(self,'^%s*(.*%S)') or '' -- posted on lua list
+--~     return find(s,'^%s*$') and '' or match(s,'^%s*(.*%S)')
+--~ end
+
+do -- roberto's variant:
+    local space    = lpeg.S(" \t\v\n")
+    local nospace  = 1 - space
+    local stripper = space^0 * lpeg.C((space^0 * nospace^1)^0)
+    function string.strip(str)
+        return lpegmatch(stripper,str) or ""
+    end
+end
+
+function string:is_empty()
+    return not find(self,"%S")
+end
+
+function string:enhance(pattern,action)
+    local ok, n = true, 0
+    while ok do
+        ok = false
+        self = gsub(self,pattern, function(...)
+            ok, n = true, n + 1
+            return action(...)
+        end)
+    end
+    return self, n
+end
+
+local chr_to_hex, hex_to_chr = { }, { }
+
+for i=0,255 do
+    local c, h = char(i), format("%02X",i)
+    chr_to_hex[c], hex_to_chr[h] = h, c
+end
+
+function string:to_hex()
+    return (gsub(self or "","(.)",chr_to_hex))
+end
+
+function string:from_hex()
+    return (gsub(self or "","(..)",hex_to_chr))
+end
+
+if not string.characters then
+
+    local function nextchar(str, index)
+        index = index + 1
+        return (index <= #str) and index or nil, sub(str,index,index)
+    end
+    function string:characters()
+        return nextchar, self, 0
+    end
+    local function nextbyte(str, index)
+        index = index + 1
+        return (index <= #str) and index or nil, byte(sub(str,index,index))
+    end
+    function string:bytes()
+        return nextbyte, self, 0
+    end
+
+end
+
+-- we can use format for this (neg n)
+
+function string:rpadd(n,chr)
+    local m = n-#self
+    if m > 0 then
+        return self .. rep(chr or " ",m)
+    else
+        return self
+    end
+end
+
+function string:lpadd(n,chr)
+    local m = n-#self
+    if m > 0 then
+        return rep(chr or " ",m) .. self
+    else
+        return self
+    end
+end
+
+string.padd = string.rpadd
+
+function is_number(str) -- tonumber
+    return find(str,"^[%-%+]?[%d]-%.?[%d+]$") == 1
+end
+
+--~ print(is_number("1"))
+--~ print(is_number("1.1"))
+--~ print(is_number(".1"))
+--~ print(is_number("-0.1"))
+--~ print(is_number("+0.1"))
+--~ print(is_number("-.1"))
+--~ print(is_number("+.1"))
+
+function string:split_settings() -- no {} handling, see l-aux for lpeg variant
+    if find(self,"=") then
+        local t = { }
+        for k,v in gmatch(self,"(%a+)=([^%,]*)") do
+            t[k] = v
+        end
+        return t
+    else
+        return nil
+    end
+end
+
+local patterns_escapes = {
+    ["-"] = "%-",
+    ["."] = "%.",
+    ["+"] = "%+",
+    ["*"] = "%*",
+    ["%"] = "%%",
+    ["("] = "%)",
+    [")"] = "%)",
+    ["["] = "%[",
+    ["]"] = "%]",
+}
+
+function string:pattesc()
+    return (gsub(self,".",patterns_escapes))
+end
+
+local simple_escapes = {
+    ["-"] = "%-",
+    ["."] = "%.",
+    ["?"] = ".",
+    ["*"] = ".*",
+}
+
+function string:simpleesc()
+    return (gsub(self,".",simple_escapes))
+end
+
+function string:tohash()
+    local t = { }
+    for s in gmatch(self,"([^, ]+)") do -- lpeg
+        t[s] = true
+    end
+    return t
+end
+
+local pattern = lpeg.Ct(lpeg.C(1)^0)
+
+function string:totable()
+    return lpegmatch(pattern,self)
+end
+
+--~ local t = {
+--~     "1234567123456712345671234567",
+--~     "a\tb\tc",
+--~     "aa\tbb\tcc",
+--~     "aaa\tbbb\tccc",
+--~     "aaaa\tbbbb\tcccc",
+--~     "aaaaa\tbbbbb\tccccc",
+--~     "aaaaaa\tbbbbbb\tcccccc",
+--~ }
+--~ for k,v do
+--~     print(string.tabtospace(t[k]))
+--~ end
+
+function string.tabtospace(str,tab)
+    -- we don't handle embedded newlines
+    while true do
+        local s = find(str,"\t")
+        if s then
+            if not tab then tab = 7 end -- only when found
+            local d = tab-(s-1) % tab
+            if d > 0 then
+                str = gsub(str,"\t",rep(" ",d),1)
+            else
+                str = gsub(str,"\t","",1)
+            end
+        else
+            break
+        end
+    end
+    return str
+end
+
+function string:compactlong() -- strips newlines and leading spaces
+    self = gsub(self,"[\n\r]+ *","")
+    self = gsub(self,"^ *","")
+    return self
+end
+
+function string:striplong() -- strips newlines and leading spaces
+    self = gsub(self,"^%s*","")
+    self = gsub(self,"[\n\r]+ *","\n")
+    return self
+end
+
+function string:topattern(lowercase,strict)
+    if lowercase then
+        self = lower(self)
+    end
+    self = gsub(self,".",simple_escapes)
+    if self == "" then
+        self = ".*"
+    elseif strict then
+        self = "^" .. self .. "$"
+    end
+    return self
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-lpeg'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local lpeg = require("lpeg")
+
+lpeg.patterns  = lpeg.patterns or { } -- so that we can share
+local patterns = lpeg.patterns
+
+local P, R, S, Ct, C, Cs, Cc, V = lpeg.P, lpeg.R, lpeg.S, lpeg.Ct, lpeg.C, lpeg.Cs, lpeg.Cc, lpeg.V
+local match = lpeg.match
+
+local digit, sign      = R('09'), S('+-')
+local cr, lf, crlf     = P("\r"), P("\n"), P("\r\n")
+local utf8byte         = R("\128\191")
+
+patterns.utf8byte      = utf8byte
+patterns.utf8one       = R("\000\127")
+patterns.utf8two       = R("\194\223") * utf8byte
+patterns.utf8three     = R("\224\239") * utf8byte * utf8byte
+patterns.utf8four      = R("\240\244") * utf8byte * utf8byte * utf8byte
+
+patterns.digit         = digit
+patterns.sign          = sign
+patterns.cardinal      = sign^0 * digit^1
+patterns.integer       = sign^0 * digit^1
+patterns.float         = sign^0 * digit^0 * P('.') * digit^1
+patterns.number        = patterns.float + patterns.integer
+patterns.oct           = P("0") * R("07")^1
+patterns.octal         = patterns.oct
+patterns.HEX           = P("0x") * R("09","AF")^1
+patterns.hex           = P("0x") * R("09","af")^1
+patterns.hexadecimal   = P("0x") * R("09","AF","af")^1
+patterns.lowercase     = R("az")
+patterns.uppercase     = R("AZ")
+patterns.letter        = patterns.lowercase + patterns.uppercase
+patterns.space         = S(" ")
+patterns.eol           = S("\n\r")
+patterns.spacer        = S(" \t\f\v")  -- + string.char(0xc2, 0xa0) if we want utf (cf mail roberto)
+patterns.newline       = crlf + cr + lf
+patterns.nonspace      = 1 - patterns.space
+patterns.nonspacer     = 1 - patterns.spacer
+patterns.whitespace    = patterns.eol + patterns.spacer
+patterns.nonwhitespace = 1 - patterns.whitespace
+patterns.utf8          = patterns.utf8one + patterns.utf8two + patterns.utf8three + patterns.utf8four
+patterns.utfbom        = P('\000\000\254\255') + P('\255\254\000\000') + P('\255\254') + P('\254\255') + P('\239\187\191')
+
+function lpeg.anywhere(pattern) --slightly adapted from website
+    return P { P(pattern) + 1 * V(1) } -- why so complex?
+end
+
+function lpeg.splitter(pattern, action)
+    return (((1-P(pattern))^1)/action+1)^0
+end
+
+local spacing  = patterns.spacer^0 * patterns.newline -- sort of strip
+local empty    = spacing * Cc("")
+local nonempty = Cs((1-spacing)^1) * spacing^-1
+local content  = (empty + nonempty)^1
+
+local capture = Ct(content^0)
+
+function string:splitlines()
+    return match(capture,self)
+end
+
+patterns.textline = content
+
+--~ local p = lpeg.splitat("->",false)  print(match(p,"oeps->what->more"))  -- oeps what more
+--~ local p = lpeg.splitat("->",true)   print(match(p,"oeps->what->more"))  -- oeps what->more
+--~ local p = lpeg.splitat("->",false)  print(match(p,"oeps"))              -- oeps
+--~ local p = lpeg.splitat("->",true)   print(match(p,"oeps"))              -- oeps
+
+local splitters_s, splitters_m = { }, { }
+
+local function splitat(separator,single)
+    local splitter = (single and splitters_s[separator]) or splitters_m[separator]
+    if not splitter then
+        separator = P(separator)
+        if single then
+            local other, any = C((1 - separator)^0), P(1)
+            splitter = other * (separator * C(any^0) + "") -- ?
+            splitters_s[separator] = splitter
+        else
+            local other = C((1 - separator)^0)
+            splitter = other * (separator * other)^0
+            splitters_m[separator] = splitter
+        end
+    end
+    return splitter
+end
+
+lpeg.splitat = splitat
+
+local cache = { }
+
+function lpeg.split(separator,str)
+    local c = cache[separator]
+    if not c then
+        c = Ct(splitat(separator))
+        cache[separator] = c
+    end
+    return match(c,str)
+end
+
+function string:split(separator)
+    local c = cache[separator]
+    if not c then
+        c = Ct(splitat(separator))
+        cache[separator] = c
+    end
+    return match(c,self)
+end
+
+lpeg.splitters = cache
+
+local cache = { }
+
+function lpeg.checkedsplit(separator,str)
+    local c = cache[separator]
+    if not c then
+        separator = P(separator)
+        local other = C((1 - separator)^0)
+        c = Ct(separator^0 * other * (separator^1 * other)^0)
+        cache[separator] = c
+    end
+    return match(c,str)
+end
+
+function string:checkedsplit(separator)
+    local c = cache[separator]
+    if not c then
+        separator = P(separator)
+        local other = C((1 - separator)^0)
+        c = Ct(separator^0 * other * (separator^1 * other)^0)
+        cache[separator] = c
+    end
+    return match(c,self)
+end
+
+--~ function lpeg.append(list,pp)
+--~     local p = pp
+--~     for l=1,#list do
+--~         if p then
+--~             p = p + P(list[l])
+--~         else
+--~             p = P(list[l])
+--~         end
+--~     end
+--~     return p
+--~ end
+
+--~ from roberto's site:
+
+local f1 = string.byte
+
+local function f2(s) local c1, c2         = f1(s,1,2) return   c1 * 64 + c2                       -    12416 end
+local function f3(s) local c1, c2, c3     = f1(s,1,3) return  (c1 * 64 + c2) * 64 + c3            -   925824 end
+local function f4(s) local c1, c2, c3, c4 = f1(s,1,4) return ((c1 * 64 + c2) * 64 + c3) * 64 + c4 - 63447168 end
+
+patterns.utf8byte = patterns.utf8one/f1 + patterns.utf8two/f2 + patterns.utf8three/f3 + patterns.utf8four/f4
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-table'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+table.join = table.concat
+
+local concat, sort, insert, remove = table.concat, table.sort, table.insert, table.remove
+local format, find, gsub, lower, dump, match = string.format, string.find, string.gsub, string.lower, string.dump, string.match
+local getmetatable, setmetatable = getmetatable, setmetatable
+local type, next, tostring, tonumber, ipairs = type, next, tostring, tonumber, ipairs
+local unpack = unpack or table.unpack
+
+function table.strip(tab)
+    local lst = { }
+    for i=1,#tab do
+        local s = gsub(tab[i],"^%s*(.-)%s*$","%1")
+        if s == "" then
+            -- skip this one
+        else
+            lst[#lst+1] = s
+        end
+    end
+    return lst
+end
+
+function table.keys(t)
+    local k = { }
+    for key, _ in next, t do
+        k[#k+1] = key
+    end
+    return k
+end
+
+local function compare(a,b)
+    return (tostring(a) < tostring(b))
+end
+
+local function sortedkeys(tab)
+    local srt, kind = { }, 0 -- 0=unknown 1=string, 2=number 3=mixed
+    for key,_ in next, tab do
+        srt[#srt+1] = key
+        if kind == 3 then
+            -- no further check
+        else
+            local tkey = type(key)
+            if tkey == "string" then
+            --  if kind == 2 then kind = 3 else kind = 1 end
+                kind = (kind == 2 and 3) or 1
+            elseif tkey == "number" then
+            --  if kind == 1 then kind = 3 else kind = 2 end
+                kind = (kind == 1 and 3) or 2
+            else
+                kind = 3
+            end
+        end
+    end
+    if kind == 0 or kind == 3 then
+        sort(srt,compare)
+    else
+        sort(srt)
+    end
+    return srt
+end
+
+local function sortedhashkeys(tab) -- fast one
+    local srt = { }
+    for key,_ in next, tab do
+        srt[#srt+1] = key
+    end
+    sort(srt)
+    return srt
+end
+
+table.sortedkeys     = sortedkeys
+table.sortedhashkeys = sortedhashkeys
+
+function table.sortedhash(t)
+    local s = sortedhashkeys(t) -- maybe just sortedkeys
+    local n = 0
+    local function kv(s)
+        n = n + 1
+        local k = s[n]
+        return k, t[k]
+    end
+    return kv, s
+end
+
+table.sortedpairs = table.sortedhash
+
+function table.append(t, list)
+    for _,v in next, list do
+        insert(t,v)
+    end
+end
+
+function table.prepend(t, list)
+    for k,v in next, list do
+        insert(t,k,v)
+    end
+end
+
+function table.merge(t, ...) -- first one is target
+    t = t or {}
+    local lst = {...}
+    for i=1,#lst do
+        for k, v in next, lst[i] do
+            t[k] = v
+        end
+    end
+    return t
+end
+
+function table.merged(...)
+    local tmp, lst = { }, {...}
+    for i=1,#lst do
+        for k, v in next, lst[i] do
+            tmp[k] = v
+        end
+    end
+    return tmp
+end
+
+function table.imerge(t, ...)
+    local lst = {...}
+    for i=1,#lst do
+        local nst = lst[i]
+        for j=1,#nst do
+            t[#t+1] = nst[j]
+        end
+    end
+    return t
+end
+
+function table.imerged(...)
+    local tmp, lst = { }, {...}
+    for i=1,#lst do
+        local nst = lst[i]
+        for j=1,#nst do
+            tmp[#tmp+1] = nst[j]
+        end
+    end
+    return tmp
+end
+
+local function fastcopy(old) -- fast one
+    if old then
+        local new = { }
+        for k,v in next, old do
+            if type(v) == "table" then
+                new[k] = fastcopy(v) -- was just table.copy
+            else
+                new[k] = v
+            end
+        end
+        -- optional second arg
+        local mt = getmetatable(old)
+        if mt then
+            setmetatable(new,mt)
+        end
+        return new
+    else
+        return { }
+    end
+end
+
+local function copy(t, tables) -- taken from lua wiki, slightly adapted
+    tables = tables or { }
+    local tcopy = {}
+    if not tables[t] then
+        tables[t] = tcopy
+    end
+    for i,v in next, t do -- brrr, what happens with sparse indexed
+        if type(i) == "table" then
+            if tables[i] then
+                i = tables[i]
+            else
+                i = copy(i, tables)
+            end
+        end
+        if type(v) ~= "table" then
+            tcopy[i] = v
+        elseif tables[v] then
+            tcopy[i] = tables[v]
+        else
+            tcopy[i] = copy(v, tables)
+        end
+    end
+    local mt = getmetatable(t)
+    if mt then
+        setmetatable(tcopy,mt)
+    end
+    return tcopy
+end
+
+table.fastcopy = fastcopy
+table.copy     = copy
+
+-- rougly: copy-loop : unpack : sub == 0.9 : 0.4 : 0.45 (so in critical apps, use unpack)
+
+function table.sub(t,i,j)
+    return { unpack(t,i,j) }
+end
+
+function table.replace(a,b)
+    for k,v in next, b do
+        a[k] = v
+    end
+end
+
+-- slower than #t on indexed tables (#t only returns the size of the numerically indexed slice)
+
+function table.is_empty(t) -- obolete, use inline code instead
+    return not t or not next(t)
+end
+
+function table.one_entry(t) -- obolete, use inline code instead
+    local n = next(t)
+    return n and not next(t,n)
+end
+
+--~ function table.starts_at(t) -- obsolete, not nice
+--~     return ipairs(t,1)(t,0)
+--~ end
+
+function table.tohash(t,value)
+    local h = { }
+    if t then
+        if value == nil then value = true end
+        for _, v in next, t do -- no ipairs here
+            h[v] = value
+        end
+    end
+    return h
+end
+
+function table.fromhash(t)
+    local h = { }
+    for k, v in next, t do -- no ipairs here
+        if v then h[#h+1] = k end
+    end
+    return h
+end
+
+--~ print(table.serialize(t), "\n")
+--~ print(table.serialize(t,"name"), "\n")
+--~ print(table.serialize(t,false), "\n")
+--~ print(table.serialize(t,true), "\n")
+--~ print(table.serialize(t,"name",true), "\n")
+--~ print(table.serialize(t,"name",true,true), "\n")
+
+table.serialize_functions = true
+table.serialize_compact   = true
+table.serialize_inline    = true
+
+local noquotes, hexify, handle, reduce, compact, inline, functions
+
+local reserved = table.tohash { -- intercept a language flaw, no reserved words as key
+    'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'if',
+    'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while',
+}
+
+local function simple_table(t)
+    if #t > 0 then
+        local n = 0
+        for _,v in next, t do
+            n = n + 1
+        end
+        if n == #t then
+            local tt = { }
+            for i=1,#t do
+                local v = t[i]
+                local tv = type(v)
+                if tv == "number" then
+                    if hexify then
+                        tt[#tt+1] = format("0x%04X",v)
+                    else
+                        tt[#tt+1] = tostring(v) -- tostring not needed
+                    end
+                elseif tv == "boolean" then
+                    tt[#tt+1] = tostring(v)
+                elseif tv == "string" then
+                    tt[#tt+1] = format("%q",v)
+                else
+                    tt = nil
+                    break
+                end
+            end
+            return tt
+        end
+    end
+    return nil
+end
+
+-- Because this is a core function of mkiv I moved some function calls
+-- inline.
+--
+-- twice as fast in a test:
+--
+-- local propername = lpeg.P(lpeg.R("AZ","az","__") * lpeg.R("09","AZ","az", "__")^0 * lpeg.P(-1) )
+
+-- problem: there no good number_to_string converter with the best resolution
+
+local function do_serialize(root,name,depth,level,indexed)
+    if level > 0 then
+        depth = depth .. " "
+        if indexed then
+            handle(format("%s{",depth))
+        elseif name then
+        --~ handle(format("%s%s={",depth,key(name)))
+            if type(name) == "number" then -- or find(k,"^%d+$") then
+                if hexify then
+                    handle(format("%s[0x%04X]={",depth,name))
+                else
+                    handle(format("%s[%s]={",depth,name))
+                end
+            elseif noquotes and not reserved[name] and find(name,"^%a[%w%_]*$") then
+                handle(format("%s%s={",depth,name))
+            else
+                handle(format("%s[%q]={",depth,name))
+            end
+        else
+            handle(format("%s{",depth))
+        end
+    end
+    -- we could check for k (index) being number (cardinal)
+    if root and next(root) then
+        local first, last = nil, 0 -- #root cannot be trusted here (will be ok in 5.2 when ipairs is gone)
+        if compact then
+            -- NOT: for k=1,#root do (we need to quit at nil)
+            for k,v in ipairs(root) do -- can we use next?
+                if not first then first = k end
+                last = last + 1
+            end
+        end
+        local sk = sortedkeys(root)
+        for i=1,#sk do
+            local k = sk[i]
+            local v = root[k]
+            --~ if v == root then
+                -- circular
+            --~ else
+            local t = type(v)
+            if compact and first and type(k) == "number" and k >= first and k <= last then
+                if t == "number" then
+                    if hexify then
+                        handle(format("%s 0x%04X,",depth,v))
+                    else
+                        handle(format("%s %s,",depth,v)) -- %.99g
+                    end
+                elseif t == "string" then
+                    if reduce and tonumber(v) then
+                        handle(format("%s %s,",depth,v))
+                    else
+                        handle(format("%s %q,",depth,v))
+                    end
+                elseif t == "table" then
+                    if not next(v) then
+                        handle(format("%s {},",depth))
+                    elseif inline then -- and #t > 0
+                        local st = simple_table(v)
+                        if st then
+                            handle(format("%s { %s },",depth,concat(st,", ")))
+                        else
+                            do_serialize(v,k,depth,level+1,true)
+                        end
+                    else
+                        do_serialize(v,k,depth,level+1,true)
+                    end
+                elseif t == "boolean" then
+                    handle(format("%s %s,",depth,tostring(v)))
+                elseif t == "function" then
+                    if functions then
+                        handle(format('%s loadstring(%q),',depth,dump(v)))
+                    else
+                        handle(format('%s "function",',depth))
+                    end
+                else
+                    handle(format("%s %q,",depth,tostring(v)))
+                end
+            elseif k == "__p__" then -- parent
+                if false then
+                    handle(format("%s __p__=nil,",depth))
+                end
+            elseif t == "number" then
+            --~ if hexify then
+            --~     handle(format("%s %s=0x%04X,",depth,key(k),v))
+            --~ else
+            --~     handle(format("%s %s=%s,",depth,key(k),v)) -- %.99g
+            --~ end
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s [%s]=%s,",depth,k,v)) -- %.99g
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    if hexify then
+                        handle(format("%s %s=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s %s=%s,",depth,k,v)) -- %.99g
+                    end
+                else
+                    if hexify then
+                        handle(format("%s [%q]=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%s,",depth,k,v)) -- %.99g
+                    end
+                end
+            elseif t == "string" then
+                if reduce and tonumber(v) then
+                --~ handle(format("%s %s=%s,",depth,key(k),v))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=%s,",depth,k,v))
+                        else
+                            handle(format("%s [%s]=%s,",depth,k,v))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=%s,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%s,",depth,k,v))
+                    end
+                else
+                --~ handle(format("%s %s=%q,",depth,key(k),v))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=%q,",depth,k,v))
+                        else
+                            handle(format("%s [%s]=%q,",depth,k,v))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=%q,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%q,",depth,k,v))
+                    end
+                end
+            elseif t == "table" then
+                if not next(v) then
+                    --~ handle(format("%s %s={},",depth,key(k)))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]={},",depth,k))
+                        else
+                            handle(format("%s [%s]={},",depth,k))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s={},",depth,k))
+                    else
+                        handle(format("%s [%q]={},",depth,k))
+                    end
+                elseif inline then
+                    local st = simple_table(v)
+                    if st then
+                    --~ handle(format("%s %s={ %s },",depth,key(k),concat(st,", ")))
+                        if type(k) == "number" then -- or find(k,"^%d+$") then
+                            if hexify then
+                                handle(format("%s [0x%04X]={ %s },",depth,k,concat(st,", ")))
+                            else
+                                handle(format("%s [%s]={ %s },",depth,k,concat(st,", ")))
+                            end
+                        elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                            handle(format("%s %s={ %s },",depth,k,concat(st,", ")))
+                        else
+                            handle(format("%s [%q]={ %s },",depth,k,concat(st,", ")))
+                        end
+                    else
+                        do_serialize(v,k,depth,level+1)
+                    end
+                else
+                    do_serialize(v,k,depth,level+1)
+                end
+            elseif t == "boolean" then
+            --~ handle(format("%s %s=%s,",depth,key(k),tostring(v)))
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=%s,",depth,k,tostring(v)))
+                    else
+                        handle(format("%s [%s]=%s,",depth,k,tostring(v)))
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    handle(format("%s %s=%s,",depth,k,tostring(v)))
+                else
+                    handle(format("%s [%q]=%s,",depth,k,tostring(v)))
+                end
+            elseif t == "function" then
+                if functions then
+                    --~ handle(format('%s %s=loadstring(%q),',depth,key(k),dump(v)))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=loadstring(%q),",depth,k,dump(v)))
+                        else
+                            handle(format("%s [%s]=loadstring(%q),",depth,k,dump(v)))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=loadstring(%q),",depth,k,dump(v)))
+                    else
+                        handle(format("%s [%q]=loadstring(%q),",depth,k,dump(v)))
+                    end
+                end
+            else
+                --~ handle(format("%s %s=%q,",depth,key(k),tostring(v)))
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=%q,",depth,k,tostring(v)))
+                    else
+                        handle(format("%s [%s]=%q,",depth,k,tostring(v)))
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    handle(format("%s %s=%q,",depth,k,tostring(v)))
+                else
+                    handle(format("%s [%q]=%q,",depth,k,tostring(v)))
+                end
+            end
+            --~ end
+        end
+    end
+   if level > 0 then
+        handle(format("%s},",depth))
+    end
+end
+
+-- replacing handle by a direct t[#t+1] = ... (plus test) is not much
+-- faster (0.03 on 1.00 for zapfino.tma)
+
+local function serialize(root,name,_handle,_reduce,_noquotes,_hexify)
+    noquotes = _noquotes
+    hexify = _hexify
+    handle = _handle or print
+    reduce = _reduce or false
+    compact = table.serialize_compact
+    inline  = compact and table.serialize_inline
+    functions = table.serialize_functions
+    local tname = type(name)
+    if tname == "string" then
+        if name == "return" then
+            handle("return {")
+        else
+            handle(name .. "={")
+        end
+    elseif tname == "number" then
+        if hexify then
+            handle(format("[0x%04X]={",name))
+        else
+            handle("[" .. name .. "]={")
+        end
+    elseif tname == "boolean" then
+        if name then
+            handle("return {")
+        else
+            handle("{")
+        end
+    else
+        handle("t={")
+    end
+    if root and next(root) then
+        do_serialize(root,name,"",0,indexed)
+    end
+    handle("}")
+end
+
+--~ name:
+--~
+--~ true     : return     { }
+--~ false    :            { }
+--~ nil      : t        = { }
+--~ string   : string   = { }
+--~ 'return' : return     { }
+--~ number   : [number] = { }
+
+function table.serialize(root,name,reduce,noquotes,hexify)
+    local t = { }
+    local function flush(s)
+        t[#t+1] = s
+    end
+    serialize(root,name,flush,reduce,noquotes,hexify)
+    return concat(t,"\n")
+end
+
+function table.tohandle(handle,root,name,reduce,noquotes,hexify)
+    serialize(root,name,handle,reduce,noquotes,hexify)
+end
+
+-- sometimes tables are real use (zapfino extra pro is some 85M) in which
+-- case a stepwise serialization is nice; actually, we could consider:
+--
+-- for line in table.serializer(root,name,reduce,noquotes) do
+--    ...(line)
+-- end
+--
+-- so this is on the todo list
+
+table.tofile_maxtab = 2*1024
+
+function table.tofile(filename,root,name,reduce,noquotes,hexify)
+    local f = io.open(filename,'w')
+    if f then
+        local maxtab = table.tofile_maxtab
+        if maxtab > 1 then
+            local t = { }
+            local function flush(s)
+                t[#t+1] = s
+                if #t > maxtab then
+                    f:write(concat(t,"\n"),"\n") -- hm, write(sometable) should be nice
+                    t = { }
+                end
+            end
+            serialize(root,name,flush,reduce,noquotes,hexify)
+            f:write(concat(t,"\n"),"\n")
+        else
+            local function flush(s)
+                f:write(s,"\n")
+            end
+            serialize(root,name,flush,reduce,noquotes,hexify)
+        end
+        f:close()
+    end
+end
+
+local function flatten(t,f,complete) -- is this used? meybe a variant with next, ...
+    for i=1,#t do
+        local v = t[i]
+        if type(v) == "table" then
+            if complete or type(v[1]) == "table" then
+                flatten(v,f,complete)
+            else
+                f[#f+1] = v
+            end
+        else
+            f[#f+1] = v
+        end
+    end
+end
+
+function table.flatten(t)
+    local f = { }
+    flatten(t,f,true)
+    return f
+end
+
+function table.unnest(t) -- bad name
+    local f = { }
+    flatten(t,f,false)
+    return f
+end
+
+table.flatten_one_level = table.unnest
+
+-- a better one:
+
+local function flattened(t,f)
+    if not f then
+        f = { }
+    end
+    for k, v in next, t do
+        if type(v) == "table" then
+            flattened(v,f)
+        else
+            f[k] = v
+        end
+    end
+    return f
+end
+
+table.flattened = flattened
+
+-- the next three may disappear
+
+function table.remove_value(t,value) -- todo: n
+    if value then
+        for i=1,#t do
+            if t[i] == value then
+                remove(t,i)
+                -- remove all, so no: return
+            end
+        end
+    end
+end
+
+function table.insert_before_value(t,value,str)
+    if str then
+        if value then
+            for i=1,#t do
+                if t[i] == value then
+                    insert(t,i,str)
+                    return
+                end
+            end
+        end
+        insert(t,1,str)
+    elseif value then
+        insert(t,1,value)
+    end
+end
+
+function table.insert_after_value(t,value,str)
+    if str then
+        if value then
+            for i=1,#t do
+                if t[i] == value then
+                    insert(t,i+1,str)
+                    return
+                end
+            end
+        end
+        t[#t+1] = str
+    elseif value then
+        t[#t+1] = value
+    end
+end
+
+local function are_equal(a,b,n,m) -- indexed
+    if a and b and #a == #b then
+        n = n or 1
+        m = m or #a
+        for i=n,m do
+            local ai, bi = a[i], b[i]
+            if ai==bi then
+                -- same
+            elseif type(ai)=="table" and type(bi)=="table" then
+                if not are_equal(ai,bi) then
+                    return false
+                end
+            else
+                return false
+            end
+        end
+        return true
+    else
+        return false
+    end
+end
+
+local function identical(a,b) -- assumes same structure
+    for ka, va in next, a do
+        local vb = b[k]
+        if va == vb then
+            -- same
+        elseif type(va) == "table" and  type(vb) == "table" then
+            if not identical(va,vb) then
+                return false
+            end
+        else
+            return false
+        end
+    end
+    return true
+end
+
+table.are_equal = are_equal
+table.identical = identical
+
+-- maybe also make a combined one
+
+function table.compact(t)
+    if t then
+        for k,v in next, t do
+            if not next(v) then
+                t[k] = nil
+            end
+        end
+    end
+end
+
+function table.contains(t, v)
+    if t then
+        for i=1, #t do
+            if t[i] == v then
+                return i
+            end
+        end
+    end
+    return false
+end
+
+function table.count(t)
+    local n, e = 0, next(t)
+    while e do
+        n, e = n + 1, next(t,e)
+    end
+    return n
+end
+
+function table.swapped(t)
+    local s = { }
+    for k, v in next, t do
+        s[v] = k
+    end
+    return s
+end
+
+--~ function table.are_equal(a,b)
+--~     return table.serialize(a) == table.serialize(b)
+--~ end
+
+function table.clone(t,p) -- t is optional or nil or table
+    if not p then
+        t, p = { }, t or { }
+    elseif not t then
+        t = { }
+    end
+    setmetatable(t, { __index = function(_,key) return p[key] end }) -- why not __index = p ?
+    return t
+end
+
+function table.hexed(t,seperator)
+    local tt = { }
+    for i=1,#t do tt[i] = format("0x%04X",t[i]) end
+    return concat(tt,seperator or " ")
+end
+
+function table.reverse_hash(h)
+    local r = { }
+    for k,v in next, h do
+        r[v] = lower(gsub(k," ",""))
+    end
+    return r
+end
+
+function table.reverse(t)
+    local tt = { }
+    if #t > 0 then
+        for i=#t,1,-1 do
+            tt[#tt+1] = t[i]
+        end
+    end
+    return tt
+end
+
+function table.insert_before_value(t,value,extra)
+    for i=1,#t do
+        if t[i] == extra then
+            remove(t,i)
+        end
+    end
+    for i=1,#t do
+        if t[i] == value then
+            insert(t,i,extra)
+            return
+        end
+    end
+    insert(t,1,extra)
+end
+
+function table.insert_after_value(t,value,extra)
+    for i=1,#t do
+        if t[i] == extra then
+            remove(t,i)
+        end
+    end
+    for i=1,#t do
+        if t[i] == value then
+            insert(t,i+1,extra)
+            return
+        end
+    end
+    insert(t,#t+1,extra)
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-io'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local byte, find, gsub = string.byte, string.find, string.gsub
+
+if string.find(os.getenv("PATH"),";") then
+    io.fileseparator, io.pathseparator = "\\", ";"
+else
+    io.fileseparator, io.pathseparator = "/" , ":"
+end
+
+function io.loaddata(filename,textmode)
+    local f = io.open(filename,(textmode and 'r') or 'rb')
+    if f then
+    --  collectgarbage("step") -- sometimes makes a big difference in mem consumption
+        local data = f:read('*all')
+    --  garbagecollector.check(data)
+        f:close()
+        return data
+    else
+        return nil
+    end
+end
+
+function io.savedata(filename,data,joiner)
+    local f = io.open(filename,"wb")
+    if f then
+        if type(data) == "table" then
+            f:write(table.join(data,joiner or ""))
+        elseif type(data) == "function" then
+            data(f)
+        else
+            f:write(data or "")
+        end
+        f:close()
+        return true
+    else
+        return false
+    end
+end
+
+function io.exists(filename)
+    local f = io.open(filename)
+    if f == nil then
+        return false
+    else
+        assert(f:close())
+        return true
+    end
+end
+
+function io.size(filename)
+    local f = io.open(filename)
+    if f == nil then
+        return 0
+    else
+        local s = f:seek("end")
+        assert(f:close())
+        return s
+    end
+end
+
+function io.noflines(f)
+    local n = 0
+    for _ in f:lines() do
+        n = n + 1
+    end
+    f:seek('set',0)
+    return n
+end
+
+local nextchar = {
+    [ 4] = function(f)
+        return f:read(1,1,1,1)
+    end,
+    [ 2] = function(f)
+        return f:read(1,1)
+    end,
+    [ 1] = function(f)
+        return f:read(1)
+    end,
+    [-2] = function(f)
+        local a, b = f:read(1,1)
+        return b, a
+    end,
+    [-4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        return d, c, b, a
+    end
+}
+
+function io.characters(f,n)
+    if f then
+        return nextchar[n or 1], f
+    else
+        return nil, nil
+    end
+end
+
+local nextbyte = {
+    [4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        if d then
+            return byte(a), byte(b), byte(c), byte(d)
+        else
+            return nil, nil, nil, nil
+        end
+    end,
+    [2] = function(f)
+        local a, b = f:read(1,1)
+        if b then
+            return byte(a), byte(b)
+        else
+            return nil, nil
+        end
+    end,
+    [1] = function (f)
+        local a = f:read(1)
+        if a then
+            return byte(a)
+        else
+            return nil
+        end
+    end,
+    [-2] = function (f)
+        local a, b = f:read(1,1)
+        if b then
+            return byte(b), byte(a)
+        else
+            return nil, nil
+        end
+    end,
+    [-4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        if d then
+            return byte(d), byte(c), byte(b), byte(a)
+        else
+            return nil, nil, nil, nil
+        end
+    end
+}
+
+function io.bytes(f,n)
+    if f then
+        return nextbyte[n or 1], f
+    else
+        return nil, nil
+    end
+end
+
+function io.ask(question,default,options)
+    while true do
+        io.write(question)
+        if options then
+            io.write(string.format(" [%s]",table.concat(options,"|")))
+        end
+        if default then
+            io.write(string.format(" [%s]",default))
+        end
+        io.write(string.format(" "))
+        local answer = io.read()
+        answer = gsub(answer,"^%s*(.*)%s*$","%1")
+        if answer == "" and default then
+            return default
+        elseif not options then
+            return answer
+        else
+            for k=1,#options do
+                if options[k] == answer then
+                    return answer
+                end
+            end
+            local pattern = "^" .. answer
+            for k=1,#options do
+                local v = options[k]
+                if find(v,pattern) then
+                    return v
+                end
+            end
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-number'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local tostring = tostring
+local format, floor, insert, match = string.format, math.floor, table.insert, string.match
+local lpegmatch = lpeg.match
+
+number = number or { }
+
+-- a,b,c,d,e,f = number.toset(100101)
+
+function number.toset(n)
+    return match(tostring(n),"(.?)(.?)(.?)(.?)(.?)(.?)(.?)(.?)")
+end
+
+function number.toevenhex(n)
+    local s = format("%X",n)
+    if #s % 2 == 0 then
+        return s
+    else
+        return "0" .. s
+    end
+end
+
+-- the lpeg way is slower on 8 digits, but faster on 4 digits, some 7.5%
+-- on
+--
+-- for i=1,1000000 do
+--     local a,b,c,d,e,f,g,h = number.toset(12345678)
+--     local a,b,c,d         = number.toset(1234)
+--     local a,b,c           = number.toset(123)
+-- end
+--
+-- of course dedicated "(.)(.)(.)(.)" matches are even faster
+
+local one = lpeg.C(1-lpeg.S(''))^1
+
+function number.toset(n)
+    return lpegmatch(one,tostring(n))
+end
+
+function number.bits(n,zero)
+    local t, i = { }, (zero and 0) or 1
+    while n > 0 do
+        local m = n % 2
+        if m > 0 then
+            insert(t,1,i)
+        end
+        n = floor(n/2)
+        i = i + 1
+    end
+    return t
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-set'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+set = set or { }
+
+local nums   = { }
+local tabs   = { }
+local concat = table.concat
+local next, type = next, type
+
+set.create = table.tohash
+
+function set.tonumber(t)
+    if next(t) then
+        local s = ""
+    --  we could save mem by sorting, but it slows down
+        for k, v in next, t do
+            if v then
+            --  why bother about the leading space
+                s = s .. " " .. k
+            end
+        end
+        local n = nums[s]
+        if not n then
+            n = #tabs + 1
+            tabs[n] = t
+            nums[s] = n
+        end
+        return n
+    else
+        return 0
+    end
+end
+
+function set.totable(n)
+    if n == 0 then
+        return { }
+    else
+        return tabs[n] or { }
+    end
+end
+
+function set.tolist(n)
+    if n == 0 or not tabs[n] then
+        return ""
+    else
+        local t = { }
+        for k, v in next, tabs[n] do
+            if v then
+                t[#t+1] = k
+            end
+        end
+        return concat(t," ")
+    end
+end
+
+function set.contains(n,s)
+    if type(n) == "table" then
+        return n[s]
+    elseif n == 0 then
+        return false
+    else
+        local t = tabs[n]
+        return t and t[s]
+    end
+end
+
+--~ local c = set.create{'aap','noot','mies'}
+--~ local s = set.tonumber(c)
+--~ local t = set.totable(s)
+--~ print(t['aap'])
+--~ local c = set.create{'zus','wim','jet'}
+--~ local s = set.tonumber(c)
+--~ local t = set.totable(s)
+--~ print(t['aap'])
+--~ print(t['jet'])
+--~ print(set.contains(t,'jet'))
+--~ print(set.contains(t,'aap'))
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-os'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- maybe build io.flush in os.execute
+
+local find, format, gsub = string.find, string.format, string.gsub
+local random, ceil = math.random, math.ceil
+
+local execute, spawn, exec, ioflush = os.execute, os.spawn or os.execute, os.exec or os.execute, io.flush
+
+function os.execute(...) ioflush() return execute(...) end
+function os.spawn  (...) ioflush() return spawn  (...) end
+function os.exec   (...) ioflush() return exec   (...) end
+
+function os.resultof(command)
+    ioflush() -- else messed up logging
+    local handle = io.popen(command,"r")
+    if not handle then
+    --  print("unknown command '".. command .. "' in os.resultof")
+        return ""
+    else
+        return handle:read("*all") or ""
+    end
+end
+
+--~ os.type     : windows | unix (new, we already guessed os.platform)
+--~ os.name     : windows | msdos | linux | macosx | solaris | .. | generic (new)
+--~ os.platform : extended os.name with architecture
+
+if not io.fileseparator then
+    if find(os.getenv("PATH"),";") then
+        io.fileseparator, io.pathseparator, os.type = "\\", ";", os.type or "mswin"
+    else
+        io.fileseparator, io.pathseparator, os.type = "/" , ":", os.type or "unix"
+    end
+end
+
+os.type = os.type or (io.pathseparator == ";"       and "windows") or "unix"
+os.name = os.name or (os.type          == "windows" and "mswin"  ) or "linux"
+
+if os.type == "windows" then
+    os.libsuffix, os.binsuffix = 'dll', 'exe'
+else
+    os.libsuffix, os.binsuffix = 'so', ''
+end
+
+function os.launch(str)
+    if os.type == "windows" then
+        os.execute("start " .. str) -- os.spawn ?
+    else
+        os.execute(str .. " &")     -- os.spawn ?
+    end
+end
+
+if not os.times then
+    -- utime  = user time
+    -- stime  = system time
+    -- cutime = children user time
+    -- cstime = children system time
+    function os.times()
+        return {
+            utime  = os.gettimeofday(), -- user
+            stime  = 0,                 -- system
+            cutime = 0,                 -- children user
+            cstime = 0,                 -- children system
+        }
+    end
+end
+
+os.gettimeofday = os.gettimeofday or os.clock
+
+local startuptime = os.gettimeofday()
+
+function os.runtime()
+    return os.gettimeofday() - startuptime
+end
+
+--~ print(os.gettimeofday()-os.time())
+--~ os.sleep(1.234)
+--~ print (">>",os.runtime())
+--~ print(os.date("%H:%M:%S",os.gettimeofday()))
+--~ print(os.date("%H:%M:%S",os.time()))
+
+-- no need for function anymore as we have more clever code and helpers now
+-- this metatable trickery might as well disappear
+
+os.resolvers = os.resolvers or { }
+
+local resolvers = os.resolvers
+
+local osmt = getmetatable(os) or { __index = function(t,k) t[k] = "unset" return "unset" end } -- maybe nil
+local osix = osmt.__index
+
+osmt.__index = function(t,k)
+    return (resolvers[k] or osix)(t,k)
+end
+
+setmetatable(os,osmt)
+
+if not os.setenv then
+
+    -- we still store them but they won't be seen in
+    -- child processes although we might pass them some day
+    -- using command concatination
+
+    local env, getenv = { }, os.getenv
+
+    function os.setenv(k,v)
+        env[k] = v
+    end
+
+    function os.getenv(k)
+        return env[k] or getenv(k)
+    end
+
+end
+
+-- we can use HOSTTYPE on some platforms
+
+local name, platform = os.name or "linux", os.getenv("MTX_PLATFORM") or ""
+
+local function guess()
+    local architecture = os.resultof("uname -m") or ""
+    if architecture ~= "" then
+        return architecture
+    end
+    architecture = os.getenv("HOSTTYPE") or ""
+    if architecture ~= "" then
+        return architecture
+    end
+    return os.resultof("echo $HOSTTYPE") or ""
+end
+
+if platform ~= "" then
+
+    os.platform = platform
+
+elseif os.type == "windows" then
+
+    -- we could set the variable directly, no function needed here
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.getenv("PROCESSOR_ARCHITECTURE") or ""
+        if find(architecture,"AMD64") then
+            platform = "mswin-64"
+        else
+            platform = "mswin"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "linux" then
+
+    function os.resolvers.platform(t,k)
+        -- we sometims have HOSTTYPE set so let's check that first
+        local platform, architecture = "", os.getenv("HOSTTYPE") or os.resultof("uname -m") or ""
+        if find(architecture,"x86_64") then
+            platform = "linux-64"
+        elseif find(architecture,"ppc") then
+            platform = "linux-ppc"
+        else
+            platform = "linux"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "macosx" then
+
+    --[[
+        Identifying the architecture of OSX is quite a mess and this
+        is the best we can come up with. For some reason $HOSTTYPE is
+        a kind of pseudo environment variable, not known to the current
+        environment. And yes, uname cannot be trusted either, so there
+        is a change that you end up with a 32 bit run on a 64 bit system.
+        Also, some proper 64 bit intel macs are too cheap (low-end) and
+        therefore not permitted to run the 64 bit kernel.
+      ]]--
+
+    function os.resolvers.platform(t,k)
+     -- local platform, architecture = "", os.getenv("HOSTTYPE") or ""
+     -- if architecture == "" then
+     --     architecture = os.resultof("echo $HOSTTYPE") or ""
+     -- end
+        local platform, architecture = "", os.resultof("echo $HOSTTYPE") or ""
+        if architecture == "" then
+         -- print("\nI have no clue what kind of OSX you're running so let's assume an 32 bit intel.\n")
+            platform = "osx-intel"
+        elseif find(architecture,"i386") then
+            platform = "osx-intel"
+        elseif find(architecture,"x86_64") then
+            platform = "osx-64"
+        else
+            platform = "osx-ppc"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "sunos" then
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.resultof("uname -m") or ""
+        if find(architecture,"sparc") then
+            platform = "solaris-sparc"
+        else -- if architecture == 'i86pc'
+            platform = "solaris-intel"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "freebsd" then
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.resultof("uname -m") or ""
+        if find(architecture,"amd64") then
+            platform = "freebsd-amd64"
+        else
+            platform = "freebsd"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "kfreebsd" then
+
+    function os.resolvers.platform(t,k)
+        -- we sometims have HOSTTYPE set so let's check that first
+        local platform, architecture = "", os.getenv("HOSTTYPE") or os.resultof("uname -m") or ""
+        if find(architecture,"x86_64") then
+            platform = "kfreebsd-64"
+        else
+            platform = "kfreebsd-i386"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+else
+
+    -- platform = "linux"
+    -- os.setenv("MTX_PLATFORM",platform)
+    -- os.platform = platform
+
+    function os.resolvers.platform(t,k)
+        local platform = "linux"
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+end
+
+-- beware, we set the randomseed
+
+-- from wikipedia: Version 4 UUIDs use a scheme relying only on random numbers. This algorithm sets the
+-- version number as well as two reserved bits. All other bits are set using a random or pseudorandom
+-- data source. Version 4 UUIDs have the form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx with hexadecimal
+-- digits x and hexadecimal digits 8, 9, A, or B for y. e.g. f47ac10b-58cc-4372-a567-0e02b2c3d479.
+--
+-- as we don't call this function too often there is not so much risk on repetition
+
+local t = { 8, 9, "a", "b" }
+
+function os.uuid()
+    return format("%04x%04x-4%03x-%s%03x-%04x-%04x%04x%04x",
+        random(0xFFFF),random(0xFFFF),
+        random(0x0FFF),
+        t[ceil(random(4))] or 8,random(0x0FFF),
+        random(0xFFFF),
+        random(0xFFFF),random(0xFFFF),random(0xFFFF)
+    )
+end
+
+local d
+
+function os.timezone(delta)
+    d = d or tonumber(tonumber(os.date("%H")-os.date("!%H")))
+    if delta then
+        if d > 0 then
+            return format("+%02i:00",d)
+        else
+            return format("-%02i:00",-d)
+        end
+    else
+        return 1
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-file'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- needs a cleanup
+
+file = file or { }
+
+local concat = table.concat
+local find, gmatch, match, gsub, sub, char = string.find, string.gmatch, string.match, string.gsub, string.sub, string.char
+local lpegmatch = lpeg.match
+
+function file.removesuffix(filename)
+    return (gsub(filename,"%.[%a%d]+$",""))
+end
+
+function file.addsuffix(filename, suffix)
+    if not suffix or suffix == "" then
+        return filename
+    elseif not find(filename,"%.[%a%d]+$") then
+        return filename .. "." .. suffix
+    else
+        return filename
+    end
+end
+
+function file.replacesuffix(filename, suffix)
+    return (gsub(filename,"%.[%a%d]+$","")) .. "." .. suffix
+end
+
+function file.dirname(name,default)
+    return match(name,"^(.+)[/\\].-$") or (default or "")
+end
+
+function file.basename(name)
+    return match(name,"^.+[/\\](.-)$") or name
+end
+
+function file.nameonly(name)
+    return (gsub(match(name,"^.+[/\\](.-)$") or name,"%..*$",""))
+end
+
+function file.extname(name,default)
+    return match(name,"^.+%.([^/\\]-)$") or default or ""
+end
+
+file.suffix = file.extname
+
+--~ function file.join(...)
+--~     local pth = concat({...},"/")
+--~     pth = gsub(pth,"\\","/")
+--~     local a, b = match(pth,"^(.*://)(.*)$")
+--~     if a and b then
+--~         return a .. gsub(b,"//+","/")
+--~     end
+--~     a, b = match(pth,"^(//)(.*)$")
+--~     if a and b then
+--~         return a .. gsub(b,"//+","/")
+--~     end
+--~     return (gsub(pth,"//+","/"))
+--~ end
+
+local trick_1 = char(1)
+local trick_2 = "^" .. trick_1 .. "/+"
+
+function file.join(...)
+    local lst = { ... }
+    local a, b = lst[1], lst[2]
+    if a == "" then
+        lst[1] = trick_1
+    elseif b and find(a,"^/+$") and find(b,"^/") then
+        lst[1] = ""
+        lst[2] = gsub(b,"^/+","")
+    end
+    local pth = concat(lst,"/")
+    pth = gsub(pth,"\\","/")
+    local a, b = match(pth,"^(.*://)(.*)$")
+    if a and b then
+        return a .. gsub(b,"//+","/")
+    end
+    a, b = match(pth,"^(//)(.*)$")
+    if a and b then
+        return a .. gsub(b,"//+","/")
+    end
+    pth = gsub(pth,trick_2,"")
+    return (gsub(pth,"//+","/"))
+end
+
+--~ print(file.join("//","/y"))
+--~ print(file.join("/","/y"))
+--~ print(file.join("","/y"))
+--~ print(file.join("/x/","/y"))
+--~ print(file.join("x/","/y"))
+--~ print(file.join("http://","/y"))
+--~ print(file.join("http://a","/y"))
+--~ print(file.join("http:///a","/y"))
+--~ print(file.join("//nas-1","/y"))
+
+function file.iswritable(name)
+    local a = lfs.attributes(name) or lfs.attributes(file.dirname(name,"."))
+    return a and sub(a.permissions,2,2) == "w"
+end
+
+function file.isreadable(name)
+    local a = lfs.attributes(name)
+    return a and sub(a.permissions,1,1) == "r"
+end
+
+file.is_readable = file.isreadable
+file.is_writable = file.iswritable
+
+-- todo: lpeg
+
+--~ function file.split_path(str)
+--~     local t = { }
+--~     str = gsub(str,"\\", "/")
+--~     str = gsub(str,"(%a):([;/])", "%1\001%2")
+--~     for name in gmatch(str,"([^;:]+)") do
+--~         if name ~= "" then
+--~             t[#t+1] = gsub(name,"\001",":")
+--~         end
+--~     end
+--~     return t
+--~ end
+
+local checkedsplit = string.checkedsplit
+
+function file.split_path(str,separator)
+    str = gsub(str,"\\","/")
+    return checkedsplit(str,separator or io.pathseparator)
+end
+
+function file.join_path(tab)
+    return concat(tab,io.pathseparator) -- can have trailing //
+end
+
+-- we can hash them weakly
+
+function file.collapse_path(str)
+    str = gsub(str,"\\","/")
+    if find(str,"/") then
+        str = gsub(str,"^%./",(gsub(lfs.currentdir(),"\\","/")) .. "/") -- ./xx in qualified
+        str = gsub(str,"/%./","/")
+        local n, m = 1, 1
+        while n > 0 or m > 0 do
+            str, n = gsub(str,"[^/%.]+/%.%.$","")
+            str, m = gsub(str,"[^/%.]+/%.%./","")
+        end
+        str = gsub(str,"([^/])/$","%1")
+    --  str = gsub(str,"^%./","") -- ./xx in qualified
+        str = gsub(str,"/%.$","")
+    end
+    if str == "" then str = "." end
+    return str
+end
+
+--~ print(file.collapse_path("/a"))
+--~ print(file.collapse_path("a/./b/.."))
+--~ print(file.collapse_path("a/aa/../b/bb"))
+--~ print(file.collapse_path("a/../.."))
+--~ print(file.collapse_path("a/.././././b/.."))
+--~ print(file.collapse_path("a/./././b/.."))
+--~ print(file.collapse_path("a/b/c/../.."))
+
+function file.robustname(str)
+    return (gsub(str,"[^%a%d%/%-%.\\]+","-"))
+end
+
+file.readdata = io.loaddata
+file.savedata = io.savedata
+
+function file.copy(oldname,newname)
+    file.savedata(newname,io.loaddata(oldname))
+end
+
+-- lpeg variants, slightly faster, not always
+
+--~ local period    = lpeg.P(".")
+--~ local slashes   = lpeg.S("\\/")
+--~ local noperiod  = 1-period
+--~ local noslashes = 1-slashes
+--~ local name      = noperiod^1
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.C(noperiod^1) * -1
+
+--~ function file.extname(name)
+--~     return lpegmatch(pattern,name) or ""
+--~ end
+
+--~ local pattern = lpeg.Cs(((period * noperiod^1 * -1)/"" + 1)^1)
+
+--~ function file.removesuffix(name)
+--~     return lpegmatch(pattern,name)
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^1 * lpeg.C(noslashes^1) * -1
+
+--~ function file.basename(name)
+--~     return lpegmatch(pattern,name) or name
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^1 * lpeg.Cp() * noslashes^1 * -1
+
+--~ function file.dirname(name)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return sub(name,1,p-2)
+--~     else
+--~         return ""
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.Cp() * noperiod^1 * -1
+
+--~ function file.addsuffix(name, suffix)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return name
+--~     else
+--~         return name .. "." .. suffix
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.Cp() * noperiod^1 * -1
+
+--~ function file.replacesuffix(name,suffix)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return sub(name,1,p-2) .. "." .. suffix
+--~     else
+--~         return name .. "." .. suffix
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * lpeg.Cp() * ((noperiod^1 * period)^1 * lpeg.Cp() + lpeg.P(true)) * noperiod^1 * -1
+
+--~ function file.nameonly(name)
+--~     local a, b = lpegmatch(pattern,name)
+--~     if b then
+--~         return sub(name,a,b-2)
+--~     elseif a then
+--~         return sub(name,a)
+--~     else
+--~         return name
+--~     end
+--~ end
+
+--~ local test = file.extname
+--~ local test = file.basename
+--~ local test = file.dirname
+--~ local test = file.addsuffix
+--~ local test = file.replacesuffix
+--~ local test = file.nameonly
+
+--~ print(1,test("./a/b/c/abd.def.xxx","!!!"))
+--~ print(2,test("./../b/c/abd.def.xxx","!!!"))
+--~ print(3,test("a/b/c/abd.def.xxx","!!!"))
+--~ print(4,test("a/b/c/def.xxx","!!!"))
+--~ print(5,test("a/b/c/def","!!!"))
+--~ print(6,test("def","!!!"))
+--~ print(7,test("def.xxx","!!!"))
+
+--~ local tim = os.clock() for i=1,250000 do local ext = test("abd.def.xxx","!!!") end print(os.clock()-tim)
+
+-- also rewrite previous
+
+local letter    = lpeg.R("az","AZ") + lpeg.S("_-+")
+local separator = lpeg.P("://")
+
+local qualified = lpeg.P(".")^0 * lpeg.P("/") + letter*lpeg.P(":") + letter^1*separator + letter^1 * lpeg.P("/")
+local rootbased = lpeg.P("/") + letter*lpeg.P(":")
+
+-- ./name ../name  /name c: :// name/name
+
+function file.is_qualified_path(filename)
+    return lpegmatch(qualified,filename) ~= nil
+end
+
+function file.is_rootbased_path(filename)
+    return lpegmatch(rootbased,filename) ~= nil
+end
+
+local slash  = lpeg.S("\\/")
+local period = lpeg.P(".")
+local drive  = lpeg.C(lpeg.R("az","AZ")) * lpeg.P(":")
+local path   = lpeg.C(((1-slash)^0 * slash)^0)
+local suffix = period * lpeg.C(lpeg.P(1-period)^0 * lpeg.P(-1))
+local base   = lpeg.C((1-suffix)^0)
+
+local pattern = (drive + lpeg.Cc("")) * (path + lpeg.Cc("")) * (base + lpeg.Cc("")) * (suffix + lpeg.Cc(""))
+
+function file.splitname(str) -- returns drive, path, base, suffix
+    return lpegmatch(pattern,str)
+end
+
+-- function test(t) for k, v in next, t do print(v, "=>", file.splitname(v)) end end
+--
+-- test { "c:", "c:/aa", "c:/aa/bb", "c:/aa/bb/cc", "c:/aa/bb/cc.dd", "c:/aa/bb/cc.dd.ee" }
+-- test { "c:", "c:aa", "c:aa/bb", "c:aa/bb/cc", "c:aa/bb/cc.dd", "c:aa/bb/cc.dd.ee" }
+-- test { "/aa", "/aa/bb", "/aa/bb/cc", "/aa/bb/cc.dd", "/aa/bb/cc.dd.ee" }
+-- test { "aa", "aa/bb", "aa/bb/cc", "aa/bb/cc.dd", "aa/bb/cc.dd.ee" }
+
+--~ -- todo:
+--~
+--~ if os.type == "windows" then
+--~     local currentdir = lfs.currentdir
+--~     function lfs.currentdir()
+--~         return (gsub(currentdir(),"\\","/"))
+--~     end
+--~ end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-md5'] = {
+    version   = 1.001,
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- This also provides file checksums and checkers.
+
+local gsub, format, byte = string.gsub, string.format, string.byte
+
+local function convert(str,fmt)
+    return (gsub(md5.sum(str),".",function(chr) return format(fmt,byte(chr)) end))
+end
+
+if not md5.HEX then function md5.HEX(str) return convert(str,"%02X") end end
+if not md5.hex then function md5.hex(str) return convert(str,"%02x") end end
+if not md5.dec then function md5.dec(str) return convert(str,"%03i") end end
+
+--~ if not md5.HEX then
+--~     local function remap(chr) return format("%02X",byte(chr)) end
+--~     function md5.HEX(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+--~ if not md5.hex then
+--~     local function remap(chr) return format("%02x",byte(chr)) end
+--~     function md5.hex(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+--~ if not md5.dec then
+--~     local function remap(chr) return format("%03i",byte(chr)) end
+--~     function md5.dec(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+
+file.needs_updating_threshold = 1
+
+function file.needs_updating(oldname,newname) -- size modification access change
+    local oldtime = lfs.attributes(oldname, modification)
+    local newtime = lfs.attributes(newname, modification)
+    if newtime >= oldtime then
+        return false
+    elseif oldtime - newtime < file.needs_updating_threshold then
+        return false
+    else
+        return true
+    end
+end
+
+function file.checksum(name)
+    if md5 then
+        local data = io.loaddata(name)
+        if data then
+            return md5.HEX(data)
+        end
+    end
+    return nil
+end
+
+function file.loadchecksum(name)
+    if md5 then
+        local data = io.loaddata(name .. ".md5")
+        return data and (gsub(data,"%s",""))
+    end
+    return nil
+end
+
+function file.savechecksum(name, checksum)
+    if not checksum then checksum = file.checksum(name) end
+    if checksum then
+        io.savedata(name .. ".md5",checksum)
+        return checksum
+    end
+    return nil
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-url'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local char, gmatch, gsub = string.char, string.gmatch, string.gsub
+local tonumber, type = tonumber, type
+local lpegmatch = lpeg.match
+
+-- from the spec (on the web):
+--
+--     foo://example.com:8042/over/there?name=ferret#nose
+--     \_/   \______________/\_________/ \_________/ \__/
+--      |           |            |            |        |
+--   scheme     authority       path        query   fragment
+--      |   _____________________|__
+--     / \ /                        \
+--     urn:example:animal:ferret:nose
+
+url = url or { }
+
+local function tochar(s)
+    return char(tonumber(s,16))
+end
+
+local colon, qmark, hash, slash, percent, endofstring = lpeg.P(":"), lpeg.P("?"), lpeg.P("#"), lpeg.P("/"), lpeg.P("%"), lpeg.P(-1)
+
+local hexdigit  = lpeg.R("09","AF","af")
+local plus      = lpeg.P("+")
+local escaped   = (plus / " ") + (percent * lpeg.C(hexdigit * hexdigit) / tochar)
+
+-- we assume schemes with more than 1 character (in order to avoid problems with windows disks)
+
+local scheme    =                 lpeg.Cs((escaped+(1-colon-slash-qmark-hash))^2) * colon + lpeg.Cc("")
+local authority = slash * slash * lpeg.Cs((escaped+(1-      slash-qmark-hash))^0)         + lpeg.Cc("")
+local path      = slash *         lpeg.Cs((escaped+(1-            qmark-hash))^0)         + lpeg.Cc("")
+local query     = qmark         * lpeg.Cs((escaped+(1-                  hash))^0)         + lpeg.Cc("")
+local fragment  = hash          * lpeg.Cs((escaped+(1-           endofstring))^0)         + lpeg.Cc("")
+
+local parser = lpeg.Ct(scheme * authority * path * query * fragment)
+
+-- todo: reconsider Ct as we can as well have five return values (saves a table)
+-- so we can have two parsers, one with and one without
+
+function url.split(str)
+    return (type(str) == "string" and lpegmatch(parser,str)) or str
+end
+
+-- todo: cache them
+
+function url.hashed(str)
+    local s = url.split(str)
+    local somescheme = s[1] ~= ""
+    return {
+        scheme    = (somescheme and s[1]) or "file",
+        authority = s[2],
+        path      = s[3],
+        query     = s[4],
+        fragment  = s[5],
+        original  = str,
+        noscheme  = not somescheme,
+    }
+end
+
+function url.hasscheme(str)
+    return url.split(str)[1] ~= ""
+end
+
+function url.addscheme(str,scheme)
+    return (url.hasscheme(str) and str) or ((scheme or "file:///") .. str)
+end
+
+function url.construct(hash)
+    local fullurl = hash.sheme .. "://".. hash.authority .. hash.path
+    if hash.query then
+        fullurl = fullurl .. "?".. hash.query
+    end
+    if hash.fragment then
+        fullurl = fullurl .. "?".. hash.fragment
+    end
+    return fullurl
+end
+
+function url.filename(filename)
+    local t = url.hashed(filename)
+    return (t.scheme == "file" and (gsub(t.path,"^/([a-zA-Z])([:|])/)","%1:"))) or filename
+end
+
+function url.query(str)
+    if type(str) == "string" then
+        local t = { }
+        for k, v in gmatch(str,"([^&=]*)=([^&=]*)") do
+            t[k] = v
+        end
+        return t
+    else
+        return str
+    end
+end
+
+--~ print(url.filename("file:///c:/oeps.txt"))
+--~ print(url.filename("c:/oeps.txt"))
+--~ print(url.filename("file:///oeps.txt"))
+--~ print(url.filename("file:///etc/test.txt"))
+--~ print(url.filename("/oeps.txt"))
+
+--~ from the spec on the web (sort of):
+--~
+--~ function test(str)
+--~     print(table.serialize(url.hashed(str)))
+--~ end
+--~
+--~ test("%56pass%20words")
+--~ test("file:///c:/oeps.txt")
+--~ test("file:///c|/oeps.txt")
+--~ test("file:///etc/oeps.txt")
+--~ test("file://./etc/oeps.txt")
+--~ test("file:////etc/oeps.txt")
+--~ test("ftp://ftp.is.co.za/rfc/rfc1808.txt")
+--~ test("http://www.ietf.org/rfc/rfc2396.txt")
+--~ test("ldap://[2001:db8::7]/c=GB?objectClass?one#what")
+--~ test("mailto:John.Doe@example.com")
+--~ test("news:comp.infosystems.www.servers.unix")
+--~ test("tel:+1-816-555-1212")
+--~ test("telnet://192.0.2.16:80/")
+--~ test("urn:oasis:names:specification:docbook:dtd:xml:4.1.2")
+--~ test("/etc/passwords")
+--~ test("http://www.pragma-ade.com/spaced%20name")
+
+--~ test("zip:///oeps/oeps.zip#bla/bla.tex")
+--~ test("zip:///oeps/oeps.zip?bla/bla.tex")
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-dir'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- dir.expand_name will be merged with cleanpath and collapsepath
+
+local type = type
+local find, gmatch, match, gsub = string.find, string.gmatch, string.match, string.gsub
+local lpegmatch = lpeg.match
+
+dir = dir or { }
+
+-- handy
+
+function dir.current()
+    return (gsub(lfs.currentdir(),"\\","/"))
+end
+
+-- optimizing for no string.find (*) does not save time
+
+local attributes = lfs.attributes
+local walkdir    = lfs.dir
+
+local function glob_pattern(path,patt,recurse,action)
+    local ok, scanner
+    if path == "/" then
+        ok, scanner = xpcall(function() return walkdir(path..".") end, function() end) -- kepler safe
+    else
+        ok, scanner = xpcall(function() return walkdir(path)      end, function() end) -- kepler safe
+    end
+    if ok and type(scanner) == "function" then
+        if not find(path,"/$") then path = path .. '/' end
+        for name in scanner do
+            local full = path .. name
+            local mode = attributes(full,'mode')
+            if mode == 'file' then
+                if find(full,patt) then
+                    action(full)
+                end
+            elseif recurse and (mode == "directory") and (name ~= '.') and (name ~= "..") then
+                glob_pattern(full,patt,recurse,action)
+            end
+        end
+    end
+end
+
+dir.glob_pattern = glob_pattern
+
+local function collect_pattern(path,patt,recurse,result)
+    local ok, scanner
+    result = result or { }
+    if path == "/" then
+        ok, scanner = xpcall(function() return walkdir(path..".") end, function() end) -- kepler safe
+    else
+        ok, scanner = xpcall(function() return walkdir(path)      end, function() end) -- kepler safe
+    end
+    if ok and type(scanner) == "function" then
+        if not find(path,"/$") then path = path .. '/' end
+        for name in scanner do
+            local full = path .. name
+            local attr = attributes(full)
+            local mode = attr.mode
+            if mode == 'file' then
+                if find(full,patt) then
+                    result[name] = attr
+                end
+            elseif recurse and (mode == "directory") and (name ~= '.') and (name ~= "..") then
+                attr.list = collect_pattern(full,patt,recurse)
+                result[name] = attr
+            end
+        end
+    end
+    return result
+end
+
+dir.collect_pattern = collect_pattern
+
+local P, S, R, C, Cc, Cs, Ct, Cv, V = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.Cc, lpeg.Cs, lpeg.Ct, lpeg.Cv, lpeg.V
+
+local pattern = Ct {
+    [1] = (C(P(".") + P("/")^1) + C(R("az","AZ") * P(":") * P("/")^0) + Cc("./")) * V(2) * V(3),
+    [2] = C(((1-S("*?/"))^0 * P("/"))^0),
+    [3] = C(P(1)^0)
+}
+
+local filter = Cs ( (
+    P("**") / ".*" +
+    P("*")  / "[^/]*" +
+    P("?")  / "[^/]" +
+    P(".")  / "%%." +
+    P("+")  / "%%+" +
+    P("-")  / "%%-" +
+    P(1)
+)^0 )
+
+local function glob(str,t)
+    if type(t) == "function" then
+        if type(str) == "table" then
+            for s=1,#str do
+                glob(str[s],t)
+            end
+        elseif lfs.isfile(str) then
+            t(str)
+        else
+            local split = lpegmatch(pattern,str)
+            if split then
+                local root, path, base = split[1], split[2], split[3]
+                local recurse = find(base,"%*%*")
+                local start = root .. path
+                local result = lpegmatch(filter,start .. base)
+                glob_pattern(start,result,recurse,t)
+            end
+        end
+    else
+        if type(str) == "table" then
+            local t = t or { }
+            for s=1,#str do
+                glob(str[s],t)
+            end
+            return t
+        elseif lfs.isfile(str) then
+            local t = t or { }
+            t[#t+1] = str
+            return t
+        else
+            local split = lpegmatch(pattern,str)
+            if split then
+                local t = t or { }
+                local action = action or function(name) t[#t+1] = name end
+                local root, path, base = split[1], split[2], split[3]
+                local recurse = find(base,"%*%*")
+                local start = root .. path
+                local result = lpegmatch(filter,start .. base)
+                glob_pattern(start,result,recurse,action)
+                return t
+            else
+                return { }
+            end
+        end
+    end
+end
+
+dir.glob = glob
+
+--~ list = dir.glob("**/*.tif")
+--~ list = dir.glob("/**/*.tif")
+--~ list = dir.glob("./**/*.tif")
+--~ list = dir.glob("oeps/**/*.tif")
+--~ list = dir.glob("/oeps/**/*.tif")
+
+local function globfiles(path,recurse,func,files) -- func == pattern or function
+    if type(func) == "string" then
+        local s = func -- alas, we need this indirect way
+        func = function(name) return find(name,s) end
+    end
+    files = files or { }
+    for name in walkdir(path) do
+        if find(name,"^%.") then
+            --- skip
+        else
+            local mode = attributes(name,'mode')
+            if mode == "directory" then
+                if recurse then
+                    globfiles(path .. "/" .. name,recurse,func,files)
+                end
+            elseif mode == "file" then
+                if func then
+                    if func(name) then
+                        files[#files+1] = path .. "/" .. name
+                    end
+                else
+                    files[#files+1] = path .. "/" .. name
+                end
+            end
+        end
+    end
+    return files
+end
+
+dir.globfiles = globfiles
+
+-- t = dir.glob("c:/data/develop/context/sources/**/????-*.tex")
+-- t = dir.glob("c:/data/develop/tex/texmf/**/*.tex")
+-- t = dir.glob("c:/data/develop/context/texmf/**/*.tex")
+-- t = dir.glob("f:/minimal/tex/**/*")
+-- print(dir.ls("f:/minimal/tex/**/*"))
+-- print(dir.ls("*.tex"))
+
+function dir.ls(pattern)
+    return table.concat(glob(pattern),"\n")
+end
+
+--~ mkdirs("temp")
+--~ mkdirs("a/b/c")
+--~ mkdirs(".","/a/b/c")
+--~ mkdirs("a","b","c")
+
+local make_indeed = true -- false
+
+if string.find(os.getenv("PATH"),";") then -- os.type == "windows"
+
+    function dir.mkdirs(...)
+        local str, pth, t = "", "", { ... }
+        for i=1,#t do
+            local s = t[i]
+            if s ~= "" then
+                if str ~= "" then
+                    str = str .. "/" .. s
+                else
+                    str = s
+                end
+            end
+        end
+        local first, middle, last
+        local drive = false
+        first, middle, last = match(str,"^(//)(//*)(.*)$")
+        if first then
+            -- empty network path == local path
+        else
+            first, last = match(str,"^(//)/*(.-)$")
+            if first then
+                middle, last = match(str,"([^/]+)/+(.-)$")
+                if middle then
+                    pth = "//" .. middle
+                else
+                    pth = "//" .. last
+                    last = ""
+                end
+            else
+                first, middle, last = match(str,"^([a-zA-Z]:)(/*)(.-)$")
+                if first then
+                    pth, drive = first .. middle, true
+                else
+                    middle, last = match(str,"^(/*)(.-)$")
+                    if not middle then
+                        last = str
+                    end
+                end
+            end
+        end
+        for s in gmatch(last,"[^/]+") do
+            if pth == "" then
+                pth = s
+            elseif drive then
+                pth, drive = pth .. s, false
+            else
+                pth = pth .. "/" .. s
+            end
+            if make_indeed and not lfs.isdir(pth) then
+                lfs.mkdir(pth)
+            end
+        end
+        return pth, (lfs.isdir(pth) == true)
+    end
+
+--~         print(dir.mkdirs("","","a","c"))
+--~         print(dir.mkdirs("a"))
+--~         print(dir.mkdirs("a:"))
+--~         print(dir.mkdirs("a:/b/c"))
+--~         print(dir.mkdirs("a:b/c"))
+--~         print(dir.mkdirs("a:/bbb/c"))
+--~         print(dir.mkdirs("/a/b/c"))
+--~         print(dir.mkdirs("/aaa/b/c"))
+--~         print(dir.mkdirs("//a/b/c"))
+--~         print(dir.mkdirs("///a/b/c"))
+--~         print(dir.mkdirs("a/bbb//ccc/"))
+
+    function dir.expand_name(str) -- will be merged with cleanpath and collapsepath
+        local first, nothing, last = match(str,"^(//)(//*)(.*)$")
+        if first then
+            first = dir.current() .. "/"
+        end
+        if not first then
+            first, last = match(str,"^(//)/*(.*)$")
+        end
+        if not first then
+            first, last = match(str,"^([a-zA-Z]:)(.*)$")
+            if first and not find(last,"^/") then
+                local d = lfs.currentdir()
+                if lfs.chdir(first) then
+                    first = dir.current()
+                end
+                lfs.chdir(d)
+            end
+        end
+        if not first then
+            first, last = dir.current(), str
+        end
+        last = gsub(last,"//","/")
+        last = gsub(last,"/%./","/")
+        last = gsub(last,"^/*","")
+        first = gsub(first,"/*$","")
+        if last == "" then
+            return first
+        else
+            return first .. "/" .. last
+        end
+    end
+
+else
+
+    function dir.mkdirs(...)
+        local str, pth, t = "", "", { ... }
+        for i=1,#t do
+            local s = t[i]
+            if s ~= "" then
+                if str ~= "" then
+                    str = str .. "/" .. s
+                else
+                    str = s
+                end
+            end
+        end
+        str = gsub(str,"/+","/")
+        if find(str,"^/") then
+            pth = "/"
+            for s in gmatch(str,"[^/]+") do
+                local first = (pth == "/")
+                if first then
+                    pth = pth .. s
+                else
+                    pth = pth .. "/" .. s
+                end
+                if make_indeed and not first and not lfs.isdir(pth) then
+                    lfs.mkdir(pth)
+                end
+            end
+        else
+            pth = "."
+            for s in gmatch(str,"[^/]+") do
+                pth = pth .. "/" .. s
+                if make_indeed and not lfs.isdir(pth) then
+                    lfs.mkdir(pth)
+                end
+            end
+        end
+        return pth, (lfs.isdir(pth) == true)
+    end
+
+--~         print(dir.mkdirs("","","a","c"))
+--~         print(dir.mkdirs("a"))
+--~         print(dir.mkdirs("/a/b/c"))
+--~         print(dir.mkdirs("/aaa/b/c"))
+--~         print(dir.mkdirs("//a/b/c"))
+--~         print(dir.mkdirs("///a/b/c"))
+--~         print(dir.mkdirs("a/bbb//ccc/"))
+
+    function dir.expand_name(str) -- will be merged with cleanpath and collapsepath
+        if not find(str,"^/") then
+            str = lfs.currentdir() .. "/" .. str
+        end
+        str = gsub(str,"//","/")
+        str = gsub(str,"/%./","/")
+        return str
+    end
+
+end
+
+dir.makedirs = dir.mkdirs
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-boolean'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+boolean = boolean or { }
+
+local type, tonumber = type, tonumber
+
+function boolean.tonumber(b)
+    if b then return 1 else return 0 end
+end
+
+function toboolean(str,tolerant)
+    if tolerant then
+        local tstr = type(str)
+        if tstr == "string" then
+            return str == "true" or str == "yes" or str == "on" or str == "1" or str == "t"
+        elseif tstr == "number" then
+            return tonumber(str) ~= 0
+        elseif tstr == "nil" then
+            return false
+        else
+            return str
+        end
+    elseif str == "true" then
+        return true
+    elseif str == "false" then
+        return false
+    else
+        return str
+    end
+end
+
+function string.is_boolean(str)
+    if type(str) == "string" then
+        if str == "true" or str == "yes" or str == "on" or str == "t" then
+            return true
+        elseif str == "false" or str == "no" or str == "off" or str == "f" then
+            return false
+        end
+    end
+    return nil
+end
+
+function boolean.alwaystrue()
+    return true
+end
+
+function boolean.falsetrue()
+    return false
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-unicode'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+if not unicode then
+
+    unicode = { utf8 = { } }
+
+    local floor, char = math.floor, string.char
+
+    function unicode.utf8.utfchar(n)
+        if n < 0x80 then
+            return char(n)
+        elseif n < 0x800 then
+            return char(0xC0 + floor(n/0x40))  .. char(0x80 + (n % 0x40))
+        elseif n < 0x10000 then
+            return char(0xE0 + floor(n/0x1000)) .. char(0x80 + (floor(n/0x40) % 0x40)) .. char(0x80 + (n % 0x40))
+        elseif n < 0x40000 then
+            return char(0xF0 + floor(n/0x40000)) .. char(0x80 + floor(n/0x1000)) .. char(0x80 + (floor(n/0x40) % 0x40)) .. char(0x80 + (n % 0x40))
+        else -- wrong:
+          -- return char(0xF1 + floor(n/0x1000000)) .. char(0x80 + floor(n/0x40000)) .. char(0x80 + floor(n/0x1000)) .. char(0x80 + (floor(n/0x40) % 0x40)) .. char(0x80 + (n % 0x40))
+            return "?"
+        end
+    end
+
+end
+
+utf = utf or unicode.utf8
+
+local concat, utfchar, utfgsub = table.concat, utf.char, utf.gsub
+local char, byte, find, bytepairs = string.char, string.byte, string.find, string.bytepairs
+
+-- 0  EF BB BF      UTF-8
+-- 1  FF FE         UTF-16-little-endian
+-- 2  FE FF         UTF-16-big-endian
+-- 3  FF FE 00 00   UTF-32-little-endian
+-- 4  00 00 FE FF   UTF-32-big-endian
+
+unicode.utfname = {
+    [0] = 'utf-8',
+    [1] = 'utf-16-le',
+    [2] = 'utf-16-be',
+    [3] = 'utf-32-le',
+    [4] = 'utf-32-be'
+}
+
+-- \000 fails in <= 5.0 but is valid in >=5.1 where %z is depricated
+
+function unicode.utftype(f)
+    local str = f:read(4)
+    if not str then
+        f:seek('set')
+        return 0
+ -- elseif find(str,"^%z%z\254\255") then            -- depricated
+ -- elseif find(str,"^\000\000\254\255") then        -- not permitted and bugged
+    elseif find(str,"\000\000\254\255",1,true) then  -- seems to work okay (TH)
+        return 4
+ -- elseif find(str,"^\255\254%z%z") then            -- depricated
+ -- elseif find(str,"^\255\254\000\000") then        -- not permitted and bugged
+    elseif find(str,"\255\254\000\000",1,true) then  -- seems to work okay (TH)
+        return 3
+    elseif find(str,"^\254\255") then
+        f:seek('set',2)
+        return 2
+    elseif find(str,"^\255\254") then
+        f:seek('set',2)
+        return 1
+    elseif find(str,"^\239\187\191") then
+        f:seek('set',3)
+        return 0
+    else
+        f:seek('set')
+        return 0
+    end
+end
+
+function unicode.utf16_to_utf8(str, endian) -- maybe a gsub is faster or an lpeg
+    local result, tmp, n, m, p = { }, { }, 0, 0, 0
+    -- lf | cr | crlf / (cr:13, lf:10)
+    local function doit()
+        if n == 10 then
+            if p ~= 13 then
+                result[#result+1] = concat(tmp)
+                tmp = { }
+                p = 0
+            end
+        elseif n == 13 then
+            result[#result+1] = concat(tmp)
+            tmp = { }
+            p = n
+        else
+            tmp[#tmp+1] = utfchar(n)
+            p = 0
+        end
+    end
+    for l,r in bytepairs(str) do
+        if r then
+            if endian then
+                n = l*256 + r
+            else
+                n = r*256 + l
+            end
+            if m > 0 then
+                n = (m-0xD800)*0x400 + (n-0xDC00) + 0x10000
+                m = 0
+                doit()
+            elseif n >= 0xD800 and n <= 0xDBFF then
+                m = n
+            else
+                doit()
+            end
+        end
+    end
+    if #tmp > 0 then
+        result[#result+1] = concat(tmp)
+    end
+    return result
+end
+
+function unicode.utf32_to_utf8(str, endian)
+    local result = { }
+    local tmp, n, m, p = { }, 0, -1, 0
+    -- lf | cr | crlf / (cr:13, lf:10)
+    local function doit()
+        if n == 10 then
+            if p ~= 13 then
+                result[#result+1] = concat(tmp)
+                tmp = { }
+                p = 0
+            end
+        elseif n == 13 then
+            result[#result+1] = concat(tmp)
+            tmp = { }
+            p = n
+        else
+            tmp[#tmp+1] = utfchar(n)
+            p = 0
+        end
+    end
+    for a,b in bytepairs(str) do
+        if a and b then
+            if m < 0 then
+                if endian then
+                    m = a*256*256*256 + b*256*256
+                else
+                    m = b*256 + a
+                end
+            else
+                if endian then
+                    n = m + a*256 + b
+                else
+                    n = m + b*256*256*256 + a*256*256
+                end
+                m = -1
+                doit()
+            end
+        else
+            break
+        end
+    end
+    if #tmp > 0 then
+        result[#result+1] = concat(tmp)
+    end
+    return result
+end
+
+local function little(c)
+    local b = byte(c) -- b = c:byte()
+    if b < 0x10000 then
+        return char(b%256,b/256)
+    else
+        b = b - 0x10000
+        local b1, b2 = b/1024 + 0xD800, b%1024 + 0xDC00
+        return char(b1%256,b1/256,b2%256,b2/256)
+    end
+end
+
+local function big(c)
+    local b = byte(c)
+    if b < 0x10000 then
+        return char(b/256,b%256)
+    else
+        b = b - 0x10000
+        local b1, b2 = b/1024 + 0xD800, b%1024 + 0xDC00
+        return char(b1/256,b1%256,b2/256,b2%256)
+    end
+end
+
+function unicode.utf8_to_utf16(str,littleendian)
+    if littleendian then
+        return char(255,254) .. utfgsub(str,".",little)
+    else
+        return char(254,255) .. utfgsub(str,".",big)
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-math'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local floor, sin, cos, tan = math.floor, math.sin, math.cos, math.tan
+
+if not math.round then
+    function math.round(x)
+        return floor(x + 0.5)
+    end
+end
+
+if not math.div then
+    function math.div(n,m)
+        return floor(n/m)
+    end
+end
+
+if not math.mod then
+    function math.mod(n,m)
+        return n % m
+    end
+end
+
+local pipi = 2*math.pi/360
+
+function math.sind(d)
+    return sin(d*pipi)
+end
+
+function math.cosd(d)
+    return cos(d*pipi)
+end
+
+function math.tand(d)
+    return tan(d*pipi)
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-utils'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- hm, quite unreadable
+
+local gsub = string.gsub
+local concat = table.concat
+local type, next = type, next
+
+if not utils        then utils        = { } end
+if not utils.merger then utils.merger = { } end
+if not utils.lua    then utils.lua    = { } end
+
+utils.merger.m_begin = "begin library merge"
+utils.merger.m_end   = "end library merge"
+utils.merger.pattern =
+    "%c+" ..
+    "%-%-%s+" .. utils.merger.m_begin ..
+    "%c+(.-)%c+" ..
+    "%-%-%s+" .. utils.merger.m_end ..
+    "%c+"
+
+function utils.merger._self_fake_()
+    return
+        "-- " .. "created merged file" .. "\n\n" ..
+        "-- " .. utils.merger.m_begin  .. "\n\n" ..
+        "-- " .. utils.merger.m_end    .. "\n\n"
+end
+
+function utils.report(...)
+    print(...)
+end
+
+utils.merger.strip_comment = true
+
+function utils.merger._self_load_(name)
+    local f, data = io.open(name), ""
+    if f then
+        utils.report("reading merge from %s",name)
+        data = f:read("*all")
+        f:close()
+    else
+        utils.report("unknown file to merge %s",name)
+    end
+    if data and utils.merger.strip_comment then
+        -- saves some 20K
+        data = gsub(data,"%-%-~[^\n\r]*[\r\n]", "")
+    end
+    return data or ""
+end
+
+function utils.merger._self_save_(name, data)
+    if data ~= "" then
+        local f = io.open(name,'w')
+        if f then
+            utils.report("saving merge from %s",name)
+            f:write(data)
+            f:close()
+        end
+    end
+end
+
+function utils.merger._self_swap_(data,code)
+    if data ~= "" then
+        return (gsub(data,utils.merger.pattern, function(s)
+            return "\n\n" .. "-- "..utils.merger.m_begin .. "\n" .. code .. "\n" .. "-- "..utils.merger.m_end .. "\n\n"
+        end, 1))
+    else
+        return ""
+    end
+end
+
+--~ stripper:
+--~
+--~ data = gsub(data,"%-%-~[^\n]*\n","")
+--~ data = gsub(data,"\n\n+","\n")
+
+function utils.merger._self_libs_(libs,list)
+    local result, f, frozen = { }, nil, false
+    result[#result+1] = "\n"
+    if type(libs) == 'string' then libs = { libs } end
+    if type(list) == 'string' then list = { list } end
+    local foundpath = nil
+    for i=1,#libs do
+        local lib = libs[i]
+        for j=1,#list do
+            local pth = gsub(list[j],"\\","/") -- file.clean_path
+            utils.report("checking library path %s",pth)
+            local name = pth .. "/" .. lib
+            if lfs.isfile(name) then
+                foundpath = pth
+            end
+        end
+        if foundpath then break end
+    end
+    if foundpath then
+        utils.report("using library path %s",foundpath)
+        local right, wrong = { }, { }
+        for i=1,#libs do
+            local lib = libs[i]
+            local fullname = foundpath .. "/" .. lib
+            if lfs.isfile(fullname) then
+            --  right[#right+1] = lib
+                utils.report("merging library %s",fullname)
+                result[#result+1] = "do -- create closure to overcome 200 locals limit"
+                result[#result+1] = io.loaddata(fullname,true)
+                result[#result+1] = "end -- of closure"
+            else
+            --  wrong[#wrong+1] = lib
+                utils.report("no library %s",fullname)
+            end
+        end
+        if #right > 0 then
+            utils.report("merged libraries: %s",concat(right," "))
+        end
+        if #wrong > 0 then
+            utils.report("skipped libraries: %s",concat(wrong," "))
+        end
+    else
+        utils.report("no valid library path found")
+    end
+    return concat(result, "\n\n")
+end
+
+function utils.merger.selfcreate(libs,list,target)
+    if target then
+        utils.merger._self_save_(
+            target,
+            utils.merger._self_swap_(
+                utils.merger._self_fake_(),
+                utils.merger._self_libs_(libs,list)
+            )
+        )
+    end
+end
+
+function utils.merger.selfmerge(name,libs,list,target)
+    utils.merger._self_save_(
+        target or name,
+        utils.merger._self_swap_(
+            utils.merger._self_load_(name),
+            utils.merger._self_libs_(libs,list)
+        )
+    )
+end
+
+function utils.merger.selfclean(name)
+    utils.merger._self_save_(
+        name,
+        utils.merger._self_swap_(
+            utils.merger._self_load_(name),
+            ""
+        )
+    )
+end
+
+function utils.lua.compile(luafile, lucfile, cleanup, strip) -- defaults: cleanup=false strip=true
+ -- utils.report("compiling",luafile,"into",lucfile)
+    os.remove(lucfile)
+    local command = "-o " .. string.quote(lucfile) .. " " .. string.quote(luafile)
+    if strip ~= false then
+        command = "-s " .. command
+    end
+    local done = (os.spawn("texluac " .. command) == 0) or (os.spawn("luac " .. command) == 0)
+    if done and cleanup == true and lfs.isfile(lucfile) and lfs.isfile(luafile) then
+     -- utils.report("removing",luafile)
+        os.remove(luafile)
+    end
+    return done
+end
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-aux'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- for inline, no store split : for s in string.gmatch(str,",* *([^,]+)") do .. end
+
+aux = aux or { }
+
+local concat, format, gmatch = table.concat, string.format, string.gmatch
+local tostring, type = tostring, type
+local lpegmatch = lpeg.match
+
+local P, R, V = lpeg.P, lpeg.R, lpeg.V
+
+local escape, left, right = P("\\"), P('{'), P('}')
+
+lpeg.patterns.balanced = P {
+    [1] = ((escape * (left+right)) + (1 - (left+right)) + V(2))^0,
+    [2] = left * V(1) * right
+}
+
+local space     = lpeg.P(' ')
+local equal     = lpeg.P("=")
+local comma     = lpeg.P(",")
+local lbrace    = lpeg.P("{")
+local rbrace    = lpeg.P("}")
+local nobrace   = 1 - (lbrace+rbrace)
+local nested    = lpeg.P { lbrace * (nobrace + lpeg.V(1))^0 * rbrace }
+local spaces    = space^0
+
+local value     = lpeg.P(lbrace * lpeg.C((nobrace + nested)^0) * rbrace) + lpeg.C((nested + (1-comma))^0)
+
+local key       = lpeg.C((1-equal-comma)^1)
+local pattern_a = (space+comma)^0 * (key * equal * value + key * lpeg.C(""))
+local pattern_c = (space+comma)^0 * (key * equal * value)
+
+local key       = lpeg.C((1-space-equal-comma)^1)
+local pattern_b = spaces * comma^0 * spaces * (key * ((spaces * equal * spaces * value) + lpeg.C("")))
+
+-- "a=1, b=2, c=3, d={a{b,c}d}, e=12345, f=xx{a{b,c}d}xx, g={}" : outer {} removes, leading spaces ignored
+
+local hash = { }
+
+local function set(key,value) -- using Carg is slower here
+    hash[key] = value
+end
+
+local pattern_a_s = (pattern_a/set)^1
+local pattern_b_s = (pattern_b/set)^1
+local pattern_c_s = (pattern_c/set)^1
+
+aux.settings_to_hash_pattern_a = pattern_a_s
+aux.settings_to_hash_pattern_b = pattern_b_s
+aux.settings_to_hash_pattern_c = pattern_c_s
+
+function aux.make_settings_to_hash_pattern(set,how)
+    if how == "strict" then
+        return (pattern_c/set)^1
+    elseif how == "tolerant" then
+        return (pattern_b/set)^1
+    else
+        return (pattern_a/set)^1
+    end
+end
+
+function aux.settings_to_hash(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        if moretolerant then
+            lpegmatch(pattern_b_s,str)
+        else
+            lpegmatch(pattern_a_s,str)
+        end
+        return hash
+    else
+        return { }
+    end
+end
+
+function aux.settings_to_hash_tolerant(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        lpegmatch(pattern_b_s,str)
+        return hash
+    else
+        return { }
+    end
+end
+
+function aux.settings_to_hash_strict(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        lpegmatch(pattern_c_s,str)
+        return next(hash) and hash
+    else
+        return nil
+    end
+end
+
+local separator = comma * space^0
+local value     = lpeg.P(lbrace * lpeg.C((nobrace + nested)^0) * rbrace) + lpeg.C((nested + (1-comma))^0)
+local pattern   = lpeg.Ct(value*(separator*value)^0)
+
+-- "aap, {noot}, mies" : outer {} removes, leading spaces ignored
+
+aux.settings_to_array_pattern = pattern
+
+-- we could use a weak table as cache
+
+function aux.settings_to_array(str)
+    if not str or str == "" then
+        return { }
+    else
+        return lpegmatch(pattern,str)
+    end
+end
+
+local function set(t,v)
+    t[#t+1] = v
+end
+
+local value   = lpeg.P(lpeg.Carg(1)*value) / set
+local pattern = value*(separator*value)^0 * lpeg.Carg(1)
+
+function aux.add_settings_to_array(t,str)
+    return lpegmatch(pattern,str,nil,t)
+end
+
+function aux.hash_to_string(h,separator,yes,no,strict,omit)
+    if h then
+        local t, s = { }, table.sortedkeys(h)
+        omit = omit and table.tohash(omit)
+        for i=1,#s do
+            local key = s[i]
+            if not omit or not omit[key] then
+                local value = h[key]
+                if type(value) == "boolean" then
+                    if yes and no then
+                        if value then
+                            t[#t+1] = key .. '=' .. yes
+                        elseif not strict then
+                            t[#t+1] = key .. '=' .. no
+                        end
+                    elseif value or not strict then
+                        t[#t+1] = key .. '=' .. tostring(value)
+                    end
+                else
+                    t[#t+1] = key .. '=' .. value
+                end
+            end
+        end
+        return concat(t,separator or ",")
+    else
+        return ""
+    end
+end
+
+function aux.array_to_string(a,separator)
+    if a then
+        return concat(a,separator or ",")
+    else
+        return ""
+    end
+end
+
+function aux.settings_to_set(str,t)
+    t = t or { }
+    for s in gmatch(str,"%s*([^,]+)") do
+        t[s] = true
+    end
+    return t
+end
+
+local value     = lbrace * lpeg.C((nobrace + nested)^0) * rbrace
+local pattern   = lpeg.Ct((space + value)^0)
+
+function aux.arguments_to_table(str)
+    return lpegmatch(pattern,str)
+end
+
+-- temporary here
+
+function aux.getparameters(self,class,parentclass,settings)
+    local sc = self[class]
+    if not sc then
+        sc = table.clone(self[parent])
+        self[class] = sc
+    end
+    aux.settings_to_hash(settings,sc)
+end
+
+-- temporary here
+
+local digit         = lpeg.R("09")
+local period        = lpeg.P(".")
+local zero          = lpeg.P("0")
+local trailingzeros = zero^0 * -digit -- suggested by Roberto R
+local case_1        = period * trailingzeros / ""
+local case_2        = period * (digit - trailingzeros)^1 * (trailingzeros / "")
+local number        = digit^1 * (case_1 + case_2)
+local stripper      = lpeg.Cs((number + 1)^0)
+
+--~ local sample = "bla 11.00 bla 11 bla 0.1100 bla 1.00100 bla 0.00 bla 0.001 bla 1.1100 bla 0.100100100 bla 0.00100100100"
+--~ collectgarbage("collect")
+--~ str = string.rep(sample,10000)
+--~ local ts = os.clock()
+--~ lpegmatch(stripper,str)
+--~ print(#str, os.clock()-ts, lpegmatch(stripper,sample))
+
+lpeg.patterns.strip_zeros = stripper
+
+function aux.strip_zeros(str)
+    return lpegmatch(stripper,str)
+end
+
+function aux.definetable(target) -- defines undefined tables
+    local composed, t = nil, { }
+    for name in gmatch(target,"([^%.]+)") do
+        if composed then
+            composed = composed .. "." .. name
+        else
+            composed = name
+        end
+        t[#t+1] = format("%s = %s or { }",composed,composed)
+    end
+    return concat(t,"\n")
+end
+
+function aux.accesstable(target)
+    local t = _G
+    for name in gmatch(target,"([^%.]+)") do
+        t = t[name]
+    end
+    return t
+end
+
+-- as we use this a lot ...
+
+--~ function aux.cachefunction(action,weak)
+--~     local cache = { }
+--~     if weak then
+--~         setmetatable(cache, { __mode = "kv" } )
+--~     end
+--~     local function reminder(str)
+--~         local found = cache[str]
+--~         if not found then
+--~             found = action(str)
+--~             cache[str] = found
+--~         end
+--~         return found
+--~     end
+--~     return reminder, cache
+--~ end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-tra'] = {
+    version   = 1.001,
+    comment   = "companion to trac-tra.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- the <anonymous> tag is kind of generic and used for functions that are not
+-- bound to a variable, like node.new, node.copy etc (contrary to for instance
+-- node.has_attribute which is bound to a has_attribute local variable in mkiv)
+
+local debug = require "debug"
+
+local getinfo = debug.getinfo
+local type, next = type, next
+local concat = table.concat
+local format, find, lower, gmatch, gsub = string.format, string.find, string.lower, string.gmatch, string.gsub
+
+debugger = debugger or { }
+
+local counters = { }
+local names = { }
+
+-- one
+
+local function hook()
+    local f = getinfo(2,"f").func
+    local n = getinfo(2,"Sn")
+--  if n.what == "C" and n.name then print (n.namewhat .. ': ' .. n.name) end
+    if f then
+        local cf = counters[f]
+        if cf == nil then
+            counters[f] = 1
+            names[f] = n
+        else
+            counters[f] = cf + 1
+        end
+    end
+end
+local function getname(func)
+    local n = names[func]
+    if n then
+        if n.what == "C" then
+            return n.name or '<anonymous>'
+        else
+            -- source short_src linedefined what name namewhat nups func
+            local name = n.name or n.namewhat or n.what
+            if not name or name == "" then name = "?" end
+            return format("%s : %s : %s", n.short_src or "unknown source", n.linedefined or "--", name)
+        end
+    else
+        return "unknown"
+    end
+end
+function debugger.showstats(printer,threshold)
+    printer   = printer or texio.write or print
+    threshold = threshold or 0
+    local total, grandtotal, functions = 0, 0, 0
+    printer("\n") -- ugly but ok
+ -- table.sort(counters)
+    for func, count in next, counters do
+        if count > threshold then
+            local name = getname(func)
+            if not find(name,"for generator") then
+                printer(format("%8i  %s", count, name))
+                total = total + count
+            end
+        end
+        grandtotal = grandtotal + count
+        functions = functions + 1
+    end
+    printer(format("functions: %s, total: %s, grand total: %s, threshold: %s\n", functions, total, grandtotal, threshold))
+end
+
+-- two
+
+--~ local function hook()
+--~     local n = getinfo(2)
+--~     if n.what=="C" and not n.name then
+--~         local f = tostring(debug.traceback())
+--~         local cf = counters[f]
+--~         if cf == nil then
+--~             counters[f] = 1
+--~             names[f] = n
+--~         else
+--~             counters[f] = cf + 1
+--~         end
+--~     end
+--~ end
+--~ function debugger.showstats(printer,threshold)
+--~     printer   = printer or texio.write or print
+--~     threshold = threshold or 0
+--~     local total, grandtotal, functions = 0, 0, 0
+--~     printer("\n") -- ugly but ok
+--~  -- table.sort(counters)
+--~     for func, count in next, counters do
+--~         if count > threshold then
+--~             printer(format("%8i  %s", count, func))
+--~             total = total + count
+--~         end
+--~         grandtotal = grandtotal + count
+--~         functions = functions + 1
+--~     end
+--~     printer(format("functions: %s, total: %s, grand total: %s, threshold: %s\n", functions, total, grandtotal, threshold))
+--~ end
+
+-- rest
+
+function debugger.savestats(filename,threshold)
+    local f = io.open(filename,'w')
+    if f then
+        debugger.showstats(function(str) f:write(str) end,threshold)
+        f:close()
+    end
+end
+
+function debugger.enable()
+    debug.sethook(hook,"c")
+end
+
+function debugger.disable()
+    debug.sethook()
+--~ counters[debug.getinfo(2,"f").func] = nil
+end
+
+function debugger.tracing()
+    local n = tonumber(os.env['MTX.TRACE.CALLS']) or tonumber(os.env['MTX_TRACE_CALLS']) or 0
+    if n > 0 then
+        function debugger.tracing() return true  end ; return true
+    else
+        function debugger.tracing() return false end ; return false
+    end
+end
+
+--~ debugger.enable()
+
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+
+--~ debugger.disable()
+
+--~ print("")
+--~ debugger.showstats()
+--~ print("")
+--~ debugger.showstats(print,3)
+
+setters      = setters      or { }
+setters.data = setters.data or { }
+
+--~ local function set(t,what,value)
+--~     local data, done = t.data, t.done
+--~     if type(what) == "string" then
+--~         what = aux.settings_to_array(what) -- inefficient but ok
+--~     end
+--~     for i=1,#what do
+--~         local w = what[i]
+--~         for d, f in next, data do
+--~             if done[d] then
+--~                 -- prevent recursion due to wildcards
+--~             elseif find(d,w) then
+--~                 done[d] = true
+--~                 for i=1,#f do
+--~                     f[i](value)
+--~                 end
+--~             end
+--~         end
+--~     end
+--~ end
+
+local function set(t,what,value)
+    local data, done = t.data, t.done
+    if type(what) == "string" then
+        what = aux.settings_to_hash(what) -- inefficient but ok
+    end
+    for w, v in next, what do
+        if v == "" then
+            v = value
+        else
+            v = toboolean(v)
+        end
+        for d, f in next, data do
+            if done[d] then
+                -- prevent recursion due to wildcards
+            elseif find(d,w) then
+                done[d] = true
+                for i=1,#f do
+                    f[i](v)
+                end
+            end
+        end
+    end
+end
+
+local function reset(t)
+    for d, f in next, t.data do
+        for i=1,#f do
+            f[i](false)
+        end
+    end
+end
+
+local function enable(t,what)
+    set(t,what,true)
+end
+
+local function disable(t,what)
+    local data = t.data
+    if not what or what == "" then
+        t.done = { }
+        reset(t)
+    else
+        set(t,what,false)
+    end
+end
+
+function setters.register(t,what,...)
+    local data = t.data
+    what = lower(what)
+    local w = data[what]
+    if not w then
+        w = { }
+        data[what] = w
+    end
+    for _, fnc in next, { ... } do
+        local typ = type(fnc)
+        if typ == "function" then
+            w[#w+1] = fnc
+        elseif typ == "string" then
+            w[#w+1] = function(value) set(t,fnc,value,nesting) end
+        end
+    end
+end
+
+function setters.enable(t,what)
+    local e = t.enable
+    t.enable, t.done = enable, { }
+    enable(t,string.simpleesc(tostring(what)))
+    t.enable, t.done = e, { }
+end
+
+function setters.disable(t,what)
+    local e = t.disable
+    t.disable, t.done = disable, { }
+    disable(t,string.simpleesc(tostring(what)))
+    t.disable, t.done = e, { }
+end
+
+function setters.reset(t)
+    t.done = { }
+    reset(t)
+end
+
+function setters.list(t) -- pattern
+    local list = table.sortedkeys(t.data)
+    local user, system = { }, { }
+    for l=1,#list do
+        local what = list[l]
+        if find(what,"^%*") then
+            system[#system+1] = what
+        else
+            user[#user+1] = what
+        end
+    end
+    return user, system
+end
+
+function setters.show(t)
+    commands.writestatus("","")
+    local list = setters.list(t)
+    for k=1,#list do
+        commands.writestatus(t.name,list[k])
+    end
+    commands.writestatus("","")
+end
+
+-- we could have used a bit of oo and the trackers:enable syntax but
+-- there is already a lot of code around using the singular tracker
+
+-- we could make this into a module
+
+function setters.new(name)
+    local t
+    t = {
+        data     = { },
+        name     = name,
+        enable   = function(...) setters.enable  (t,...) end,
+        disable  = function(...) setters.disable (t,...) end,
+        register = function(...) setters.register(t,...) end,
+        list     = function(...) setters.list    (t,...) end,
+        show     = function(...) setters.show    (t,...) end,
+    }
+    setters.data[name] = t
+    return t
+end
+
+trackers    = setters.new("trackers")
+directives  = setters.new("directives")
+experiments = setters.new("experiments")
+
+-- nice trick: we overload two of the directives related functions with variants that
+-- do tracing (itself using a tracker) .. proof of concept
+
+local trace_directives  = false local trace_directives  = false  trackers.register("system.directives",  function(v) trace_directives  = v end)
+local trace_experiments = false local trace_experiments = false  trackers.register("system.experiments", function(v) trace_experiments = v end)
+
+local e = directives.enable
+local d = directives.disable
+
+function directives.enable(...)
+    commands.writestatus("directives","enabling: %s",concat({...}," "))
+    e(...)
+end
+
+function directives.disable(...)
+    commands.writestatus("directives","disabling: %s",concat({...}," "))
+    d(...)
+end
+
+local e = experiments.enable
+local d = experiments.disable
+
+function experiments.enable(...)
+    commands.writestatus("experiments","enabling: %s",concat({...}," "))
+    e(...)
+end
+
+function experiments.disable(...)
+    commands.writestatus("experiments","disabling: %s",concat({...}," "))
+    d(...)
+end
+
+-- a useful example
+
+directives.register("system.nostatistics", function(v)
+    statistics.enable = not v
+end)
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['luat-env'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- A former version provided functionality for non embeded core
+-- scripts i.e. runtime library loading. Given the amount of
+-- Lua code we use now, this no longer makes sense. Much of this
+-- evolved before bytecode arrays were available and so a lot of
+-- code has disappeared already.
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+local format, sub, match, gsub, find = string.format, string.sub, string.match, string.gsub, string.find
+local unquote, quote = string.unquote, string.quote
+
+-- precautions
+
+os.setlocale(nil,nil) -- useless feature and even dangerous in luatex
+
+function os.setlocale()
+    -- no way you can mess with it
+end
+
+-- dirty tricks
+
+if arg and (arg[0] == 'luatex' or arg[0] == 'luatex.exe') and arg[1] == "--luaonly" then
+    arg[-1]=arg[0] arg[0]=arg[2] for k=3,#arg do arg[k-2]=arg[k] end arg[#arg]=nil arg[#arg]=nil
+end
+
+if profiler and os.env["MTX_PROFILE_RUN"] == "YES" then
+    profiler.start("luatex-profile.log")
+end
+
+-- environment
+
+environment             = environment or { }
+environment.arguments   = { }
+environment.files       = { }
+environment.sortedflags = nil
+
+if not environment.jobname or environment.jobname == "" then if tex then environment.jobname = tex.jobname end end
+if not environment.version or environment.version == "" then             environment.version = "unknown"   end
+if not environment.jobname                              then             environment.jobname = "unknown"   end
+
+function environment.initialize_arguments(arg)
+    local arguments, files = { }, { }
+    environment.arguments, environment.files, environment.sortedflags = arguments, files, nil
+    for index=1,#arg do
+        local argument = arg[index]
+        if index > 0 then
+            local flag, value = match(argument,"^%-+(.-)=(.-)$")
+            if flag then
+                arguments[flag] = unquote(value or "")
+            else
+                flag = match(argument,"^%-+(.+)")
+                if flag then
+                    arguments[flag] = true
+                else
+                    files[#files+1] = argument
+                end
+            end
+        end
+    end
+    environment.ownname = environment.ownname or arg[0] or 'unknown.lua'
+end
+
+function environment.setargument(name,value)
+    environment.arguments[name] = value
+end
+
+-- todo: defaults, better checks e.g on type (boolean versus string)
+--
+-- tricky: too many hits when we support partials unless we add
+-- a registration of arguments so from now on we have 'partial'
+
+function environment.argument(name,partial)
+    local arguments, sortedflags = environment.arguments, environment.sortedflags
+    if arguments[name] then
+        return arguments[name]
+    elseif partial then
+        if not sortedflags then
+            sortedflags = table.sortedkeys(arguments)
+            for k=1,#sortedflags do
+                sortedflags[k] = "^" .. sortedflags[k]
+            end
+            environment.sortedflags = sortedflags
+        end
+        -- example of potential clash: ^mode ^modefile
+        for k=1,#sortedflags do
+            local v = sortedflags[k]
+            if find(name,v) then
+                return arguments[sub(v,2,#v)]
+            end
+        end
+    end
+    return nil
+end
+
+environment.argument("x",true)
+
+function environment.split_arguments(separator) -- rather special, cut-off before separator
+    local done, before, after = false, { }, { }
+    local original_arguments = environment.original_arguments
+    for k=1,#original_arguments do
+        local v = original_arguments[k]
+        if not done and v == separator then
+            done = true
+        elseif done then
+            after[#after+1] = v
+        else
+            before[#before+1] = v
+        end
+    end
+    return before, after
+end
+
+function environment.reconstruct_commandline(arg,noquote)
+    arg = arg or environment.original_arguments
+    if noquote and #arg == 1 then
+        local a = arg[1]
+        a = resolvers.resolve(a)
+        a = unquote(a)
+        return a
+    elseif #arg > 0 then
+        local result = { }
+        for i=1,#arg do
+            local a = arg[i]
+            a = resolvers.resolve(a)
+            a = unquote(a)
+            a = gsub(a,'"','\\"') -- tricky
+            if find(a," ") then
+                result[#result+1] = quote(a)
+            else
+                result[#result+1] = a
+            end
+        end
+        return table.join(result," ")
+    else
+        return ""
+    end
+end
+
+if arg then
+
+    -- new, reconstruct quoted snippets (maybe better just remove the " then and add them later)
+    local newarg, instring = { }, false
+
+    for index=1,#arg do
+        local argument = arg[index]
+        if find(argument,"^\"") then
+            newarg[#newarg+1] = gsub(argument,"^\"","")
+            if not find(argument,"\"$") then
+                instring = true
+            end
+        elseif find(argument,"\"$") then
+            newarg[#newarg] = newarg[#newarg] .. " " .. gsub(argument,"\"$","")
+            instring = false
+        elseif instring then
+            newarg[#newarg] = newarg[#newarg] .. " " .. argument
+        else
+            newarg[#newarg+1] = argument
+        end
+    end
+    for i=1,-5,-1 do
+        newarg[i] = arg[i]
+    end
+
+    environment.initialize_arguments(newarg)
+    environment.original_arguments = newarg
+    environment.raw_arguments = arg
+
+    arg = { } -- prevent duplicate handling
+
+end
+
+-- weird place ... depends on a not yet loaded module
+
+function environment.texfile(filename)
+    return resolvers.find_file(filename,'tex')
+end
+
+function environment.luafile(filename)
+    local resolved = resolvers.find_file(filename,'tex') or ""
+    if resolved ~= "" then
+        return resolved
+    end
+    resolved = resolvers.find_file(filename,'texmfscripts') or ""
+    if resolved ~= "" then
+        return resolved
+    end
+    return resolvers.find_file(filename,'luatexlibs') or ""
+end
+
+environment.loadedluacode = loadfile -- can be overloaded
+
+--~ function environment.loadedluacode(name)
+--~     if os.spawn("texluac -s -o texluac.luc " .. name) == 0 then
+--~         local chunk = loadstring(io.loaddata("texluac.luc"))
+--~         os.remove("texluac.luc")
+--~         return chunk
+--~     else
+--~         environment.loadedluacode = loadfile -- can be overloaded
+--~         return loadfile(name)
+--~     end
+--~ end
+
+function environment.luafilechunk(filename) -- used for loading lua bytecode in the format
+    filename = file.replacesuffix(filename, "lua")
+    local fullname = environment.luafile(filename)
+    if fullname and fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading file %s", fullname)
+        end
+        return environment.loadedluacode(fullname)
+    else
+        if trace_locating then
+            logs.report("fileio","unknown file %s", filename)
+        end
+        return nil
+    end
+end
+
+-- the next ones can use the previous ones / combine
+
+function environment.loadluafile(filename, version)
+    local lucname, luaname, chunk
+    local basename = file.removesuffix(filename)
+    if basename == filename then
+        lucname, luaname = basename .. ".luc",  basename .. ".lua"
+    else
+        lucname, luaname = nil, basename -- forced suffix
+    end
+    -- when not overloaded by explicit suffix we look for a luc file first
+    local fullname = (lucname and environment.luafile(lucname)) or ""
+    if fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading %s", fullname)
+        end
+        chunk = loadfile(fullname) -- this way we don't need a file exists check
+    end
+    if chunk then
+        assert(chunk)()
+        if version then
+            -- we check of the version number of this chunk matches
+            local v = version -- can be nil
+            if modules and modules[filename] then
+                v = modules[filename].version -- new method
+            elseif versions and versions[filename] then
+                v = versions[filename]        -- old method
+            end
+            if v == version then
+                return true
+            else
+                if trace_locating then
+                    logs.report("fileio","version mismatch for %s: lua=%s, luc=%s", filename, v, version)
+                end
+                environment.loadluafile(filename)
+            end
+        else
+            return true
+        end
+    end
+    fullname = (luaname and environment.luafile(luaname)) or ""
+    if fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading %s", fullname)
+        end
+        chunk = loadfile(fullname) -- this way we don't need a file exists check
+        if not chunk then
+            if trace_locating then
+                logs.report("fileio","unknown file %s", filename)
+            end
+        else
+            assert(chunk)()
+            return true
+        end
+    end
+    return false
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-inf'] = {
+    version   = 1.001,
+    comment   = "companion to trac-inf.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format = string.format
+
+local statusinfo, n, registered = { }, 0, { }
+
+statistics = statistics or { }
+
+statistics.enable    = true
+statistics.threshold = 0.05
+
+-- timing functions
+
+local clock = os.gettimeofday or os.clock
+
+local notimer
+
+function statistics.hastimer(instance)
+    return instance and instance.starttime
+end
+
+function statistics.resettiming(instance)
+    if not instance then
+        notimer = { timing = 0, loadtime = 0 }
+    else
+        instance.timing, instance.loadtime = 0, 0
+    end
+end
+
+function statistics.starttiming(instance)
+    if not instance then
+        notimer = { }
+        instance = notimer
+    end
+    local it = instance.timing
+    if not it then
+        it = 0
+    end
+    if it == 0 then
+        instance.starttime = clock()
+        if not instance.loadtime then
+            instance.loadtime = 0
+        end
+    else
+--~         logs.report("system","nested timing (%s)",tostring(instance))
+    end
+    instance.timing = it + 1
+end
+
+function statistics.stoptiming(instance, report)
+    if not instance then
+        instance = notimer
+    end
+    if instance then
+        local it = instance.timing
+        if it > 1 then
+            instance.timing = it - 1
+        else
+            local starttime = instance.starttime
+            if starttime then
+                local stoptime = clock()
+                local loadtime = stoptime - starttime
+                instance.stoptime = stoptime
+                instance.loadtime = instance.loadtime + loadtime
+                if report then
+                    statistics.report("load time %0.3f",loadtime)
+                end
+                instance.timing = 0
+                return loadtime
+            end
+        end
+    end
+    return 0
+end
+
+function statistics.elapsedtime(instance)
+    if not instance then
+        instance = notimer
+    end
+    return format("%0.3f",(instance and instance.loadtime) or 0)
+end
+
+function statistics.elapsedindeed(instance)
+    if not instance then
+        instance = notimer
+    end
+    local t = (instance and instance.loadtime) or 0
+    return t > statistics.threshold
+end
+
+function statistics.elapsedseconds(instance,rest) -- returns nil if 0 seconds
+    if statistics.elapsedindeed(instance) then
+        return format("%s seconds %s", statistics.elapsedtime(instance),rest or "")
+    end
+end
+
+-- general function
+
+function statistics.register(tag,fnc)
+    if statistics.enable and type(fnc) == "function" then
+        local rt = registered[tag] or (#statusinfo + 1)
+        statusinfo[rt] = { tag, fnc }
+        registered[tag] = rt
+        if #tag > n then n = #tag end
+    end
+end
+
+function statistics.show(reporter)
+    if statistics.enable then
+        if not reporter then reporter = function(tag,data,n) texio.write_nl(tag .. " " .. data) end end
+        -- this code will move
+        local register = statistics.register
+        register("luatex banner", function()
+            return string.lower(status.banner)
+        end)
+        register("control sequences", function()
+            return format("%s of %s", status.cs_count, status.hash_size+status.hash_extra)
+        end)
+        register("callbacks", function()
+            local total, indirect = status.callbacks or 0, status.indirect_callbacks or 0
+            return format("direct: %s, indirect: %s, total: %s", total-indirect, indirect, total)
+        end)
+        register("current memory usage", statistics.memused)
+        register("runtime",statistics.runtime)
+--         --
+        for i=1,#statusinfo do
+            local s = statusinfo[i]
+            local r = s[2]()
+            if r then
+                reporter(s[1],r,n)
+            end
+        end
+        texio.write_nl("") -- final newline
+        statistics.enable = false
+    end
+end
+
+function statistics.show_job_stat(tag,data,n)
+    texio.write_nl(format("%-15s: %s - %s","mkiv lua stats",tag:rpadd(n," "),data))
+end
+
+function statistics.memused() -- no math.round yet -)
+    local round = math.round or math.floor
+    return format("%s MB (ctx: %s MB)",round(collectgarbage("count")/1000), round(status.luastate_bytes/1000000))
+end
+
+if statistics.runtime then
+    -- already loaded and set
+elseif luatex and luatex.starttime then
+    statistics.starttime = luatex.starttime
+    statistics.loadtime = 0
+    statistics.timing = 0
+else
+    statistics.starttiming(statistics)
+end
+
+function statistics.runtime()
+    statistics.stoptiming(statistics)
+    return statistics.formatruntime(statistics.elapsedtime(statistics))
+end
+
+function statistics.formatruntime(runtime)
+    return format("%s seconds", statistics.elapsedtime(statistics))
+end
+
+function statistics.timed(action,report)
+    local timer = { }
+    report = report or logs.simple
+    statistics.starttiming(timer)
+    action()
+    statistics.stoptiming(timer)
+    report("total runtime: %s",statistics.elapsedtime(timer))
+end
+
+-- where, not really the best spot for this:
+
+commands = commands or { }
+
+local timer
+
+function commands.resettimer()
+    statistics.resettiming(timer)
+    statistics.starttiming(timer)
+end
+
+function commands.elapsedtime()
+    statistics.stoptiming(timer)
+    tex.sprint(statistics.elapsedtime(timer))
+end
+
+commands.resettimer()
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-log'] = {
+    version   = 1.001,
+    comment   = "companion to trac-log.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- this is old code that needs an overhaul
+
+--~ io.stdout:setvbuf("no")
+--~ io.stderr:setvbuf("no")
+
+local write_nl, write = texio.write_nl or print, texio.write or io.write
+local format, gmatch = string.format, string.gmatch
+local texcount = tex and tex.count
+
+if texlua then
+    write_nl = print
+    write    = io.write
+end
+
+--[[ldx--
+<p>This is a prelude to a more extensive logging module. For the sake
+of parsing log files, in addition to the standard logging we will
+provide an <l n='xml'/> structured file. Actually, any logging that
+is hooked into callbacks will be \XML\ by default.</p>
+--ldx]]--
+
+logs     = logs     or { }
+logs.xml = logs.xml or { }
+logs.tex = logs.tex or { }
+
+--[[ldx--
+<p>This looks pretty ugly but we need to speed things up a bit.</p>
+--ldx]]--
+
+logs.moreinfo = [[
+more information about ConTeXt and the tools that come with it can be found at:
+
+maillist : ntg-context@ntg.nl / http://www.ntg.nl/mailman/listinfo/ntg-context
+webpage  : http://www.pragma-ade.nl / http://tex.aanhet.net
+wiki     : http://contextgarden.net
+]]
+
+logs.levels = {
+    ['error']   = 1,
+    ['warning'] = 2,
+    ['info']    = 3,
+    ['debug']   = 4,
+}
+
+logs.functions = {
+    'report', 'start', 'stop', 'push', 'pop', 'line', 'direct',
+    'start_run', 'stop_run',
+    'start_page_number', 'stop_page_number',
+    'report_output_pages', 'report_output_log',
+    'report_tex_stat', 'report_job_stat',
+    'show_open', 'show_close', 'show_load',
+}
+
+logs.tracers = {
+}
+
+logs.level = 0
+logs.mode  = string.lower((os.getenv("MTX.LOG.MODE") or os.getenv("MTX_LOG_MODE") or "tex"))
+
+function logs.set_level(level)
+    logs.level = logs.levels[level] or level
+end
+
+function logs.set_method(method)
+    for _, v in next, logs.functions do
+        logs[v] = logs[method][v] or function() end
+    end
+end
+
+-- tex logging
+
+function logs.tex.report(category,fmt,...) -- new
+    if fmt then
+        write_nl(category .. " | " .. format(fmt,...))
+    else
+        write_nl(category .. " |")
+    end
+end
+
+function logs.tex.line(fmt,...) -- new
+    if fmt then
+        write_nl(format(fmt,...))
+    else
+        write_nl("")
+    end
+end
+
+--~ function logs.tex.start_page_number()
+--~     local real, user, sub = texcount.realpageno, texcount.userpageno, texcount.subpageno
+--~     if real > 0 then
+--~         if user > 0 then
+--~             if sub > 0 then
+--~                 write(format("[%s.%s.%s",real,user,sub))
+--~             else
+--~                 write(format("[%s.%s",real,user))
+--~             end
+--~         else
+--~             write(format("[%s",real))
+--~         end
+--~     else
+--~         write("[-")
+--~     end
+--~ end
+
+--~ function logs.tex.stop_page_number()
+--~     write("]")
+--~ end
+
+local real, user, sub
+
+function logs.tex.start_page_number()
+    real, user, sub = texcount.realpageno, texcount.userpageno, texcount.subpageno
+end
+
+function logs.tex.stop_page_number()
+    if real > 0 then
+        if user > 0 then
+            if sub > 0 then
+                logs.report("pages", "flushing realpage %s, userpage %s, subpage %s",real,user,sub)
+            else
+                logs.report("pages", "flushing realpage %s, userpage %s",real,user)
+            end
+        else
+            logs.report("pages", "flushing realpage %s",real)
+        end
+    else
+        logs.report("pages", "flushing page")
+    end
+    io.flush()
+end
+
+logs.tex.report_job_stat = statistics.show_job_stat
+
+-- xml logging
+
+function logs.xml.report(category,fmt,...) -- new
+    if fmt then
+        write_nl(format("<r category='%s'>%s</r>",category,format(fmt,...)))
+    else
+        write_nl(format("<r category='%s'/>",category))
+    end
+end
+function logs.xml.line(fmt,...) -- new
+    if fmt then
+        write_nl(format("<r>%s</r>",format(fmt,...)))
+    else
+        write_nl("<r/>")
+    end
+end
+
+function logs.xml.start() if logs.level > 0 then tw("<%s>" ) end end
+function logs.xml.stop () if logs.level > 0 then tw("</%s>") end end
+function logs.xml.push () if logs.level > 0 then tw("<!-- ") end end
+function logs.xml.pop  () if logs.level > 0 then tw(" -->" ) end end
+
+function logs.xml.start_run()
+    write_nl("<?xml version='1.0' standalone='yes'?>")
+    write_nl("<job>") --  xmlns='www.pragma-ade.com/luatex/schemas/context-job.rng'
+    write_nl("")
+end
+
+function logs.xml.stop_run()
+    write_nl("</job>")
+end
+
+function logs.xml.start_page_number()
+    write_nl(format("<p real='%s' page='%s' sub='%s'", texcount.realpageno, texcount.userpageno, texcount.subpageno))
+end
+
+function logs.xml.stop_page_number()
+    write("/>")
+    write_nl("")
+end
+
+function logs.xml.report_output_pages(p,b)
+    write_nl(format("<v k='pages' v='%s'/>", p))
+    write_nl(format("<v k='bytes' v='%s'/>", b))
+    write_nl("")
+end
+
+function logs.xml.report_output_log()
+end
+
+function logs.xml.report_tex_stat(k,v)
+    texiowrite_nl("log","<v k='"..k.."'>"..tostring(v).."</v>")
+end
+
+local level = 0
+
+function logs.xml.show_open(name)
+    level = level + 1
+    texiowrite_nl(format("<f l='%s' n='%s'>",level,name))
+end
+
+function logs.xml.show_close(name)
+    texiowrite("</f> ")
+    level = level - 1
+end
+
+function logs.xml.show_load(name)
+    texiowrite_nl(format("<f l='%s' n='%s'/>",level+1,name))
+end
+
+--
+
+local name, banner = 'report', 'context'
+
+local function report(category,fmt,...)
+    if fmt then
+        write_nl(format("%s | %s: %s",name,category,format(fmt,...)))
+    elseif category then
+        write_nl(format("%s | %s",name,category))
+    else
+        write_nl(format("%s |",name))
+    end
+end
+
+local function simple(fmt,...)
+    if fmt then
+        write_nl(format("%s | %s",name,format(fmt,...)))
+    else
+        write_nl(format("%s |",name))
+    end
+end
+
+function logs.setprogram(_name_,_banner_,_verbose_)
+    name, banner = _name_, _banner_
+    if _verbose_ then
+        trackers.enable("resolvers.locating")
+    end
+    logs.set_method("tex")
+    logs.report = report -- also used in libraries
+    logs.simple = simple -- only used in scripts !
+    if utils then
+        utils.report = simple
+    end
+    logs.verbose = _verbose_
+end
+
+function logs.setverbose(what)
+    if what then
+        trackers.enable("resolvers.locating")
+    else
+        trackers.disable("resolvers.locating")
+    end
+    logs.verbose = what or false
+end
+
+function logs.extendbanner(_banner_,_verbose_)
+    banner = banner .. " | ".. _banner_
+    if _verbose_ ~= nil then
+        logs.setverbose(what)
+    end
+end
+
+logs.verbose = false
+logs.report  = logs.tex.report
+logs.simple  = logs.tex.report
+
+function logs.reportlines(str) -- todo: <lines></lines>
+    for line in gmatch(str,"(.-)[\n\r]") do
+        logs.report(line)
+    end
+end
+
+function logs.reportline() -- for scripts too
+    logs.report()
+end
+
+logs.simpleline = logs.reportline
+
+function logs.reportbanner() -- for scripts too
+    logs.report(banner)
+end
+
+function logs.help(message,option)
+    logs.reportbanner()
+    logs.reportline()
+    logs.reportlines(message)
+    local moreinfo = logs.moreinfo or ""
+    if moreinfo ~= "" and option ~= "nomoreinfo" then
+        logs.reportline()
+        logs.reportlines(moreinfo)
+    end
+end
+
+logs.set_level('error')
+logs.set_method('tex')
+
+function logs.system(whereto,process,jobname,category,...)
+    for i=1,10 do
+        local f = io.open(whereto,"a")
+        if f then
+            f:write(format("%s %s => %s => %s => %s\r",os.date("%d/%m/%y %H:%m:%S"),process,jobname,category,format(...)))
+            f:close()
+            break
+        else
+            sleep(0.1)
+        end
+    end
+end
+
+--~ local syslogname = "oeps.xxx"
+--~
+--~ for i=1,10 do
+--~     logs.system(syslogname,"context","test","fonts","font %s recached due to newer version (%s)","blabla","123")
+--~ end
+
+function logs.fatal(where,...)
+    logs.report(where,"fatal error: %s, aborting now",format(...))
+    os.exit()
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-inp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files",
+}
+
+-- After a few years using the code the large luat-inp.lua file
+-- has been split up a bit. In the process some functionality was
+-- dropped:
+--
+-- * support for reading lsr files
+-- * selective scanning (subtrees)
+-- * some public auxiliary functions were made private
+--
+-- TODO: os.getenv -> os.env[]
+-- TODO: instances.[hashes,cnffiles,configurations,522]
+-- TODO: check escaping in find etc, too much, too slow
+
+-- This lib is multi-purpose and can be loaded again later on so that
+-- additional functionality becomes available. We will split thislogs.report("fileio",
+-- module in components once we're done with prototyping. This is the
+-- first code I wrote for LuaTeX, so it needs some cleanup. Before changing
+-- something in this module one can best check with Taco or Hans first; there
+-- is some nasty trickery going on that relates to traditional kpse support.
+
+-- To be considered: hash key lowercase, first entry in table filename
+-- (any case), rest paths (so no need for optimization). Or maybe a
+-- separate table that matches lowercase names to mixed case when
+-- present. In that case the lower() cases can go away. I will do that
+-- only when we run into problems with names ... well ... Iwona-Regular.
+
+-- Beware, loading and saving is overloaded in luat-tmp!
+
+local format, gsub, find, lower, upper, match, gmatch = string.format, string.gsub, string.find, string.lower, string.upper, string.match, string.gmatch
+local concat, insert, sortedkeys = table.concat, table.insert, table.sortedkeys
+local next, type = next, type
+local lpegmatch = lpeg.match
+
+local trace_locating, trace_detail, trace_expansions = false, false, false
+
+trackers.register("resolvers.locating",   function(v) trace_locating   = v end)
+trackers.register("resolvers.details",    function(v) trace_detail     = v end)
+trackers.register("resolvers.expansions", function(v) trace_expansions = v end) -- todo
+
+if not resolvers then
+    resolvers = {
+        suffixes     = { },
+        formats      = { },
+        dangerous    = { },
+        suffixmap    = { },
+        alternatives = { },
+        locators     = { },  -- locate databases
+        hashers      = { },  -- load databases
+        generators   = { },  -- generate databases
+    }
+end
+
+local resolvers = resolvers
+
+resolvers.locators  .notfound = { nil }
+resolvers.hashers   .notfound = { nil }
+resolvers.generators.notfound = { nil }
+
+resolvers.cacheversion = '1.0.1'
+resolvers.cnfname      = 'texmf.cnf'
+resolvers.luaname      = 'texmfcnf.lua'
+resolvers.homedir      = os.env[os.type == "windows" and 'USERPROFILE'] or os.env['HOME'] or '~'
+resolvers.cnfdefault   = '{$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,.local,}/web2c}'
+
+local dummy_path_expr = "^!*unset/*$"
+
+local formats      = resolvers.formats
+local suffixes     = resolvers.suffixes
+local dangerous    = resolvers.dangerous
+local suffixmap    = resolvers.suffixmap
+local alternatives = resolvers.alternatives
+
+formats['afm'] = 'AFMFONTS'       suffixes['afm'] = { 'afm' }
+formats['enc'] = 'ENCFONTS'       suffixes['enc'] = { 'enc' }
+formats['fmt'] = 'TEXFORMATS'     suffixes['fmt'] = { 'fmt' }
+formats['map'] = 'TEXFONTMAPS'    suffixes['map'] = { 'map' }
+formats['mp']  = 'MPINPUTS'       suffixes['mp']  = { 'mp' }
+formats['ocp'] = 'OCPINPUTS'      suffixes['ocp'] = { 'ocp' }
+formats['ofm'] = 'OFMFONTS'       suffixes['ofm'] = { 'ofm', 'tfm' }
+formats['otf'] = 'OPENTYPEFONTS'  suffixes['otf'] = { 'otf' } -- 'ttf'
+formats['opl'] = 'OPLFONTS'       suffixes['opl'] = { 'opl' }
+formats['otp'] = 'OTPINPUTS'      suffixes['otp'] = { 'otp' }
+formats['ovf'] = 'OVFFONTS'       suffixes['ovf'] = { 'ovf', 'vf' }
+formats['ovp'] = 'OVPFONTS'       suffixes['ovp'] = { 'ovp' }
+formats['tex'] = 'TEXINPUTS'      suffixes['tex'] = { 'tex' }
+formats['tfm'] = 'TFMFONTS'       suffixes['tfm'] = { 'tfm' }
+formats['ttf'] = 'TTFONTS'        suffixes['ttf'] = { 'ttf', 'ttc', 'dfont' }
+formats['pfb'] = 'T1FONTS'        suffixes['pfb'] = { 'pfb', 'pfa' }
+formats['vf']  = 'VFFONTS'        suffixes['vf']  = { 'vf' }
+
+formats['fea'] = 'FONTFEATURES'   suffixes['fea'] = { 'fea' }
+formats['cid'] = 'FONTCIDMAPS'    suffixes['cid'] = { 'cid', 'cidmap' }
+
+formats ['texmfscripts'] = 'TEXMFSCRIPTS' -- new
+suffixes['texmfscripts'] = { 'rb', 'pl', 'py' } -- 'lua'
+
+formats ['lua'] = 'LUAINPUTS' -- new
+suffixes['lua'] = { 'lua', 'luc', 'tma', 'tmc' }
+
+-- backward compatible ones
+
+alternatives['map files']            = 'map'
+alternatives['enc files']            = 'enc'
+alternatives['cid maps']             = 'cid' -- great, why no cid files
+alternatives['font feature files']   = 'fea' -- and fea files here
+alternatives['opentype fonts']       = 'otf'
+alternatives['truetype fonts']       = 'ttf'
+alternatives['truetype collections'] = 'ttc'
+alternatives['truetype dictionary']  = 'dfont'
+alternatives['type1 fonts']          = 'pfb'
+
+-- obscure ones
+
+formats ['misc fonts'] = ''
+suffixes['misc fonts'] = { }
+
+formats     ['sfd']                      = 'SFDFONTS'
+suffixes    ['sfd']                      = { 'sfd' }
+alternatives['subfont definition files'] = 'sfd'
+
+-- lib paths
+
+formats ['lib'] = 'CLUAINPUTS' -- new (needs checking)
+suffixes['lib'] = (os.libsuffix and { os.libsuffix }) or { 'dll', 'so' }
+
+-- In practice we will work within one tds tree, but i want to keep
+-- the option open to build tools that look at multiple trees, which is
+-- why we keep the tree specific data in a table. We used to pass the
+-- instance but for practical pusposes we now avoid this and use a
+-- instance variable.
+
+-- here we catch a few new thingies (todo: add these paths to context.tmf)
+--
+-- FONTFEATURES  = .;$TEXMF/fonts/fea//
+-- FONTCIDMAPS   = .;$TEXMF/fonts/cid//
+
+-- we always have one instance active
+
+resolvers.instance = resolvers.instance or nil -- the current one (slow access)
+local instance = resolvers.instance or nil -- the current one (fast access)
+
+function resolvers.newinstance()
+
+    -- store once, freeze and faster (once reset we can best use
+    -- instance.environment) maybe better have a register suffix
+    -- function
+
+    for k, v in next, suffixes do
+        for i=1,#v do
+            local vi = v[i]
+            if vi then
+                suffixmap[vi] = k
+            end
+        end
+    end
+
+    -- because vf searching is somewhat dangerous, we want to prevent
+    -- too liberal searching esp because we do a lookup on the current
+    -- path anyway; only tex (or any) is safe
+
+    for k, v in next, formats do
+        dangerous[k] = true
+    end
+    dangerous.tex = nil
+
+    -- the instance
+
+    local newinstance = {
+        rootpath        = '',
+        treepath        = '',
+        progname        = 'context',
+        engine          = 'luatex',
+        format          = '',
+        environment     = { },
+        variables       = { },
+        expansions      = { },
+        files           = { },
+        remap           = { },
+        configuration   = { },
+        setup           = { },
+        order           = { },
+        found           = { },
+        foundintrees    = { },
+        kpsevars        = { },
+        hashes          = { },
+        cnffiles        = { },
+        luafiles        = { },
+        lists           = { },
+        remember        = true,
+        diskcache       = true,
+        renewcache      = false,
+        scandisk        = true,
+        cachepath       = nil,
+        loaderror       = false,
+        sortdata        = false,
+        savelists       = true,
+        cleanuppaths    = true,
+        allresults      = false,
+        pattern         = nil, -- lists
+        data            = { }, -- only for loading
+        force_suffixes  = true,
+        fakepaths       = { },
+    }
+
+    local ne = newinstance.environment
+
+    for k,v in next, os.env do
+        ne[k] = resolvers.bare_variable(v)
+    end
+
+    return newinstance
+
+end
+
+function resolvers.setinstance(someinstance)
+    instance = someinstance
+    resolvers.instance = someinstance
+    return someinstance
+end
+
+function resolvers.reset()
+    return resolvers.setinstance(resolvers.newinstance())
+end
+
+local function reset_hashes()
+    instance.lists = { }
+    instance.found = { }
+end
+
+local function check_configuration() -- not yet ok, no time for debugging now
+    local ie, iv = instance.environment, instance.variables
+    local function fix(varname,default)
+        local proname = varname .. "." .. instance.progname or "crap"
+        local p, v = ie[proname], ie[varname] or iv[varname]
+        if not ((p and p ~= "") or (v and v ~= "")) then
+            iv[varname] = default -- or environment?
+        end
+    end
+    local name = os.name
+    if name == "windows" then
+        fix("OSFONTDIR", "c:/windows/fonts//")
+    elseif name == "macosx" then
+        fix("OSFONTDIR", "$HOME/Library/Fonts//;/Library/Fonts//;/System/Library/Fonts//")
+    else
+        -- bad luck
+    end
+    fix("LUAINPUTS"   , ".;$TEXINPUTS;$TEXMFSCRIPTS") -- no progname, hm
+    -- this will go away some day
+    fix("FONTFEATURES", ".;$TEXMF/fonts/{data,fea}//;$OPENTYPEFONTS;$TTFONTS;$T1FONTS;$AFMFONTS")
+    fix("FONTCIDMAPS" , ".;$TEXMF/fonts/{data,cid}//;$OPENTYPEFONTS;$TTFONTS;$T1FONTS;$AFMFONTS")
+    --
+    fix("LUATEXLIBS"  , ".;$TEXMF/luatex/lua//")
+end
+
+function resolvers.bare_variable(str) -- assumes str is a string
+    return (gsub(str,"\s*([\"\']?)(.+)%1\s*", "%2"))
+end
+
+function resolvers.settrace(n) -- no longer number but: 'locating' or 'detail'
+    if n then
+        trackers.disable("resolvers.*")
+        trackers.enable("resolvers."..n)
+    end
+end
+
+resolvers.settrace(os.getenv("MTX_INPUT_TRACE"))
+
+function resolvers.osenv(key)
+    local ie = instance.environment
+    local value = ie[key]
+    if value == nil then
+     -- local e = os.getenv(key)
+        local e = os.env[key]
+        if e == nil then
+         -- value = "" -- false
+        else
+            value = resolvers.bare_variable(e)
+        end
+        ie[key] = value
+    end
+    return value or ""
+end
+
+function resolvers.env(key)
+    return instance.environment[key] or resolvers.osenv(key)
+end
+
+--
+
+local function expand_vars(lst) -- simple vars
+    local variables, env = instance.variables, resolvers.env
+    local function resolve(a)
+        return variables[a] or env(a)
+    end
+    for k=1,#lst do
+        lst[k] = gsub(lst[k],"%$([%a%d%_%-]+)",resolve)
+    end
+end
+
+local function expanded_var(var) -- simple vars
+    local function resolve(a)
+        return instance.variables[a] or resolvers.env(a)
+    end
+    return (gsub(var,"%$([%a%d%_%-]+)",resolve))
+end
+
+local function entry(entries,name)
+    if name and (name ~= "") then
+        name = gsub(name,'%$','')
+        local result = entries[name..'.'..instance.progname] or entries[name]
+        if result then
+            return result
+        else
+            result = resolvers.env(name)
+            if result then
+                instance.variables[name] = result
+                resolvers.expand_variables()
+                return instance.expansions[name] or ""
+            end
+        end
+    end
+    return ""
+end
+
+local function is_entry(entries,name)
+    if name and name ~= "" then
+        name = gsub(name,'%$','')
+        return (entries[name..'.'..instance.progname] or entries[name]) ~= nil
+    else
+        return false
+    end
+end
+
+-- {a,b,c,d}
+-- a,b,c/{p,q,r},d
+-- a,b,c/{p,q,r}/d/{x,y,z}//
+-- a,b,c/{p,q/{x,y,z},r},d/{p,q,r}
+-- a,b,c/{p,q/{x,y,z},r},d/{p,q,r}
+-- a{b,c}{d,e}f
+-- {a,b,c,d}
+-- {a,b,c/{p,q,r},d}
+-- {a,b,c/{p,q,r}/d/{x,y,z}//}
+-- {a,b,c/{p,q/{x,y,z}},d/{p,q,r}}
+-- {a,b,c/{p,q/{x,y,z},w}v,d/{p,q,r}}
+-- {$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,.local,}/web2c}
+
+-- this one is better and faster, but it took me a while to realize
+-- that this kind of replacement is cleaner than messy parsing and
+-- fuzzy concatenating we can probably gain a bit with selectively
+-- applying lpeg, but experiments with lpeg parsing this proved not to
+-- work that well; the parsing is ok, but dealing with the resulting
+-- table is a pain because we need to work inside-out recursively
+
+local function do_first(a,b)
+    local t = { }
+    for s in gmatch(b,"[^,]+") do t[#t+1] = a .. s end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_second(a,b)
+    local t = { }
+    for s in gmatch(a,"[^,]+") do t[#t+1] = s .. b end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_both(a,b)
+    local t = { }
+    for sa in gmatch(a,"[^,]+") do
+        for sb in gmatch(b,"[^,]+") do
+            t[#t+1] = sa .. sb
+        end
+    end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_three(a,b,c)
+    return a .. b.. c
+end
+
+local function splitpathexpr(str, t, validate)
+    -- no need for further optimization as it is only called a
+    -- few times, we can use lpeg for the sub
+    if trace_expansions then
+        logs.report("fileio","expanding variable '%s'",str)
+    end
+    t = t or { }
+    str = gsub(str,",}",",@}")
+    str = gsub(str,"{,","{@,")
+ -- str = "@" .. str .. "@"
+    local ok, done
+    while true do
+        done = false
+        while true do
+            str, ok = gsub(str,"([^{},]+){([^{}]+)}",do_first)
+            if ok > 0 then done = true else break end
+        end
+        while true do
+            str, ok = gsub(str,"{([^{}]+)}([^{},]+)",do_second)
+            if ok > 0 then done = true else break end
+        end
+        while true do
+            str, ok = gsub(str,"{([^{}]+)}{([^{}]+)}",do_both)
+            if ok > 0 then done = true else break end
+        end
+        str, ok = gsub(str,"({[^{}]*){([^{}]+)}([^{}]*})",do_three)
+        if ok > 0 then done = true end
+        if not done then break end
+    end
+    str = gsub(str,"[{}]", "")
+    str = gsub(str,"@","")
+    if validate then
+        for s in gmatch(str,"[^,]+") do
+            s = validate(s)
+            if s then t[#t+1] = s end
+        end
+    else
+        for s in gmatch(str,"[^,]+") do
+            t[#t+1] = s
+        end
+    end
+    if trace_expansions then
+        for k=1,#t do
+            logs.report("fileio","% 4i: %s",k,t[k])
+        end
+    end
+    return t
+end
+
+local function expanded_path_from_list(pathlist) -- maybe not a list, just a path
+    -- a previous version fed back into pathlist
+    local newlist, ok = { }, false
+    for k=1,#pathlist do
+        if find(pathlist[k],"[{}]") then
+            ok = true
+            break
+        end
+    end
+    if ok then
+        local function validate(s)
+            s = file.collapse_path(s)
+            return s ~= "" and not find(s,dummy_path_expr) and s
+        end
+        for k=1,#pathlist do
+            splitpathexpr(pathlist[k],newlist,validate)
+        end
+    else
+        for k=1,#pathlist do
+            for p in gmatch(pathlist[k],"([^,]+)") do
+                p = file.collapse_path(p)
+                if p ~= "" then newlist[#newlist+1] = p end
+            end
+        end
+    end
+    return newlist
+end
+
+-- we follow a rather traditional approach:
+--
+-- (1) texmf.cnf given in TEXMFCNF
+-- (2) texmf.cnf searched in default variable
+--
+-- also we now follow the stupid route: if not set then just assume *one*
+-- cnf file under texmf (i.e. distribution)
+
+local args = environment and environment.original_arguments or arg -- this needs a cleanup
+
+resolvers.ownbin = resolvers.ownbin or args[-2] or arg[-2] or args[-1] or arg[-1] or arg[0] or "luatex"
+resolvers.ownbin = gsub(resolvers.ownbin,"\\","/")
+
+function resolvers.getownpath()
+    local ownpath = resolvers.ownpath or os.selfdir
+    if not ownpath or ownpath == "" or ownpath == "unset" then
+        ownpath = args[-1] or arg[-1]
+        ownpath = ownpath and file.dirname(gsub(ownpath,"\\","/"))
+        if not ownpath or ownpath == "" then
+            ownpath = args[-0] or arg[-0]
+            ownpath = ownpath and file.dirname(gsub(ownpath,"\\","/"))
+        end
+        local binary = resolvers.ownbin
+        if not ownpath or ownpath == "" then
+            ownpath = ownpath and file.dirname(binary)
+        end
+        if not ownpath or ownpath == "" then
+            if os.binsuffix ~= "" then
+                binary = file.replacesuffix(binary,os.binsuffix)
+            end
+            for p in gmatch(os.getenv("PATH"),"[^"..io.pathseparator.."]+") do
+                local b = file.join(p,binary)
+                if lfs.isfile(b) then
+                    -- we assume that after changing to the path the currentdir function
+                    -- resolves to the real location and use this side effect here; this
+                    -- trick is needed because on the mac installations use symlinks in the
+                    -- path instead of real locations
+                    local olddir = lfs.currentdir()
+                    if lfs.chdir(p) then
+                        local pp = lfs.currentdir()
+                        if trace_locating and p ~= pp then
+                            logs.report("fileio","following symlink '%s' to '%s'",p,pp)
+                        end
+                        ownpath = pp
+                        lfs.chdir(olddir)
+                    else
+                        if trace_locating then
+                            logs.report("fileio","unable to check path '%s'",p)
+                        end
+                        ownpath =  p
+                    end
+                    break
+                end
+            end
+        end
+        if not ownpath or ownpath == "" then
+            ownpath = "."
+            logs.report("fileio","forcing fallback ownpath .")
+        elseif trace_locating then
+            logs.report("fileio","using ownpath '%s'",ownpath)
+        end
+    end
+    resolvers.ownpath = ownpath
+    function resolvers.getownpath()
+        return resolvers.ownpath
+    end
+    return ownpath
+end
+
+local own_places = { "SELFAUTOLOC", "SELFAUTODIR", "SELFAUTOPARENT", "TEXMFCNF" }
+
+local function identify_own()
+    local ownpath = resolvers.getownpath() or dir.current()
+    local ie = instance.environment
+    if ownpath then
+        if resolvers.env('SELFAUTOLOC')    == "" then os.env['SELFAUTOLOC']    = file.collapse_path(ownpath) end
+        if resolvers.env('SELFAUTODIR')    == "" then os.env['SELFAUTODIR']    = file.collapse_path(ownpath .. "/..") end
+        if resolvers.env('SELFAUTOPARENT') == "" then os.env['SELFAUTOPARENT'] = file.collapse_path(ownpath .. "/../..") end
+    else
+        logs.report("fileio","error: unable to locate ownpath")
+        os.exit()
+    end
+    if resolvers.env('TEXMFCNF') == "" then os.env['TEXMFCNF'] = resolvers.cnfdefault end
+    if resolvers.env('TEXOS')    == "" then os.env['TEXOS']    = resolvers.env('SELFAUTODIR') end
+    if resolvers.env('TEXROOT')  == "" then os.env['TEXROOT']  = resolvers.env('SELFAUTOPARENT') end
+    if trace_locating then
+        for i=1,#own_places do
+            local v = own_places[i]
+            logs.report("fileio","variable '%s' set to '%s'",v,resolvers.env(v) or "unknown")
+        end
+    end
+    identify_own = function() end
+end
+
+function resolvers.identify_cnf()
+    if #instance.cnffiles == 0 then
+        -- fallback
+        identify_own()
+        -- the real search
+        resolvers.expand_variables()
+        local t = resolvers.split_path(resolvers.env('TEXMFCNF'))
+        t = expanded_path_from_list(t)
+        expand_vars(t) -- redundant
+        local function locate(filename,list)
+            for i=1,#t do
+                local ti = t[i]
+                local texmfcnf = file.collapse_path(file.join(ti,filename))
+                if lfs.isfile(texmfcnf) then
+                    list[#list+1] = texmfcnf
+                end
+            end
+        end
+        locate(resolvers.luaname,instance.luafiles)
+        locate(resolvers.cnfname,instance.cnffiles)
+    end
+end
+
+local function load_cnf_file(fname)
+    fname = resolvers.clean_path(fname)
+    local lname = file.replacesuffix(fname,'lua')
+    if lfs.isfile(lname) then
+        local dname = file.dirname(fname) -- fname ?
+        if not instance.configuration[dname] then
+            resolvers.load_data(dname,'configuration',lname and file.basename(lname))
+            instance.order[#instance.order+1] = instance.configuration[dname]
+        end
+    else
+        f = io.open(fname)
+        if f then
+            if trace_locating then
+                logs.report("fileio","loading configuration file %s", fname)
+            end
+            local line, data, n, k, v
+            local dname = file.dirname(fname)
+            if not instance.configuration[dname] then
+                instance.configuration[dname] = { }
+                instance.order[#instance.order+1] = instance.configuration[dname]
+            end
+            local data = instance.configuration[dname]
+            while true do
+                local line, n = f:read(), 0
+                if line then
+                    while true do -- join lines
+                        line, n = gsub(line,"\\%s*$", "")
+                        if n > 0 then
+                            line = line .. f:read()
+                        else
+                            break
+                        end
+                    end
+                    if not find(line,"^[%%#]") then
+                        local l = gsub(line,"%s*%%.*$","")
+                        local k, v = match(l,"%s*(.-)%s*=%s*(.-)%s*$")
+                        if k and v and not data[k] then
+                            v = gsub(v,"[%%#].*",'')
+                            data[k] = gsub(v,"~","$HOME")
+                            instance.kpsevars[k] = true
+                        end
+                    end
+                else
+                    break
+                end
+            end
+            f:close()
+        elseif trace_locating then
+            logs.report("fileio","skipping configuration file '%s'", fname)
+        end
+    end
+end
+
+local function collapse_cnf_data() -- potential optimization: pass start index (setup and configuration are shared)
+    local order = instance.order
+    for i=1,#order do
+        local c = order[i]
+        for k,v in next, c do
+            if not instance.variables[k] then
+                if instance.environment[k] then
+                    instance.variables[k] = instance.environment[k]
+                else
+                    instance.kpsevars[k] = true
+                    instance.variables[k] = resolvers.bare_variable(v)
+                end
+            end
+        end
+    end
+end
+
+function resolvers.load_cnf()
+    local function loadoldconfigdata()
+        local cnffiles = instance.cnffiles
+        for i=1,#cnffiles do
+            load_cnf_file(cnffiles[i])
+        end
+    end
+    -- instance.cnffiles contain complete names now !
+    -- we still use a funny mix of cnf and new but soon
+    -- we will switch to lua exclusively as we only use
+    -- the file to collect the tree roots
+    if #instance.cnffiles == 0 then
+        if trace_locating then
+            logs.report("fileio","no cnf files found (TEXMFCNF may not be set/known)")
+        end
+    else
+        local cnffiles = instance.cnffiles
+        instance.rootpath = cnffiles[1]
+        for k=1,#cnffiles do
+            instance.cnffiles[k] = file.collapse_path(cnffiles[k])
+        end
+        for i=1,3 do
+            instance.rootpath = file.dirname(instance.rootpath)
+        end
+        instance.rootpath = file.collapse_path(instance.rootpath)
+        if instance.diskcache and not instance.renewcache then
+            resolvers.loadoldconfig(instance.cnffiles)
+            if instance.loaderror then
+                loadoldconfigdata()
+                resolvers.saveoldconfig()
+            end
+        else
+            loadoldconfigdata()
+            if instance.renewcache then
+                resolvers.saveoldconfig()
+            end
+        end
+        collapse_cnf_data()
+    end
+    check_configuration()
+end
+
+function resolvers.load_lua()
+    if #instance.luafiles == 0 then
+        -- yet harmless
+    else
+        instance.rootpath = instance.luafiles[1]
+        local luafiles = instance.luafiles
+        for k=1,#luafiles do
+            instance.luafiles[k] = file.collapse_path(luafiles[k])
+        end
+        for i=1,3 do
+            instance.rootpath = file.dirname(instance.rootpath)
+        end
+        instance.rootpath = file.collapse_path(instance.rootpath)
+        resolvers.loadnewconfig()
+        collapse_cnf_data()
+    end
+    check_configuration()
+end
+
+-- database loading
+
+function resolvers.load_hash()
+    resolvers.locatelists()
+    if instance.diskcache and not instance.renewcache then
+        resolvers.loadfiles()
+        if instance.loaderror then
+            resolvers.loadlists()
+            resolvers.savefiles()
+        end
+    else
+        resolvers.loadlists()
+        if instance.renewcache then
+            resolvers.savefiles()
+        end
+    end
+end
+
+function resolvers.append_hash(type,tag,name)
+    if trace_locating then
+        logs.report("fileio","hash '%s' appended",tag)
+    end
+    insert(instance.hashes, { ['type']=type, ['tag']=tag, ['name']=name } )
+end
+
+function resolvers.prepend_hash(type,tag,name)
+    if trace_locating then
+        logs.report("fileio","hash '%s' prepended",tag)
+    end
+    insert(instance.hashes, 1, { ['type']=type, ['tag']=tag, ['name']=name } )
+end
+
+function resolvers.extend_texmf_var(specification) -- crap, we could better prepend the hash
+--  local t = resolvers.expanded_path_list('TEXMF') -- full expansion
+    local t = resolvers.split_path(resolvers.env('TEXMF'))
+    insert(t,1,specification)
+    local newspec = concat(t,";")
+    if instance.environment["TEXMF"] then
+        instance.environment["TEXMF"] = newspec
+    elseif instance.variables["TEXMF"] then
+        instance.variables["TEXMF"] = newspec
+    else
+        -- weird
+    end
+    resolvers.expand_variables()
+    reset_hashes()
+end
+
+-- locators
+
+function resolvers.locatelists()
+    local texmfpaths = resolvers.clean_path_list('TEXMF')
+    for i=1,#texmfpaths do
+        local path = texmfpaths[i]
+        if trace_locating then
+            logs.report("fileio","locating list of '%s'",path)
+        end
+        resolvers.locatedatabase(file.collapse_path(path))
+    end
+end
+
+function resolvers.locatedatabase(specification)
+    return resolvers.methodhandler('locators', specification)
+end
+
+function resolvers.locators.tex(specification)
+    if specification and specification ~= '' and lfs.isdir(specification) then
+        if trace_locating then
+            logs.report("fileio","tex locator '%s' found",specification)
+        end
+        resolvers.append_hash('file',specification,filename)
+    elseif trace_locating then
+        logs.report("fileio","tex locator '%s' not found",specification)
+    end
+end
+
+-- hashers
+
+function resolvers.hashdatabase(tag,name)
+    return resolvers.methodhandler('hashers',tag,name)
+end
+
+function resolvers.loadfiles()
+    instance.loaderror = false
+    instance.files = { }
+    if not instance.renewcache then
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            resolvers.hashdatabase(hash.tag,hash.name)
+            if instance.loaderror then break end
+        end
+    end
+end
+
+function resolvers.hashers.tex(tag,name)
+    resolvers.load_data(tag,'files')
+end
+
+-- generators:
+
+function resolvers.loadlists()
+    local hashes = instance.hashes
+    for i=1,#hashes do
+        resolvers.generatedatabase(hashes[i].tag)
+    end
+end
+
+function resolvers.generatedatabase(specification)
+    return resolvers.methodhandler('generators', specification)
+end
+
+-- starting with . or .. etc or funny char
+
+local weird = lpeg.P(".")^1 + lpeg.anywhere(lpeg.S("~`!#$%^&*()={}[]:;\"\'||<>,?\n\r\t"))
+
+--~ local l_forbidden = lpeg.S("~`!#$%^&*()={}[]:;\"\'||\\/<>,?\n\r\t")
+--~ local l_confusing = lpeg.P(" ")
+--~ local l_character = lpeg.patterns.utf8
+--~ local l_dangerous = lpeg.P(".")
+
+--~ local l_normal = (l_character - l_forbidden - l_confusing - l_dangerous) * (l_character - l_forbidden - l_confusing^2)^0 * lpeg.P(-1)
+--~ ----- l_normal = l_normal * lpeg.Cc(true) + lpeg.Cc(false)
+
+--~ local function test(str)
+--~     print(str,lpeg.match(l_normal,str))
+--~ end
+--~ test("ヒラギノ明朝 Pro W3")
+--~ test("..ヒラギノ明朝 Pro W3")
+--~ test(":ヒラギノ明朝 Pro W3;")
+--~ test("ヒラギノ明朝 /Pro W3;")
+--~ test("ヒラギノ明朝 Pro  W3")
+
+function resolvers.generators.tex(specification)
+    local tag = specification
+    if trace_locating then
+        logs.report("fileio","scanning path '%s'",specification)
+    end
+    instance.files[tag] = { }
+    local files = instance.files[tag]
+    local n, m, r = 0, 0, 0
+    local spec = specification .. '/'
+    local attributes = lfs.attributes
+    local directory = lfs.dir
+    local function action(path)
+        local full
+        if path then
+            full = spec .. path .. '/'
+        else
+            full = spec
+        end
+        for name in directory(full) do
+            if not lpegmatch(weird,name) then
+         -- if lpegmatch(l_normal,name) then
+                local mode = attributes(full..name,'mode')
+                if mode == 'file' then
+                    if path then
+                        n = n + 1
+                        local f = files[name]
+                        if f then
+                            if type(f) == 'string' then
+                                files[name] = { f, path }
+                            else
+                                f[#f+1] = path
+                            end
+                        else -- probably unique anyway
+                            files[name] = path
+                            local lower = lower(name)
+                            if name ~= lower then
+                                files["remap:"..lower] = name
+                                r = r + 1
+                            end
+                        end
+                    end
+                elseif mode == 'directory' then
+                    m = m + 1
+                    if path then
+                        action(path..'/'..name)
+                    else
+                        action(name)
+                    end
+                end
+            end
+        end
+    end
+    action()
+    if trace_locating then
+        logs.report("fileio","%s files found on %s directories with %s uppercase remappings",n,m,r)
+    end
+end
+
+-- savers, todo
+
+function resolvers.savefiles()
+    resolvers.save_data('files')
+end
+
+-- A config (optionally) has the paths split in tables. Internally
+-- we join them and split them after the expansion has taken place. This
+-- is more convenient.
+
+--~ local checkedsplit = string.checkedsplit
+
+local cache = { }
+
+local splitter = lpeg.Ct(lpeg.splitat(lpeg.S(os.type == "windows" and ";" or ":;")))
+
+local function split_kpse_path(str) -- beware, this can be either a path or a {specification}
+    local found = cache[str]
+    if not found then
+        if str == "" then
+            found = { }
+        else
+            str = gsub(str,"\\","/")
+--~             local split = (find(str,";") and checkedsplit(str,";")) or checkedsplit(str,io.pathseparator)
+local split = lpegmatch(splitter,str)
+            found = { }
+            for i=1,#split do
+                local s = split[i]
+                if not find(s,"^{*unset}*") then
+                    found[#found+1] = s
+                end
+            end
+            if trace_expansions then
+                logs.report("fileio","splitting path specification '%s'",str)
+                for k=1,#found do
+                    logs.report("fileio","% 4i: %s",k,found[k])
+                end
+            end
+            cache[str] = found
+        end
+    end
+    return found
+end
+
+resolvers.split_kpse_path = split_kpse_path
+
+function resolvers.splitconfig()
+    for i=1,#instance do
+        local c = instance[i]
+        for k,v in next, c do
+            if type(v) == 'string' then
+                local t = split_kpse_path(v)
+                if #t > 1 then
+                    c[k] = t
+                end
+            end
+        end
+    end
+end
+
+function resolvers.joinconfig()
+    local order = instance.order
+    for i=1,#order do
+        local c = order[i]
+        for k,v in next, c do -- indexed?
+            if type(v) == 'table' then
+                c[k] = file.join_path(v)
+            end
+        end
+    end
+end
+
+function resolvers.split_path(str)
+    if type(str) == 'table' then
+        return str
+    else
+        return split_kpse_path(str)
+    end
+end
+
+function resolvers.join_path(str)
+    if type(str) == 'table' then
+        return file.join_path(str)
+    else
+        return str
+    end
+end
+
+function resolvers.splitexpansions()
+    local ie = instance.expansions
+    for k,v in next, ie do
+        local t, h, p = { }, { }, split_kpse_path(v)
+        for kk=1,#p do
+            local vv = p[kk]
+            if vv ~= "" and not h[vv] then
+                t[#t+1] = vv
+                h[vv] = true
+            end
+        end
+        if #t > 1 then
+            ie[k] = t
+        else
+            ie[k] = t[1]
+        end
+    end
+end
+
+-- end of split/join code
+
+function resolvers.saveoldconfig()
+    resolvers.splitconfig()
+    resolvers.save_data('configuration')
+    resolvers.joinconfig()
+end
+
+resolvers.configbanner = [[
+-- This is a Luatex configuration file created by 'luatools.lua' or
+-- 'luatex.exe' directly. For comment, suggestions and questions you can
+-- contact the ConTeXt Development Team. This configuration file is
+-- not copyrighted. [HH & TH]
+]]
+
+function resolvers.serialize(files)
+    -- This version is somewhat optimized for the kind of
+    -- tables that we deal with, so it's much faster than
+    -- the generic serializer. This makes sense because
+    -- luatools and mtxtools are called frequently. Okay,
+    -- we pay a small price for properly tabbed tables.
+    local t = { }
+    local function dump(k,v,m) -- could be moved inline
+        if type(v) == 'string' then
+            return m .. "['" .. k .. "']='" .. v .. "',"
+        elseif #v == 1 then
+            return m .. "['" .. k .. "']='" .. v[1] .. "',"
+        else
+            return m .. "['" .. k .. "']={'" .. concat(v,"','").. "'},"
+        end
+    end
+    t[#t+1] = "return {"
+    if instance.sortdata then
+	local sortedfiles = sortedkeys(files)
+	for i=1,#sortedfiles do
+	    local k = sortedfiles[i]
+            local fk  = files[k]
+            if type(fk) == 'table' then
+                t[#t+1] = "\t['" .. k .. "']={"
+		local sortedfk = sortedkeys(fk)
+        	for j=1,#sortedfk do
+                    local kk = sortedfk[j]
+                    t[#t+1] = dump(kk,fk[kk],"\t\t")
+                end
+                t[#t+1] = "\t},"
+            else
+                t[#t+1] = dump(k,fk,"\t")
+            end
+        end
+    else
+        for k, v in next, files do
+            if type(v) == 'table' then
+                t[#t+1] = "\t['" .. k .. "']={"
+                for kk,vv in next, v do
+                    t[#t+1] = dump(kk,vv,"\t\t")
+                end
+                t[#t+1] = "\t},"
+            else
+                t[#t+1] = dump(k,v,"\t")
+            end
+        end
+    end
+    t[#t+1] = "}"
+    return concat(t,"\n")
+end
+
+local data_state = { }
+
+function resolvers.data_state()
+    return data_state or { }
+end
+
+function resolvers.save_data(dataname, makename) -- untested without cache overload
+    for cachename, files in next, instance[dataname] do
+        local name = (makename or file.join)(cachename,dataname)
+        local luaname, lucname = name .. ".lua", name .. ".luc"
+        if trace_locating then
+            logs.report("fileio","preparing '%s' for '%s'",dataname,cachename)
+        end
+        for k, v in next, files do
+            if type(v) == "table" and #v == 1 then
+                files[k] = v[1]
+            end
+        end
+        local data = {
+            type    = dataname,
+            root    = cachename,
+            version = resolvers.cacheversion,
+            date    = os.date("%Y-%m-%d"),
+            time    = os.date("%H:%M:%S"),
+            content = files,
+            uuid    = os.uuid(),
+        }
+        local ok = io.savedata(luaname,resolvers.serialize(data))
+        if ok then
+            if trace_locating then
+                logs.report("fileio","'%s' saved in '%s'",dataname,luaname)
+            end
+            if utils.lua.compile(luaname,lucname,false,true) then -- no cleanup but strip
+                if trace_locating then
+                    logs.report("fileio","'%s' compiled to '%s'",dataname,lucname)
+                end
+            else
+                if trace_locating then
+                    logs.report("fileio","compiling failed for '%s', deleting file '%s'",dataname,lucname)
+                end
+                os.remove(lucname)
+            end
+        elseif trace_locating then
+            logs.report("fileio","unable to save '%s' in '%s' (access error)",dataname,luaname)
+        end
+    end
+end
+
+function resolvers.load_data(pathname,dataname,filename,makename) -- untested without cache overload
+    filename = ((not filename or (filename == "")) and dataname) or filename
+    filename = (makename and makename(dataname,filename)) or file.join(pathname,filename)
+    local blob = loadfile(filename .. ".luc") or loadfile(filename .. ".lua")
+    if blob then
+        local data = blob()
+        if data and data.content and data.type == dataname and data.version == resolvers.cacheversion then
+            data_state[#data_state+1] = data.uuid
+            if trace_locating then
+                logs.report("fileio","loading '%s' for '%s' from '%s'",dataname,pathname,filename)
+            end
+            instance[dataname][pathname] = data.content
+        else
+            if trace_locating then
+                logs.report("fileio","skipping '%s' for '%s' from '%s'",dataname,pathname,filename)
+            end
+            instance[dataname][pathname] = { }
+            instance.loaderror = true
+        end
+    elseif trace_locating then
+        logs.report("fileio","skipping '%s' for '%s' from '%s'",dataname,pathname,filename)
+    end
+end
+
+-- some day i'll use the nested approach, but not yet (actually we even drop
+-- engine/progname support since we have only luatex now)
+--
+-- first texmfcnf.lua files are located, next the cached texmf.cnf files
+--
+-- return {
+--     TEXMFBOGUS = 'effe checken of dit werkt',
+-- }
+
+function resolvers.resetconfig()
+    identify_own()
+    instance.configuration, instance.setup, instance.order, instance.loaderror = { }, { }, { }, false
+end
+
+function resolvers.loadnewconfig()
+    local luafiles = instance.luafiles
+    for i=1,#luafiles do
+        local cnf = luafiles[i]
+        local pathname = file.dirname(cnf)
+        local filename = file.join(pathname,resolvers.luaname)
+        local blob = loadfile(filename)
+        if blob then
+            local data = blob()
+            if data then
+                if trace_locating then
+                    logs.report("fileio","loading configuration file '%s'",filename)
+                end
+                if true then
+                    -- flatten to variable.progname
+                    local t = { }
+                    for k, v in next, data do -- v = progname
+                        if type(v) == "string" then
+                            t[k] = v
+                        else
+                            for kk, vv in next, v do -- vv = variable
+                                if type(vv) == "string" then
+                                    t[vv.."."..v] = kk
+                                end
+                            end
+                        end
+                    end
+                    instance['setup'][pathname] = t
+                else
+                    instance['setup'][pathname] = data
+                end
+            else
+                if trace_locating then
+                    logs.report("fileio","skipping configuration file '%s'",filename)
+                end
+                instance['setup'][pathname] = { }
+                instance.loaderror = true
+            end
+        elseif trace_locating then
+            logs.report("fileio","skipping configuration file '%s'",filename)
+        end
+        instance.order[#instance.order+1] = instance.setup[pathname]
+        if instance.loaderror then break end
+    end
+end
+
+function resolvers.loadoldconfig()
+    if not instance.renewcache then
+        local cnffiles = instance.cnffiles
+        for i=1,#cnffiles do
+            local cnf = cnffiles[i]
+            local dname = file.dirname(cnf)
+            resolvers.load_data(dname,'configuration')
+            instance.order[#instance.order+1] = instance.configuration[dname]
+            if instance.loaderror then break end
+        end
+    end
+    resolvers.joinconfig()
+end
+
+function resolvers.expand_variables()
+    local expansions, environment, variables = { }, instance.environment, instance.variables
+    local env = resolvers.env
+    instance.expansions = expansions
+    if instance.engine   ~= "" then environment['engine']   = instance.engine   end
+    if instance.progname ~= "" then environment['progname'] = instance.progname end
+    for k,v in next, environment do
+        local a, b = match(k,"^(%a+)%_(.*)%s*$")
+        if a and b then
+            expansions[a..'.'..b] = v
+        else
+            expansions[k] = v
+        end
+    end
+    for k,v in next, environment do -- move environment to expansions
+        if not expansions[k] then expansions[k] = v end
+    end
+    for k,v in next, variables do -- move variables to expansions
+        if not expansions[k] then expansions[k] = v end
+    end
+    local busy = false
+    local function resolve(a)
+        busy = true
+        return expansions[a] or env(a)
+    end
+    while true do
+        busy = false
+        for k,v in next, expansions do
+            local s, n = gsub(v,"%$([%a%d%_%-]+)",resolve)
+            local s, m = gsub(s,"%$%{([%a%d%_%-]+)%}",resolve)
+            if n > 0 or m > 0 then
+                expansions[k]= s
+            end
+        end
+        if not busy then break end
+    end
+    for k,v in next, expansions do
+        expansions[k] = gsub(v,"\\", '/')
+    end
+end
+
+function resolvers.variable(name)
+    return entry(instance.variables,name)
+end
+
+function resolvers.expansion(name)
+    return entry(instance.expansions,name)
+end
+
+function resolvers.is_variable(name)
+    return is_entry(instance.variables,name)
+end
+
+function resolvers.is_expansion(name)
+    return is_entry(instance.expansions,name)
+end
+
+function resolvers.unexpanded_path_list(str)
+    local pth = resolvers.variable(str)
+    local lst = resolvers.split_path(pth)
+    return expanded_path_from_list(lst)
+end
+
+function resolvers.unexpanded_path(str)
+    return file.join_path(resolvers.unexpanded_path_list(str))
+end
+
+do -- no longer needed
+
+    local done = { }
+
+    function resolvers.reset_extra_path()
+        local ep = instance.extra_paths
+        if not ep then
+            ep, done = { }, { }
+            instance.extra_paths = ep
+        elseif #ep > 0 then
+            instance.lists, done = { }, { }
+        end
+    end
+
+    function resolvers.register_extra_path(paths,subpaths)
+        local ep = instance.extra_paths or { }
+        local n = #ep
+        if paths and paths ~= "" then
+            if subpaths and subpaths ~= "" then
+                for p in gmatch(paths,"[^,]+") do
+                    -- we gmatch each step again, not that fast, but used seldom
+                    for s in gmatch(subpaths,"[^,]+") do
+                        local ps = p .. "/" .. s
+                        if not done[ps] then
+                            ep[#ep+1] = resolvers.clean_path(ps)
+                            done[ps] = true
+                        end
+                    end
+                end
+            else
+                for p in gmatch(paths,"[^,]+") do
+                    if not done[p] then
+                        ep[#ep+1] = resolvers.clean_path(p)
+                        done[p] = true
+                    end
+                end
+            end
+        elseif subpaths and subpaths ~= "" then
+            for i=1,n do
+                -- we gmatch each step again, not that fast, but used seldom
+                for s in gmatch(subpaths,"[^,]+") do
+                    local ps = ep[i] .. "/" .. s
+                    if not done[ps] then
+                        ep[#ep+1] = resolvers.clean_path(ps)
+                        done[ps] = true
+                    end
+                end
+            end
+        end
+        if #ep > 0 then
+            instance.extra_paths = ep -- register paths
+        end
+        if #ep > n then
+            instance.lists = { } -- erase the cache
+        end
+    end
+
+end
+
+local function made_list(instance,list)
+    local ep = instance.extra_paths
+    if not ep or #ep == 0 then
+        return list
+    else
+        local done, new = { }, { }
+        -- honour . .. ../.. but only when at the start
+        for k=1,#list do
+            local v = list[k]
+            if not done[v] then
+                if find(v,"^[%.%/]$") then
+                    done[v] = true
+                    new[#new+1] = v
+                else
+                    break
+                end
+            end
+        end
+        -- first the extra paths
+        for k=1,#ep do
+            local v = ep[k]
+            if not done[v] then
+                done[v] = true
+                new[#new+1] = v
+            end
+        end
+        -- next the formal paths
+        for k=1,#list do
+            local v = list[k]
+            if not done[v] then
+                done[v] = true
+                new[#new+1] = v
+            end
+        end
+        return new
+    end
+end
+
+function resolvers.clean_path_list(str)
+    local t = resolvers.expanded_path_list(str)
+    if t then
+        for i=1,#t do
+            t[i] = file.collapse_path(resolvers.clean_path(t[i]))
+        end
+    end
+    return t
+end
+
+function resolvers.expand_path(str)
+    return file.join_path(resolvers.expanded_path_list(str))
+end
+
+function resolvers.expanded_path_list(str)
+    if not str then
+        return ep or { } -- ep ?
+    elseif instance.savelists then
+        -- engine+progname hash
+        str = gsub(str,"%$","")
+        if not instance.lists[str] then -- cached
+            local lst = made_list(instance,resolvers.split_path(resolvers.expansion(str)))
+            instance.lists[str] = expanded_path_from_list(lst)
+        end
+        return instance.lists[str]
+    else
+        local lst = resolvers.split_path(resolvers.expansion(str))
+        return made_list(instance,expanded_path_from_list(lst))
+    end
+end
+
+function resolvers.expanded_path_list_from_var(str) -- brrr
+    local tmp = resolvers.var_of_format_or_suffix(gsub(str,"%$",""))
+    if tmp ~= "" then
+        return resolvers.expanded_path_list(tmp)
+    else
+        return resolvers.expanded_path_list(str)
+    end
+end
+
+function resolvers.expand_path_from_var(str)
+    return file.join_path(resolvers.expanded_path_list_from_var(str))
+end
+
+function resolvers.format_of_var(str)
+    return formats[str] or formats[alternatives[str]] or ''
+end
+function resolvers.format_of_suffix(str)
+    return suffixmap[file.extname(str)] or 'tex'
+end
+
+function resolvers.variable_of_format(str)
+    return formats[str] or formats[alternatives[str]] or ''
+end
+
+function resolvers.var_of_format_or_suffix(str)
+    local v = formats[str]
+    if v then
+        return v
+    end
+    v = formats[alternatives[str]]
+    if v then
+        return v
+    end
+    v = suffixmap[file.extname(str)]
+    if v then
+        return formats[isf]
+    end
+    return ''
+end
+
+function resolvers.expand_braces(str) -- output variable and brace expansion of STRING
+    local ori = resolvers.variable(str)
+    local pth = expanded_path_from_list(resolvers.split_path(ori))
+    return file.join_path(pth)
+end
+
+resolvers.isreadable = { }
+
+function resolvers.isreadable.file(name)
+    local readable = lfs.isfile(name) -- brrr
+    if trace_detail then
+        if readable then
+            logs.report("fileio","file '%s' is readable",name)
+        else
+            logs.report("fileio","file '%s' is not readable", name)
+        end
+    end
+    return readable
+end
+
+resolvers.isreadable.tex = resolvers.isreadable.file
+
+-- name
+-- name/name
+
+local function collect_files(names)
+    local filelist = { }
+    for k=1,#names do
+        local fname = names[k]
+        if trace_detail then
+            logs.report("fileio","checking name '%s'",fname)
+        end
+        local bname = file.basename(fname)
+        local dname = file.dirname(fname)
+        if dname == "" or find(dname,"^%.") then
+            dname = false
+        else
+            dname = "/" .. dname .. "$"
+        end
+        local hashes = instance.hashes
+        for h=1,#hashes do
+            local hash = hashes[h]
+            local blobpath = hash.tag
+            local files = blobpath and instance.files[blobpath]
+            if files then
+                if trace_detail then
+                    logs.report("fileio","deep checking '%s' (%s)",blobpath,bname)
+                end
+                local blobfile = files[bname]
+                if not blobfile then
+                    local rname = "remap:"..bname
+                    blobfile = files[rname]
+                    if blobfile then
+                        bname = files[rname]
+                        blobfile = files[bname]
+                    end
+                end
+                if blobfile then
+                    if type(blobfile) == 'string' then
+                        if not dname or find(blobfile,dname) then
+                            filelist[#filelist+1] = {
+                                hash.type,
+                                file.join(blobpath,blobfile,bname), -- search
+                                resolvers.concatinators[hash.type](blobpath,blobfile,bname) -- result
+                            }
+                        end
+                    else
+                        for kk=1,#blobfile do
+                            local vv = blobfile[kk]
+                            if not dname or find(vv,dname) then
+                                filelist[#filelist+1] = {
+                                    hash.type,
+                                    file.join(blobpath,vv,bname), -- search
+                                    resolvers.concatinators[hash.type](blobpath,vv,bname) -- result
+                                }
+                            end
+                        end
+                    end
+                end
+            elseif trace_locating then
+                logs.report("fileio","no match in '%s' (%s)",blobpath,bname)
+            end
+        end
+    end
+    if #filelist > 0 then
+        return filelist
+    else
+        return nil
+    end
+end
+
+function resolvers.suffix_of_format(str)
+    if suffixes[str] then
+        return suffixes[str][1]
+    else
+        return ""
+    end
+end
+
+function resolvers.suffixes_of_format(str)
+    if suffixes[str] then
+        return suffixes[str]
+    else
+        return {}
+    end
+end
+
+function resolvers.register_in_trees(name)
+    if not find(name,"^%.") then
+        instance.foundintrees[name] = (instance.foundintrees[name] or 0) + 1 -- maybe only one
+    end
+end
+
+-- split the next one up for readability (bu this module needs a cleanup anyway)
+
+local function can_be_dir(name) -- can become local
+    local fakepaths = instance.fakepaths
+    if not fakepaths[name] then
+        if lfs.isdir(name) then
+            fakepaths[name] = 1 -- directory
+        else
+            fakepaths[name] = 2 -- no directory
+        end
+    end
+    return (fakepaths[name] == 1)
+end
+
+local function collect_instance_files(filename,collected) -- todo : plugin (scanners, checkers etc)
+    local result = collected or { }
+    local stamp  = nil
+    filename = file.collapse_path(filename)
+    -- speed up / beware: format problem
+    if instance.remember then
+        stamp = filename .. "--" .. instance.engine .. "--" .. instance.progname .. "--" .. instance.format
+        if instance.found[stamp] then
+            if trace_locating then
+                logs.report("fileio","remembering file '%s'",filename)
+            end
+            return instance.found[stamp]
+        end
+    end
+    if not dangerous[instance.format or "?"] then
+        if resolvers.isreadable.file(filename) then
+            if trace_detail then
+                logs.report("fileio","file '%s' found directly",filename)
+            end
+            instance.found[stamp] = { filename }
+            return { filename }
+        end
+    end
+    if find(filename,'%*') then
+        if trace_locating then
+            logs.report("fileio","checking wildcard '%s'", filename)
+        end
+        result = resolvers.find_wildcard_files(filename)
+    elseif file.is_qualified_path(filename) then
+        if resolvers.isreadable.file(filename) then
+            if trace_locating then
+                logs.report("fileio","qualified name '%s'", filename)
+            end
+            result = { filename }
+        else
+            local forcedname, ok, suffix = "", false, file.extname(filename)
+            if suffix == "" then -- why
+                if instance.format == "" then
+                    forcedname = filename .. ".tex"
+                    if resolvers.isreadable.file(forcedname) then
+                        if trace_locating then
+                            logs.report("fileio","no suffix, forcing standard filetype 'tex'")
+                        end
+                        result, ok = { forcedname }, true
+                    end
+                else
+                    local suffixes = resolvers.suffixes_of_format(instance.format)
+                    for _, s in next, suffixes do
+                        forcedname = filename .. "." .. s
+                        if resolvers.isreadable.file(forcedname) then
+                            if trace_locating then
+                                logs.report("fileio","no suffix, forcing format filetype '%s'", s)
+                            end
+                            result, ok = { forcedname }, true
+                            break
+                        end
+                    end
+                end
+            end
+            if not ok and suffix ~= "" then
+                -- try to find in tree (no suffix manipulation), here we search for the
+                -- matching last part of the name
+                local basename = file.basename(filename)
+                local pattern = gsub(filename .. "$","([%.%-])","%%%1")
+                local savedformat = instance.format
+                local format = savedformat or ""
+                if format == "" then
+                    instance.format = resolvers.format_of_suffix(suffix)
+                end
+                if not format then
+                    instance.format = "othertextfiles" -- kind of everything, maybe texinput is better
+                end
+                --
+                if basename ~= filename then
+                    local resolved = collect_instance_files(basename)
+                    if #result == 0 then
+                        local lowered = lower(basename)
+                        if filename ~= lowered then
+                            resolved = collect_instance_files(lowered)
+                        end
+                    end
+                    resolvers.format = savedformat
+                    --
+                    for r=1,#resolved do
+                        local rr = resolved[r]
+                        if find(rr,pattern) then
+                            result[#result+1], ok = rr, true
+                        end
+                    end
+                end
+                -- a real wildcard:
+                --
+                -- if not ok then
+                --     local filelist = collect_files({basename})
+                --     for f=1,#filelist do
+                --         local ff = filelist[f][3] or ""
+                --         if find(ff,pattern) then
+                --             result[#result+1], ok = ff, true
+                --         end
+                --     end
+                -- end
+            end
+            if not ok and trace_locating then
+                logs.report("fileio","qualified name '%s'", filename)
+            end
+        end
+    else
+        -- search spec
+        local filetype, extra, done, wantedfiles, ext = '', nil, false, { }, file.extname(filename)
+        if ext == "" then
+            if not instance.force_suffixes then
+                wantedfiles[#wantedfiles+1] = filename
+            end
+        else
+            wantedfiles[#wantedfiles+1] = filename
+        end
+        if instance.format == "" then
+            if ext == "" then
+                local forcedname = filename .. '.tex'
+                wantedfiles[#wantedfiles+1] = forcedname
+                filetype = resolvers.format_of_suffix(forcedname)
+                if trace_locating then
+                    logs.report("fileio","forcing filetype '%s'",filetype)
+                end
+            else
+                filetype = resolvers.format_of_suffix(filename)
+                if trace_locating then
+                    logs.report("fileio","using suffix based filetype '%s'",filetype)
+                end
+            end
+        else
+            if ext == "" then
+                local suffixes = resolvers.suffixes_of_format(instance.format)
+                for _, s in next, suffixes do
+                    wantedfiles[#wantedfiles+1] = filename .. "." .. s
+                end
+            end
+            filetype = instance.format
+            if trace_locating then
+                logs.report("fileio","using given filetype '%s'",filetype)
+            end
+        end
+        local typespec = resolvers.variable_of_format(filetype)
+        local pathlist = resolvers.expanded_path_list(typespec)
+        if not pathlist or #pathlist == 0 then
+            -- no pathlist, access check only / todo == wildcard
+            if trace_detail then
+                logs.report("fileio","checking filename '%s', filetype '%s', wanted files '%s'",filename, filetype or '?',concat(wantedfiles," | "))
+            end
+            for k=1,#wantedfiles do
+                local fname = wantedfiles[k]
+                if fname and resolvers.isreadable.file(fname) then
+                    filename, done = fname, true
+                    result[#result+1] = file.join('.',fname)
+                    break
+                end
+            end
+            -- this is actually 'other text files' or 'any' or 'whatever'
+            local filelist = collect_files(wantedfiles)
+            local fl = filelist and filelist[1]
+            if fl then
+                filename = fl[3]
+                result[#result+1] = filename
+                done = true
+            end
+        else
+            -- list search
+            local filelist = collect_files(wantedfiles)
+            local dirlist = { }
+            if filelist then
+                for i=1,#filelist do
+                    dirlist[i] = file.dirname(filelist[i][2]) .. "/"
+                end
+            end
+            if trace_detail then
+                logs.report("fileio","checking filename '%s'",filename)
+            end
+            -- a bit messy ... esp the doscan setting here
+            local doscan
+            for k=1,#pathlist do
+                local path = pathlist[k]
+                if find(path,"^!!") then doscan  = false else doscan  = true  end
+                local pathname = gsub(path,"^!+", '')
+                done = false
+                -- using file list
+                if filelist then
+                    local expression
+                    -- compare list entries with permitted pattern -- /xx /xx//
+                    if not find(pathname,"/$") then
+                        expression = pathname .. "/"
+                    else
+                        expression = pathname
+                    end
+                    expression = gsub(expression,"([%-%.])","%%%1") -- this also influences
+                    expression = gsub(expression,"//+$", '/.*')     -- later usage of pathname
+                    expression = gsub(expression,"//", '/.-/')      -- not ok for /// but harmless
+                    expression = "^" .. expression .. "$"
+                    if trace_detail then
+                        logs.report("fileio","using pattern '%s' for path '%s'",expression,pathname)
+                    end
+                    for k=1,#filelist do
+                        local fl = filelist[k]
+                        local f = fl[2]
+                        local d = dirlist[k]
+                        if find(d,expression) then
+                            --- todo, test for readable
+                            result[#result+1] = fl[3]
+                            resolvers.register_in_trees(f) -- for tracing used files
+                            done = true
+                            if instance.allresults then
+                                if trace_detail then
+                                    logs.report("fileio","match in hash for file '%s' on path '%s', continue scanning",f,d)
+                                end
+                            else
+                                if trace_detail then
+                                    logs.report("fileio","match in hash for file '%s' on path '%s', quit scanning",f,d)
+                                end
+                                break
+                            end
+                        elseif trace_detail then
+                            logs.report("fileio","no match in hash for file '%s' on path '%s'",f,d)
+                        end
+                    end
+                end
+                if not done and doscan then
+                    -- check if on disk / unchecked / does not work at all / also zips
+                    if resolvers.splitmethod(pathname).scheme == 'file' then -- ?
+                        local pname = gsub(pathname,"%.%*$",'')
+                        if not find(pname,"%*") then
+                            local ppname = gsub(pname,"/+$","")
+                            if can_be_dir(ppname) then
+                                for k=1,#wantedfiles do
+                                    local w = wantedfiles[k]
+                                    local fname = file.join(ppname,w)
+                                    if resolvers.isreadable.file(fname) then
+                                        if trace_detail then
+                                            logs.report("fileio","found '%s' by scanning",fname)
+                                        end
+                                        result[#result+1] = fname
+                                        done = true
+                                        if not instance.allresults then break end
+                                    end
+                                end
+                            else
+                                -- no access needed for non existing path, speedup (esp in large tree with lots of fake)
+                            end
+                        end
+                    end
+                end
+                if not done and doscan then
+                    -- todo: slow path scanning
+                end
+                if done and not instance.allresults then break end
+            end
+        end
+    end
+    for k=1,#result do
+        result[k] = file.collapse_path(result[k])
+    end
+    if instance.remember then
+        instance.found[stamp] = result
+    end
+    return result
+end
+
+if not resolvers.concatinators  then resolvers.concatinators = { } end
+
+resolvers.concatinators.tex  = file.join
+resolvers.concatinators.file = resolvers.concatinators.tex
+
+function resolvers.find_files(filename,filetype,mustexist)
+    if type(mustexist) == boolean then
+        -- all set
+    elseif type(filetype) == 'boolean' then
+        filetype, mustexist = nil, false
+    elseif type(filetype) ~= 'string' then
+        filetype, mustexist = nil, false
+    end
+    instance.format = filetype or ''
+    local result = collect_instance_files(filename)
+    if #result == 0 then
+        local lowered = lower(filename)
+        if filename ~= lowered then
+            return collect_instance_files(lowered)
+        end
+    end
+    instance.format = ''
+    return result
+end
+
+function resolvers.find_file(filename,filetype,mustexist)
+    return (resolvers.find_files(filename,filetype,mustexist)[1] or "")
+end
+
+function resolvers.find_given_files(filename)
+    local bname, result = file.basename(filename), { }
+    local hashes = instance.hashes
+    for k=1,#hashes do
+        local hash = hashes[k]
+        local files = instance.files[hash.tag] or { }
+        local blist = files[bname]
+        if not blist then
+            local rname = "remap:"..bname
+            blist = files[rname]
+            if blist then
+                bname = files[rname]
+                blist = files[bname]
+            end
+        end
+        if blist then
+            if type(blist) == 'string' then
+                result[#result+1] = resolvers.concatinators[hash.type](hash.tag,blist,bname) or ""
+                if not instance.allresults then break end
+            else
+                for kk=1,#blist do
+                    local vv = blist[kk]
+                    result[#result+1] = resolvers.concatinators[hash.type](hash.tag,vv,bname) or ""
+                    if not instance.allresults then break end
+                end
+            end
+        end
+    end
+    return result
+end
+
+function resolvers.find_given_file(filename)
+    return (resolvers.find_given_files(filename)[1] or "")
+end
+
+local function doit(path,blist,bname,tag,kind,result,allresults)
+    local done = false
+    if blist and kind then
+        if type(blist) == 'string' then
+            -- make function and share code
+            if find(lower(blist),path) then
+                result[#result+1] = resolvers.concatinators[kind](tag,blist,bname) or ""
+                done = true
+            end
+        else
+            for kk=1,#blist do
+                local vv = blist[kk]
+                if find(lower(vv),path) then
+                    result[#result+1] = resolvers.concatinators[kind](tag,vv,bname) or ""
+                    done = true
+                    if not allresults then break end
+                end
+            end
+        end
+    end
+    return done
+end
+
+function resolvers.find_wildcard_files(filename) -- todo: remap:
+    local result = { }
+    local bname, dname = file.basename(filename), file.dirname(filename)
+    local path = gsub(dname,"^*/","")
+    path = gsub(path,"*",".*")
+    path = gsub(path,"-","%%-")
+    if dname == "" then
+        path = ".*"
+    end
+    local name = bname
+    name = gsub(name,"*",".*")
+    name = gsub(name,"-","%%-")
+    path = lower(path)
+    name = lower(name)
+    local files, allresults, done = instance.files, instance.allresults, false
+    if find(name,"%*") then
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            local tag, kind = hash.tag, hash.type
+            for kk, hh in next, files[hash.tag] do
+                if not find(kk,"^remap:") then
+                    if find(lower(kk),name) then
+                        if doit(path,hh,kk,tag,kind,result,allresults) then done = true end
+                        if done and not allresults then break end
+                    end
+                end
+            end
+        end
+    else
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            local tag, kind = hash.tag, hash.type
+            if doit(path,files[tag][bname],bname,tag,kind,result,allresults) then done = true end
+            if done and not allresults then break end
+        end
+    end
+    -- we can consider also searching the paths not in the database, but then
+    -- we end up with a messy search (all // in all path specs)
+    return result
+end
+
+function resolvers.find_wildcard_file(filename)
+    return (resolvers.find_wildcard_files(filename)[1] or "")
+end
+
+-- main user functions
+
+function resolvers.automount()
+    -- implemented later
+end
+
+function resolvers.load(option)
+    statistics.starttiming(instance)
+    resolvers.resetconfig()
+    resolvers.identify_cnf()
+    resolvers.load_lua() -- will become the new method
+    resolvers.expand_variables()
+    resolvers.load_cnf() -- will be skipped when we have a lua file
+    resolvers.expand_variables()
+    if option ~= "nofiles" then
+        resolvers.load_hash()
+        resolvers.automount()
+    end
+    statistics.stoptiming(instance)
+end
+
+function resolvers.for_files(command, files, filetype, mustexist)
+    if files and #files > 0 then
+        local function report(str)
+            if trace_locating then
+                logs.report("fileio",str) -- has already verbose
+            else
+                print(str)
+            end
+        end
+        if trace_locating then
+            report('') -- ?
+        end
+        for f=1,#files do
+            local file = files[f]
+            local result = command(file,filetype,mustexist)
+            if type(result) == 'string' then
+                report(result)
+            else
+                for i=1,#result do
+                    report(result[i]) -- could be unpack
+                end
+            end
+        end
+    end
+end
+
+-- strtab
+
+resolvers.var_value  = resolvers.variable   -- output the value of variable $STRING.
+resolvers.expand_var = resolvers.expansion  -- output variable expansion of STRING.
+
+function resolvers.show_path(str)     -- output search path for file type NAME
+    return file.join_path(resolvers.expanded_path_list(resolvers.format_of_var(str)))
+end
+
+-- resolvers.find_file(filename)
+-- resolvers.find_file(filename, filetype, mustexist)
+-- resolvers.find_file(filename, mustexist)
+-- resolvers.find_file(filename, filetype)
+
+function resolvers.register_file(files, name, path)
+    if files[name] then
+        if type(files[name]) == 'string' then
+            files[name] = { files[name], path }
+        else
+            files[name] = path
+        end
+    else
+        files[name] = path
+    end
+end
+
+function resolvers.splitmethod(filename)
+    if not filename then
+        return { } -- safeguard
+    elseif type(filename) == "table" then
+        return filename -- already split
+    elseif not find(filename,"://") then
+        return { scheme="file", path = filename, original=filename } -- quick hack
+    else
+        return url.hashed(filename)
+    end
+end
+
+function table.sequenced(t,sep) -- temp here
+    local s = { }
+    for k, v in next, t do -- indexed?
+        s[#s+1] = k .. "=" .. tostring(v)
+    end
+    return concat(s, sep or " | ")
+end
+
+function resolvers.methodhandler(what, filename, filetype) -- ...
+    filename = file.collapse_path(filename)
+    local specification = (type(filename) == "string" and resolvers.splitmethod(filename)) or filename -- no or { }, let it bomb
+    local scheme = specification.scheme
+    if resolvers[what][scheme] then
+        if trace_locating then
+            logs.report("fileio","handler '%s' -> '%s' -> '%s'",specification.original,what,table.sequenced(specification))
+        end
+        return resolvers[what][scheme](filename,filetype) -- todo: specification
+    else
+        return resolvers[what].tex(filename,filetype) -- todo: specification
+    end
+end
+
+function resolvers.clean_path(str)
+    if str then
+        str = gsub(str,"\\","/")
+        str = gsub(str,"^!+","")
+        str = gsub(str,"^~",resolvers.homedir)
+        return str
+    else
+        return nil
+    end
+end
+
+function resolvers.do_with_path(name,func)
+    local pathlist = resolvers.expanded_path_list(name)
+    for i=1,#pathlist do
+        func("^"..resolvers.clean_path(pathlist[i]))
+    end
+end
+
+function resolvers.do_with_var(name,func)
+    func(expanded_var(name))
+end
+
+function resolvers.with_files(pattern,handle)
+    local hashes = instance.hashes
+    for i=1,#hashes do
+        local hash = hashes[i]
+        local blobpath = hash.tag
+        local blobtype = hash.type
+        if blobpath then
+            local files = instance.files[blobpath]
+            if files then
+                for k,v in next, files do
+                    if find(k,"^remap:") then
+                        k = files[k]
+                        v = files[k] -- chained
+                    end
+                    if find(k,pattern) then
+                        if type(v) == "string" then
+                            handle(blobtype,blobpath,v,k)
+                        else
+                            for _,vv in next, v do -- indexed
+                                handle(blobtype,blobpath,vv,k)
+                            end
+                        end
+                    end
+                end
+            end
+        end
+    end
+end
+
+function resolvers.locate_format(name)
+    local barename, fmtname = gsub(name,"%.%a+$",""), ""
+    if resolvers.usecache then
+        local path = file.join(caches.setpath("formats")) -- maybe platform
+        fmtname = file.join(path,barename..".fmt") or ""
+    end
+    if fmtname == "" then
+        fmtname = resolvers.find_files(barename..".fmt")[1] or ""
+    end
+    fmtname = resolvers.clean_path(fmtname)
+    if fmtname ~= "" then
+        local barename = file.removesuffix(fmtname)
+        local luaname, lucname, luiname = barename .. ".lua", barename .. ".luc", barename .. ".lui"
+        if lfs.isfile(luiname) then
+            return barename, luiname
+        elseif lfs.isfile(lucname) then
+            return barename, lucname
+        elseif lfs.isfile(luaname) then
+            return barename, luaname
+        end
+    end
+    return nil, nil
+end
+
+function resolvers.boolean_variable(str,default)
+    local b = resolvers.expansion(str)
+    if b == "" then
+        return default
+    else
+        b = toboolean(b)
+        return (b == nil and default) or b
+    end
+end
+
+texconfig.kpse_init = false
+
+kpse = { original = kpse } setmetatable(kpse, { __index = function(k,v) return resolvers[v] end } )
+
+-- for a while
+
+input = resolvers
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-tmp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+--[[ldx--
+<p>This module deals with caching data. It sets up the paths and
+implements loaders and savers for tables. Best is to set the
+following variable. When not set, the usual paths will be
+checked. Personally I prefer the (users) temporary path.</p>
+
+</code>
+TEXMFCACHE=$TMP;$TEMP;$TMPDIR;$TEMPDIR;$HOME;$TEXMFVAR;$VARTEXMF;.
+</code>
+
+<p>Currently we do no locking when we write files. This is no real
+problem because most caching involves fonts and the chance of them
+being written at the same time is small. We also need to extend
+luatools with a recache feature.</p>
+--ldx]]--
+
+local format, lower, gsub = string.format, string.lower, string.gsub
+
+local trace_cache = false  trackers.register("resolvers.cache", function(v) trace_cache = v end) -- not used yet
+
+caches = caches or { }
+
+caches.path     = caches.path or nil
+caches.base     = caches.base or "luatex-cache"
+caches.more     = caches.more or "context"
+caches.direct   = false -- true is faster but may need huge amounts of memory
+caches.tree     = false
+caches.paths    = caches.paths or nil
+caches.force    = false
+caches.defaults = { "TEXMFCACHE", "TMPDIR", "TEMPDIR", "TMP", "TEMP", "HOME", "HOMEPATH" }
+
+function caches.temp()
+    local cachepath = nil
+    local function check(list,isenv)
+        if not cachepath then
+            for k=1,#list do
+                local v = list[k]
+                cachepath = (isenv and (os.env[v] or "")) or v or ""
+                if cachepath == "" then
+                    -- next
+                else
+                    cachepath = resolvers.clean_path(cachepath)
+                    if lfs.isdir(cachepath) and file.iswritable(cachepath) then -- lfs.attributes(cachepath,"mode") == "directory"
+                        break
+                    elseif caches.force or io.ask(format("\nShould I create the cache path %s?",cachepath), "no", { "yes", "no" }) == "yes" then
+                        dir.mkdirs(cachepath)
+                        if lfs.isdir(cachepath) and file.iswritable(cachepath) then
+                            break
+                        end
+                    end
+                end
+                cachepath = nil
+            end
+        end
+    end
+    check(resolvers.clean_path_list("TEXMFCACHE") or { })
+    check(caches.defaults,true)
+    if not cachepath then
+        print("\nfatal error: there is no valid (writable) cache path defined\n")
+        os.exit()
+    elseif not lfs.isdir(cachepath) then -- lfs.attributes(cachepath,"mode") ~= "directory"
+        print(format("\nfatal error: cache path %s is not a directory\n",cachepath))
+        os.exit()
+    end
+    cachepath = file.collapse_path(cachepath)
+    function caches.temp()
+        return cachepath
+    end
+    return cachepath
+end
+
+function caches.configpath()
+    return table.concat(resolvers.instance.cnffiles,";")
+end
+
+function caches.hashed(tree)
+    return md5.hex(gsub(lower(tree),"[\\\/]+","/"))
+end
+
+function caches.treehash()
+    local tree = caches.configpath()
+    if not tree or tree == "" then
+        return false
+    else
+        return caches.hashed(tree)
+    end
+end
+
+function caches.setpath(...)
+    if not caches.path then
+        if not caches.path then
+            caches.path = caches.temp()
+        end
+        caches.path = resolvers.clean_path(caches.path) -- to be sure
+        caches.tree = caches.tree or caches.treehash()
+        if caches.tree then
+            caches.path = dir.mkdirs(caches.path,caches.base,caches.more,caches.tree)
+        else
+            caches.path = dir.mkdirs(caches.path,caches.base,caches.more)
+        end
+    end
+    if not caches.path then
+        caches.path = '.'
+    end
+    caches.path = resolvers.clean_path(caches.path)
+    local dirs = { ... }
+    if #dirs > 0 then
+        local pth = dir.mkdirs(caches.path,...)
+        return pth
+    end
+    caches.path = dir.expand_name(caches.path)
+    return caches.path
+end
+
+function caches.definepath(category,subcategory)
+    return function()
+        return caches.setpath(category,subcategory)
+    end
+end
+
+function caches.setluanames(path,name)
+    return path .. "/" .. name .. ".tma", path .. "/" .. name .. ".tmc"
+end
+
+function caches.loaddata(path,name)
+    local tmaname, tmcname = caches.setluanames(path,name)
+    local loader = loadfile(tmcname) or loadfile(tmaname)
+    if loader then
+        loader = loader()
+        collectgarbage("step")
+        return loader
+    else
+        return false
+    end
+end
+
+--~ function caches.loaddata(path,name)
+--~     local tmaname, tmcname = caches.setluanames(path,name)
+--~     return dofile(tmcname) or dofile(tmaname)
+--~ end
+
+function caches.iswritable(filepath,filename)
+    local tmaname, tmcname = caches.setluanames(filepath,filename)
+    return file.iswritable(tmaname)
+end
+
+function caches.savedata(filepath,filename,data,raw)
+    local tmaname, tmcname = caches.setluanames(filepath,filename)
+    local reduce, simplify = true, true
+    if raw then
+        reduce, simplify = false, false
+    end
+    data.cache_uuid = os.uuid()
+    if caches.direct then
+        file.savedata(tmaname, table.serialize(data,'return',false,true,false)) -- no hex
+    else
+        table.tofile(tmaname, data,'return',false,true,false) -- maybe not the last true
+    end
+    local cleanup = resolvers.boolean_variable("PURGECACHE", false)
+    local strip = resolvers.boolean_variable("LUACSTRIP", true)
+    utils.lua.compile(tmaname, tmcname, cleanup, strip)
+end
+
+-- here we use the cache for format loading (texconfig.[formatname|jobname])
+
+--~ if tex and texconfig and texconfig.formatname and texconfig.formatname == "" then
+if tex and texconfig and (not texconfig.formatname or texconfig.formatname == "") and input and resolvers.instance then
+    if not texconfig.luaname then texconfig.luaname = "cont-en.lua" end -- or luc
+    texconfig.formatname = caches.setpath("formats") .. "/" .. gsub(texconfig.luaname,"%.lu.$",".fmt")
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-inp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+resolvers.finders = resolvers.finders or { }
+resolvers.openers = resolvers.openers or { }
+resolvers.loaders = resolvers.loaders or { }
+
+resolvers.finders.notfound  = { nil }
+resolvers.openers.notfound  = { nil }
+resolvers.loaders.notfound  = { false, nil, 0 }
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-out'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+outputs = outputs or { }
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-con'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, lower, gsub = string.format, string.lower, string.gsub
+
+local trace_cache      = false  trackers.register("resolvers.cache",      function(v) trace_cache      = v end)
+local trace_containers = false  trackers.register("resolvers.containers", function(v) trace_containers = v end)
+local trace_storage    = false  trackers.register("resolvers.storage",    function(v) trace_storage    = v end)
+
+--[[ldx--
+<p>Once we found ourselves defining similar cache constructs
+several times, containers were introduced. Containers are used
+to collect tables in memory and reuse them when possible based
+on (unique) hashes (to be provided by the calling function).</p>
+
+<p>Caching to disk is disabled by default. Version numbers are
+stored in the saved table which makes it possible to change the
+table structures without bothering about the disk cache.</p>
+
+<p>Examples of usage can be found in the font related code.</p>
+--ldx]]--
+
+containers = containers or { }
+
+containers.usecache = true
+
+local function report(container,tag,name)
+    if trace_cache or trace_containers then
+        logs.report(format("%s cache",container.subcategory),"%s: %s",tag,name or 'invalid')
+    end
+end
+
+local allocated = { }
+
+-- tracing
+
+function containers.define(category, subcategory, version, enabled)
+    return function()
+        if category and subcategory then
+            local c = allocated[category]
+            if not c then
+                c  = { }
+                allocated[category] = c
+            end
+            local s = c[subcategory]
+            if not s then
+                s = {
+                    category = category,
+                    subcategory = subcategory,
+                    storage = { },
+                    enabled = enabled,
+                    version = version or 1.000,
+                    trace = false,
+                    path = caches and caches.setpath and caches.setpath(category,subcategory),
+                }
+                c[subcategory] = s
+            end
+            return s
+        else
+            return nil
+        end
+    end
+end
+
+function containers.is_usable(container, name)
+    return container.enabled and caches and caches.iswritable(container.path, name)
+end
+
+function containers.is_valid(container, name)
+    if name and name ~= "" then
+        local storage = container.storage[name]
+        return storage and storage.cache_version == container.version
+    else
+        return false
+    end
+end
+
+function containers.read(container,name)
+    if container.enabled and caches and not container.storage[name] and containers.usecache then
+        container.storage[name] = caches.loaddata(container.path,name)
+        if containers.is_valid(container,name) then
+            report(container,"loaded",name)
+        else
+            container.storage[name] = nil
+        end
+    end
+    if container.storage[name] then
+        report(container,"reusing",name)
+    end
+    return container.storage[name]
+end
+
+function containers.write(container, name, data)
+    if data then
+        data.cache_version = container.version
+        if container.enabled and caches then
+            local unique, shared = data.unique, data.shared
+            data.unique, data.shared = nil, nil
+            caches.savedata(container.path, name, data)
+            report(container,"saved",name)
+            data.unique, data.shared = unique, shared
+        end
+        report(container,"stored",name)
+        container.storage[name] = data
+    end
+    return data
+end
+
+function containers.content(container,name)
+    return container.storage[name]
+end
+
+function containers.cleanname(name)
+    return (gsub(lower(name),"[^%w%d]+","-"))
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-use'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, lower, gsub, find = string.format, string.lower, string.gsub, string.find
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+-- since we want to use the cache instead of the tree, we will now
+-- reimplement the saver.
+
+local save_data = resolvers.save_data
+local load_data = resolvers.load_data
+
+resolvers.cachepath = nil  -- public, for tracing
+resolvers.usecache  = true -- public, for tracing
+
+function resolvers.save_data(dataname)
+    save_data(dataname, function(cachename,dataname)
+        resolvers.usecache = not toboolean(resolvers.expansion("CACHEINTDS") or "false",true)
+        if resolvers.usecache then
+            resolvers.cachepath = resolvers.cachepath or caches.definepath("trees")
+            return file.join(resolvers.cachepath(),caches.hashed(cachename))
+        else
+            return file.join(cachename,dataname)
+        end
+    end)
+end
+
+function resolvers.load_data(pathname,dataname,filename)
+    load_data(pathname,dataname,filename,function(dataname,filename)
+        resolvers.usecache = not toboolean(resolvers.expansion("CACHEINTDS") or "false",true)
+        if resolvers.usecache then
+            resolvers.cachepath = resolvers.cachepath or caches.definepath("trees")
+            return file.join(resolvers.cachepath(),caches.hashed(pathname))
+        else
+            if not filename or (filename == "") then
+                filename = dataname
+            end
+            return file.join(pathname,filename)
+        end
+    end)
+end
+
+-- we will make a better format, maybe something xml or just text or lua
+
+resolvers.automounted = resolvers.automounted or { }
+
+function resolvers.automount(usecache)
+    local mountpaths = resolvers.clean_path_list(resolvers.expansion('TEXMFMOUNT'))
+    if (not mountpaths or #mountpaths == 0) and usecache then
+        mountpaths = { caches.setpath("mount") }
+    end
+    if mountpaths and #mountpaths > 0 then
+        statistics.starttiming(resolvers.instance)
+        for k=1,#mountpaths do
+            local root = mountpaths[k]
+            local f = io.open(root.."/url.tmi")
+            if f then
+                for line in f:lines() do
+                    if line then
+                        if find(line,"^[%%#%-]") then -- or %W
+                            -- skip
+                        elseif find(line,"^zip://") then
+                            if trace_locating then
+                                logs.report("fileio","mounting %s",line)
+                            end
+                            table.insert(resolvers.automounted,line)
+                            resolvers.usezipfile(line)
+                        end
+                    end
+                end
+                f:close()
+            end
+        end
+        statistics.stoptiming(resolvers.instance)
+    end
+end
+
+-- status info
+
+statistics.register("used config path", function() return caches.configpath()  end)
+statistics.register("used cache path",  function() return caches.temp() or "?" end)
+
+-- experiment (code will move)
+
+function statistics.save_fmt_status(texname,formatbanner,sourcefile) -- texname == formatname
+    local enginebanner = status.list().banner
+    if formatbanner and enginebanner and sourcefile then
+        local luvname = file.replacesuffix(texname,"luv")
+        local luvdata = {
+            enginebanner = enginebanner,
+            formatbanner = formatbanner,
+            sourcehash   = md5.hex(io.loaddata(resolvers.find_file(sourcefile)) or "unknown"),
+            sourcefile   = sourcefile,
+        }
+        io.savedata(luvname,table.serialize(luvdata,true))
+    end
+end
+
+function statistics.check_fmt_status(texname)
+    local enginebanner = status.list().banner
+    if enginebanner and texname then
+        local luvname = file.replacesuffix(texname,"luv")
+        if lfs.isfile(luvname) then
+            local luv = dofile(luvname)
+            if luv and luv.sourcefile then
+                local sourcehash = md5.hex(io.loaddata(resolvers.find_file(luv.sourcefile)) or "unknown")
+                local luvbanner = luv.enginebanner or "?"
+                if luvbanner ~= enginebanner then
+                    return string.format("engine mismatch (luv:%s <> bin:%s)",luvbanner,enginebanner)
+                end
+                local luvhash = luv.sourcehash or "?"
+                if luvhash ~= sourcehash then
+                    return string.format("source mismatch (luv:%s <> bin:%s)",luvhash,sourcehash)
+                end
+            else
+                return "invalid status file"
+            end
+        else
+            return "missing status file"
+        end
+    end
+    return true
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['luat-kps'] = {
+    version   = 1.001,
+    comment   = "companion to luatools.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+--[[ldx--
+<p>This file is used when we want the input handlers to behave like
+<type>kpsewhich</type>. What to do with the following:</p>
+
+<typing>
+{$SELFAUTOLOC,$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,}/web2c}
+$SELFAUTOLOC    : /usr/tex/bin/platform
+$SELFAUTODIR    : /usr/tex/bin
+$SELFAUTOPARENT : /usr/tex
+</typing>
+
+<p>How about just forgetting about them?</p>
+--ldx]]--
+
+local suffixes = resolvers.suffixes
+local formats  = resolvers.formats
+
+suffixes['gf']                       = { '<resolution>gf' }
+suffixes['pk']                       = { '<resolution>pk' }
+suffixes['base']                     = { 'base' }
+suffixes['bib']                      = { 'bib' }
+suffixes['bst']                      = { 'bst' }
+suffixes['cnf']                      = { 'cnf' }
+suffixes['mem']                      = { 'mem' }
+suffixes['mf']                       = { 'mf' }
+suffixes['mfpool']                   = { 'pool' }
+suffixes['mft']                      = { 'mft' }
+suffixes['mppool']                   = { 'pool' }
+suffixes['graphic/figure']           = { 'eps', 'epsi' }
+suffixes['texpool']                  = { 'pool' }
+suffixes['PostScript header']        = { 'pro' }
+suffixes['ist']                      = { 'ist' }
+suffixes['web']                      = { 'web', 'ch' }
+suffixes['cweb']                     = { 'w', 'web', 'ch' }
+suffixes['cmap files']               = { 'cmap' }
+suffixes['lig files']                = { 'lig' }
+suffixes['bitmap font']              = { }
+suffixes['MetaPost support']         = { }
+suffixes['TeX system documentation'] = { }
+suffixes['TeX system sources']       = { }
+suffixes['dvips config']             = { }
+suffixes['type42 fonts']             = { }
+suffixes['web2c files']              = { }
+suffixes['other text files']         = { }
+suffixes['other binary files']       = { }
+suffixes['opentype fonts']           = { 'otf' }
+
+suffixes['fmt']                      = { 'fmt' }
+suffixes['texmfscripts']             = { 'rb','lua','py','pl' }
+
+suffixes['pdftex config']            = { }
+suffixes['Troff fonts']              = { }
+
+suffixes['ls-R']                     = { }
+
+--[[ldx--
+<p>If you wondered abou tsome of the previous mappings, how about
+the next bunch:</p>
+--ldx]]--
+
+formats['bib']                      = ''
+formats['bst']                      = ''
+formats['mft']                      = ''
+formats['ist']                      = ''
+formats['web']                      = ''
+formats['cweb']                     = ''
+formats['MetaPost support']         = ''
+formats['TeX system documentation'] = ''
+formats['TeX system sources']       = ''
+formats['Troff fonts']              = ''
+formats['dvips config']             = ''
+formats['graphic/figure']           = ''
+formats['ls-R']                     = ''
+formats['other text files']         = ''
+formats['other binary files']       = ''
+
+formats['gf']                       = ''
+formats['pk']                       = ''
+formats['base']                     = 'MFBASES'
+formats['cnf']                      = ''
+formats['mem']                      = 'MPMEMS'
+formats['mf']                       = 'MFINPUTS'
+formats['mfpool']                   = 'MFPOOL'
+formats['mppool']                   = 'MPPOOL'
+formats['texpool']                  = 'TEXPOOL'
+formats['PostScript header']        = 'TEXPSHEADERS'
+formats['cmap files']               = 'CMAPFONTS'
+formats['type42 fonts']             = 'T42FONTS'
+formats['web2c files']              = 'WEB2C'
+formats['pdftex config']            = 'PDFTEXCONFIG'
+formats['texmfscripts']             = 'TEXMFSCRIPTS'
+formats['bitmap font']              = ''
+formats['lig files']                = 'LIGFONTS'
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-aux'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local find = string.find
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+function resolvers.update_script(oldname,newname) -- oldname -> own.name, not per se a suffix
+    local scriptpath = "scripts/context/lua"
+    newname = file.addsuffix(newname,"lua")
+    local oldscript = resolvers.clean_path(oldname)
+    if trace_locating then
+        logs.report("fileio","to be replaced old script %s", oldscript)
+    end
+    local newscripts = resolvers.find_files(newname) or { }
+    if #newscripts == 0 then
+        if trace_locating then
+            logs.report("fileio","unable to locate new script")
+        end
+    else
+        for i=1,#newscripts do
+            local newscript = resolvers.clean_path(newscripts[i])
+            if trace_locating then
+                logs.report("fileio","checking new script %s", newscript)
+            end
+            if oldscript == newscript then
+                if trace_locating then
+                    logs.report("fileio","old and new script are the same")
+                end
+            elseif not find(newscript,scriptpath) then
+                if trace_locating then
+                    logs.report("fileio","new script should come from %s",scriptpath)
+                end
+            elseif not (find(oldscript,file.removesuffix(newname).."$") or find(oldscript,newname.."$")) then
+                if trace_locating then
+                    logs.report("fileio","invalid new script name")
+                end
+            else
+                local newdata = io.loaddata(newscript)
+                if newdata then
+                    if trace_locating then
+                        logs.report("fileio","old script content replaced by new content")
+                    end
+                    io.savedata(oldscript,newdata)
+                    break
+                elseif trace_locating then
+                    logs.report("fileio","unable to load new script")
+                end
+            end
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-lst'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- used in mtxrun
+
+local find, concat, upper, format = string.find, table.concat, string.upper, string.format
+
+resolvers.listers = resolvers.listers or { }
+
+local function tabstr(str)
+    if type(str) == 'table' then
+        return concat(str," | ")
+    else
+        return str
+    end
+end
+
+local function list(list,report)
+    local instance = resolvers.instance
+    local pat = upper(pattern or "","")
+    local report = report or texio.write_nl
+    local sorted = table.sortedkeys(list)
+    for i=1,#sorted do
+        local key = sorted[i]
+        if instance.pattern == "" or find(upper(key),pat) then
+            if instance.kpseonly then
+                if instance.kpsevars[key] then
+                    report(format("%s=%s",key,tabstr(list[key])))
+                end
+            else
+                report(format('%s %s=%s',(instance.kpsevars[key] and 'K') or 'E',key,tabstr(list[key])))
+            end
+        end
+    end
+end
+
+function resolvers.listers.variables () list(resolvers.instance.variables ) end
+function resolvers.listers.expansions() list(resolvers.instance.expansions) end
+
+function resolvers.listers.configurations(report)
+    local report = report or texio.write_nl
+    local instance = resolvers.instance
+    local sorted = table.sortedkeys(instance.kpsevars)
+    for i=1,#sorted do
+        local key = sorted[i]
+        if not instance.pattern or (instance.pattern=="") or find(key,instance.pattern) then
+            report(format("%s\n",key))
+            local order = instance.order
+            for i=1,#order do
+                local str = order[i][key]
+                if str then
+                    report(format("\t%s\t%s",i,str))
+                end
+            end
+            report("")
+        end
+    end
+end
+
+
+end -- of closure
+-- end library merge
+
+-- We initialize some characteristics of this program. We need to
+-- do this before we load the libraries, else own.name will not be
+-- properly set (handy for selfcleaning the file). It's an ugly
+-- looking piece of code.
+
+own = { }
+
+own.libs = { -- todo: check which ones are really needed
+    'l-string.lua',
+    'l-lpeg.lua',
+    'l-table.lua',
+    'l-io.lua',
+    'l-number.lua',
+    'l-set.lua',
+    'l-os.lua',
+    'l-file.lua',
+    'l-md5.lua',
+    'l-url.lua',
+    'l-dir.lua',
+    'l-boolean.lua',
+    'l-unicode.lua',
+    'l-math.lua',
+    'l-utils.lua',
+    'l-aux.lua',
+    'trac-tra.lua',
+    'luat-env.lua',
+    'trac-inf.lua',
+    'trac-log.lua',
+    'data-res.lua',
+    'data-tmp.lua',
+--  'data-pre.lua',
+    'data-inp.lua',
+    'data-out.lua',
+    'data-con.lua',
+    'data-use.lua',
+--  'data-tex.lua',
+--  'data-bin.lua',
+--  'data-zip.lua',
+--  'data-crl.lua',
+--  'data-lua.lua',
+    'data-kps.lua', -- so that we can replace kpsewhich
+    'data-aux.lua', -- updater
+    'data-lst.lua', -- lister
+}
+
+-- We need this hack till luatex is fixed.
+
+if arg and arg[0] == 'luatex' and arg[1] == "--luaonly" then
+    arg[-1]=arg[0] arg[0]=arg[2] for k=3,#arg do arg[k-2]=arg[k] end arg[#arg]=nil arg[#arg]=nil
+end
+
+-- End of hack.
+
+own.name = (environment and environment.ownname) or arg[0] or 'luatools.lua'
+own.path = string.match(own.name,"^(.+)[\\/].-$") or "."
+own.list = { '.' }
+
+if own.path ~= '.' then
+    table.insert(own.list,own.path)
+end
+
+table.insert(own.list,own.path.."/../../../tex/context/base")
+table.insert(own.list,own.path.."/mtx")
+table.insert(own.list,own.path.."/../sources")
+
+function locate_libs()
+    for _, lib in pairs(own.libs) do
+        for _, pth in pairs(own.list) do
+            local filename = string.gsub(pth .. "/" .. lib,"\\","/")
+            local codeblob = loadfile(filename)
+            if codeblob then
+                codeblob()
+                own.list = { pth } -- speed up te search
+                break
+            end
+        end
+    end
+end
+
+if not resolvers then
+    locate_libs()
+end
+
+if not resolvers then
+    print("")
+    print("Luatools is unable to start up due to lack of libraries. You may")
+    print("try to run 'lua luatools.lua --selfmerge' in the path where this")
+    print("script is located (normally under ..../scripts/context/lua) which")
+    print("will make luatools library independent.")
+    os.exit()
+end
+
+logs.setprogram('LuaTools',"TDS Management Tool 1.32",environment.arguments["verbose"] or false)
+
+local instance = resolvers.reset()
+
+resolvers.defaultlibs = { -- not all are needed (this will become: context.lus (lua spec)
+    'l-string.lua',
+    'l-lpeg.lua',
+    'l-table.lua',
+    'l-boolean.lua',
+    'l-number.lua',
+    'l-unicode.lua',
+    'l-os.lua',
+    'l-io.lua',
+    'l-file.lua',
+    'l-md5.lua',
+    'l-url.lua',
+    'l-dir.lua',
+    'l-utils.lua',
+    'l-dimen.lua',
+    'trac-inf.lua',
+    'trac-tra.lua',
+    'trac-log.lua',
+    'luat-env.lua', -- here ?
+    'data-res.lua',
+    'data-inp.lua',
+    'data-out.lua',
+    'data-tmp.lua',
+    'data-con.lua',
+    'data-use.lua',
+--  'data-pre.lua',
+    'data-tex.lua',
+    'data-bin.lua',
+--  'data-zip.lua',
+--  'data-clr.lua',
+    'data-lua.lua',
+    'data-ctx.lua',
+    'luat-fio.lua',
+    'luat-cnf.lua',
+}
+
+instance.engine     =     environment.arguments["engine"]   or 'luatex'
+instance.progname   =     environment.arguments["progname"] or 'context'
+instance.luaname    =     environment.arguments["luafile"]  or "" -- environment.ownname or ""
+instance.lualibs    =     environment.arguments["lualibs"]  or table.concat(resolvers.defaultlibs,",")
+instance.allresults =     environment.arguments["all"]      or false
+instance.pattern    =     environment.arguments["pattern"]  or nil
+instance.sortdata   =     environment.arguments["sort"]     or false
+instance.kpseonly   = not environment.arguments["all"]      or false
+instance.my_format  =     environment.arguments["format"]   or instance.format
+
+if type(instance.pattern) == 'boolean' then
+    logs.simple("invalid pattern specification")
+    instance.pattern = nil
+end
+
+if environment.arguments["trace"] then resolvers.settrace(environment.arguments["trace"]) end
+
+local trackspec = environment.argument("trackers") or environment.argument("track")
+
+if trackspec then
+    trackers.enable(trackspec)
+end
+
+runners  = runners  or { }
+messages = messages or { }
+
+messages.no_ini_file = [[
+There is no lua initialization file found. This file can be forced by the
+"--progname" directive, or specified with "--luaname", or it is derived
+automatically from the formatname (aka jobname). It may be that you have
+to regenerate the file database using "luatools --generate".
+]]
+
+messages.help = [[
+--generate        generate file database
+--variables       show configuration variables
+--expansions      show expanded variables
+--configurations  show configuration order
+--expand-braces   expand complex variable
+--expand-path     expand variable (resolve paths)
+--expand-var      expand variable (resolve references)
+--show-path       show path expansion of ...
+--var-value       report value of variable
+--find-file       report file location
+--find-path       report path of file
+--make or --ini   make luatex format
+--run or --fmt=   run luatex format
+--luafile=str     lua inifile (default is <progname>.lua)
+--lualibs=list    libraries to assemble (optional when --compile)
+--compile         assemble and compile lua inifile
+--verbose         give a bit more info
+--all             show all found files
+--sort            sort cached data
+--engine=str      target engine
+--progname=str    format or backend
+--pattern=str     filter variables
+--trackers=list   enable given trackers
+]]
+
+function runners.make_format(texname)
+    local instance = resolvers.instance
+    if texname and texname ~= "" then
+        if resolvers.usecache then
+            local path = file.join(caches.setpath("formats")) -- maybe platform
+            if path and lfs then
+                lfs.chdir(path)
+            end
+        end
+        local barename = texname:gsub("%.%a+$","")
+        if barename == texname then
+            texname = texname .. ".tex"
+        end
+        local fullname = resolvers.find_files(texname)[1] or ""
+        if fullname == "" then
+            logs.simple("no tex file with name: %s",texname)
+        else
+            local luaname, lucname, luapath, lualibs = "", "", "", { }
+            -- the following is optional, since context.lua can also
+            -- handle this collect and compile business
+            if environment.arguments["compile"] then
+                if luaname == "" then luaname = barename end
+                logs.simple("creating initialization file: %s",luaname)
+                luapath = file.dirname(luaname)
+                if luapath == "" then
+                    luapath = file.dirname(texname)
+                end
+                if luapath == "" then
+                    luapath = file.dirname(resolvers.find_files(texname)[1] or "")
+                end
+                lualibs = string.split(instance.lualibs,",")
+                luaname = file.basename(barename .. ".lua")
+                lucname = file.basename(barename .. ".luc")
+                -- todo: when this fails, we can just copy the merged libraries from
+                -- luatools since they are normally the same, at least for context
+                if lualibs[1] then
+                    local firstlib = file.join(luapath,lualibs[1])
+                    if not lfs.isfile(firstlib) then
+                        local foundname = resolvers.find_files(lualibs[1])[1]
+                        if foundname then
+                            logs.simple("located library path: %s",luapath)
+                            luapath = file.dirname(foundname)
+                        end
+                    end
+                end
+                logs.simple("using library path: %s",luapath)
+                logs.simple("using lua libraries: %s",table.join(lualibs," "))
+                utils.merger.selfcreate(lualibs,luapath,luaname)
+                local strip = resolvers.boolean_variable("LUACSTRIP", true)
+                if utils.lua.compile(luaname,lucname,false,strip) and io.exists(lucname) then
+                    luaname = lucname
+                    logs.simple("using compiled initialization file: %s",lucname)
+                else
+                    logs.simple("using uncompiled initialization file: %s",luaname)
+                end
+            else
+                local what = { instance.luaname, instance.progname, barename }
+                for k=1,#what do
+                    local v = string.gsub(what[k]..".lua","%.lua%.lua$",".lua")
+                    if v and (v ~= "") then
+                        luaname = resolvers.find_files(v)[1] or ""
+                        if luaname ~= "" then
+                            break
+                        end
+                    end
+                end
+            end
+            if environment.arguments["noluc"] then
+                luaname = luaname:gsub("%.luc$",".lua") -- make this an option
+            end
+            if luaname == "" then
+                if logs.verbose then
+                    logs.simplelines(messages.no_ini_file)
+                    logs.simple("texname : %s",texname)
+                    logs.simple("luaname : %s",instance.luaname)
+                    logs.simple("progname: %s",instance.progname)
+                    logs.simple("barename: %s",barename)
+                end
+            else
+                logs.simple("using lua initialization file: %s",luaname)
+                local mp = dir.glob(file.removesuffix(file.basename(luaname)).."-*.mem")
+                if mp and #mp > 0 then
+                    for i=1,#mp do
+                        local name = mp[i]
+                        logs.simple("removing related mplib format %s", file.basename(name))
+                        os.remove(name)
+                    end
+                end
+                local flags = {
+                    "--ini",
+                    "--lua=" .. string.quote(luaname)
+                }
+                local bs = (os.platform == "unix" and "\\\\") or "\\" -- todo: make a function
+                local command = "luatex ".. table.concat(flags," ")  .. " " .. string.quote(fullname) .. " " .. bs .. "dump"
+                logs.simple("running command: %s\n",command)
+                os.spawn(command)
+                -- todo: do a dummy run that generates the related metafun and mfplain formats
+            end
+        end
+    else
+        logs.simple("no tex file given")
+    end
+end
+
+function runners.run_format(name,data,more)
+ -- hm, rather old code here; we can now use the file.whatever functions
+    if name and (name ~= "") then
+        local barename = name:gsub("%.%a+$","")
+        local fmtname = ""
+        if resolvers.usecache then
+            local path = file.join(caches.setpath("formats")) -- maybe platform
+            fmtname = file.join(path,barename..".fmt") or ""
+        end
+        if fmtname == "" then
+            fmtname = resolvers.find_files(barename..".fmt")[1] or ""
+        end
+        fmtname = resolvers.clean_path(fmtname)
+        barename = fmtname:gsub("%.%a+$","")
+        if fmtname == "" then
+            logs.simple("no format with name: %s",name)
+        else
+            local luaname = barename .. ".luc"
+            local f = io.open(luaname)
+            if not f then
+                luaname = barename .. ".lua"
+                f = io.open(luaname)
+            end
+            if f then
+                f:close()
+                local command = "luatex --fmt=" .. string.quote(barename) .. " --lua=" .. string.quote(luaname) .. " " .. string.quote(data) .. " " .. (more ~= "" and string.quote(more) or "")
+                logs.simple("running command: %s",command)
+                os.spawn(command)
+            else
+                logs.simple("using format name: %s",fmtname)
+                logs.simple("no luc/lua with name: %s",barename)
+            end
+        end
+    end
+end
+
+local ok = true
+
+-- private option --noluc for testing errors in the stub
+
+if environment.arguments["find-file"] then
+    resolvers.load()
+    instance.format  = environment.arguments["format"] or instance.format
+    if instance.pattern then
+        instance.allresults = true
+        resolvers.for_files(resolvers.find_files, { instance.pattern }, instance.my_format)
+    else
+        resolvers.for_files(resolvers.find_files, environment.files, instance.my_format)
+    end
+elseif environment.arguments["find-path"] then
+    resolvers.load()
+    local path = resolvers.find_file(environment.files[1], instance.my_format)
+    if logs.verbose then
+        logs.simple(file.dirname(path))
+    else
+        print(file.dirname(path))
+    end
+elseif environment.arguments["run"] then
+    resolvers.load("nofiles") -- ! no need for loading databases
+    logs.setverbose(true)
+    runners.run_format(environment.files[1] or "",environment.files[2] or "",environment.files[3] or "")
+elseif environment.arguments["fmt"] then
+    resolvers.load("nofiles") -- ! no need for loading databases
+    logs.setverbose(true)
+    runners.run_format(environment.arguments["fmt"], environment.files[1] or "",environment.files[2] or "")
+elseif environment.arguments["expand-braces"] then
+    resolvers.load("nofiles")
+    resolvers.for_files(resolvers.expand_braces, environment.files)
+elseif environment.arguments["expand-path"] then
+    resolvers.load("nofiles")
+    resolvers.for_files(resolvers.expand_path, environment.files)
+elseif environment.arguments["expand-var"] or environment.arguments["expand-variable"] then
+    resolvers.load("nofiles")
+    resolvers.for_files(resolvers.expand_var, environment.files)
+elseif environment.arguments["show-path"] or environment.arguments["path-value"] then
+    resolvers.load("nofiles")
+    resolvers.for_files(resolvers.show_path, environment.files)
+elseif environment.arguments["var-value"] or environment.arguments["show-value"] then
+    resolvers.load("nofiles")
+    resolvers.for_files(resolvers.var_value, environment.files)
+elseif environment.arguments["format-path"] then
+    resolvers.load()
+    logs.simple(caches.setpath("format"))
+elseif instance.pattern then -- brrr
+    resolvers.load()
+    instance.format = environment.arguments["format"] or instance.format
+    instance.allresults = true
+    resolvers.for_files(resolvers.find_files, { instance.pattern }, instance.my_format)
+elseif environment.arguments["generate"] then
+    instance.renewcache = true
+    logs.setverbose(true)
+    resolvers.load()
+elseif environment.arguments["make"] or environment.arguments["ini"] or environment.arguments["compile"] then
+    resolvers.load()
+    logs.setverbose(true)
+    runners.make_format(environment.files[1] or "")
+elseif environment.arguments["selfmerge"] then
+    utils.merger.selfmerge(own.name,own.libs,own.list)
+elseif environment.arguments["selfclean"] then
+    utils.merger.selfclean(own.name)
+elseif environment.arguments["selfupdate"] then
+    resolvers.load()
+    logs.setverbose(true)
+    resolvers.update_script(own.name,"luatools")
+elseif environment.arguments["variables"] or environment.arguments["show-variables"] then
+    resolvers.load("nofiles")
+    resolvers.listers.variables()
+elseif environment.arguments["expansions"] or environment.arguments["show-expansions"] then
+    resolvers.load("nofiles")
+    resolvers.listers.expansions()
+elseif environment.arguments["configurations"] or environment.arguments["show-configurations"] then
+    resolvers.load("nofiles")
+    resolvers.listers.configurations()
+elseif environment.arguments["help"] or (environment.files[1]=='help') or (#environment.files==0) then
+    logs.help(messages.help)
+else
+    resolvers.load()
+    resolvers.for_files(resolvers.find_files, environment.files, instance.my_format)
+end
+
+if logs.verbose then
+    logs.simpleline()
+    logs.simple("runtime: %0.3f seconds",os.runtime())
+end
+
+if os.platform == "unix" then
+    io.write("\n")
+end
diff --git a/scripts/context/stubs/mswin/metatex.exe b/scripts/context/stubs/mswin/metatex.exe
new file mode 100644
index 000000000..2d45f2749
Binary files /dev/null and b/scripts/context/stubs/mswin/metatex.exe differ
diff --git a/scripts/context/stubs/mswin/mtxrun.dll b/scripts/context/stubs/mswin/mtxrun.dll
new file mode 100644
index 000000000..23e476cac
Binary files /dev/null and b/scripts/context/stubs/mswin/mtxrun.dll differ
diff --git a/scripts/context/stubs/mswin/mtxrun.exe b/scripts/context/stubs/mswin/mtxrun.exe
new file mode 100644
index 000000000..745eaf224
Binary files /dev/null and b/scripts/context/stubs/mswin/mtxrun.exe differ
diff --git a/scripts/context/stubs/mswin/mtxrun.lua b/scripts/context/stubs/mswin/mtxrun.lua
new file mode 100644
index 000000000..b99327692
--- /dev/null
+++ b/scripts/context/stubs/mswin/mtxrun.lua
@@ -0,0 +1,12639 @@
+#!/usr/bin/env texlua
+
+if not modules then modules = { } end modules ['mtxrun'] = {
+    version   = 1.001,
+    comment   = "runner, lua replacement for texmfstart.rb",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+
+-- one can make a stub:
+--
+-- #!/bin/sh
+-- env LUATEXDIR=/....../texmf/scripts/context/lua luatex --luaonly mtxrun.lua "$@"
+
+-- filename : mtxrun.lua
+-- comment  : companion to context.tex
+-- author   : Hans Hagen, PRAGMA-ADE, Hasselt NL
+-- copyright: PRAGMA ADE / ConTeXt Development Team
+-- license  : see context related readme files
+
+-- This script is based on texmfstart.rb but does not use kpsewhich to
+-- locate files. Although kpse is a library it never came to opening up
+-- its interface to other programs (esp scripting languages) and so we
+-- do it ourselves. The lua variant evolved out of an experimental ruby
+-- one. Interesting is that using a scripting language instead of c does
+-- not have a speed penalty. Actually the lua variant is more efficient,
+-- especially when multiple calls to kpsewhich are involved. The lua
+-- library also gives way more control.
+
+-- to be done / considered
+--
+-- support for --exec or make it default
+-- support for jar files (or maybe not, never used, too messy)
+-- support for $RUBYINPUTS cum suis (if still needed)
+-- remember for subruns: _CTX_K_V_#{original}_
+-- remember for subruns: _CTX_K_S_#{original}_
+-- remember for subruns: TEXMFSTART.#{original} [tex.rb texmfstart.rb]
+
+texlua = true
+
+-- begin library merge
+
+
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-string'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local sub, gsub, find, match, gmatch, format, char, byte, rep, lower = string.sub, string.gsub, string.find, string.match, string.gmatch, string.format, string.char, string.byte, string.rep, string.lower
+local lpegmatch = lpeg.match
+
+-- some functions may disappear as they are not used anywhere
+
+if not string.split then
+
+    -- this will be overloaded by a faster lpeg variant
+
+    function string:split(pattern)
+        if #self > 0 then
+            local t = { }
+            for s in gmatch(self..pattern,"(.-)"..pattern) do
+                t[#t+1] = s
+            end
+            return t
+        else
+            return { }
+        end
+    end
+
+end
+
+local chr_to_esc = {
+    ["%"] = "%%",
+    ["."] = "%.",
+    ["+"] = "%+", ["-"] = "%-", ["*"] = "%*",
+    ["^"] = "%^", ["$"] = "%$",
+    ["["] = "%[", ["]"] = "%]",
+    ["("] = "%(", [")"] = "%)",
+    ["{"] = "%{", ["}"] = "%}"
+}
+
+string.chr_to_esc = chr_to_esc
+
+function string:esc() -- variant 2
+    return (gsub(self,"(.)",chr_to_esc))
+end
+
+function string:unquote()
+    return (gsub(self,"^([\"\'])(.*)%1$","%2"))
+end
+
+--~ function string:unquote()
+--~     if find(self,"^[\'\"]") then
+--~         return sub(self,2,-2)
+--~     else
+--~         return self
+--~     end
+--~ end
+
+function string:quote() -- we could use format("%q")
+    return format("%q",self)
+end
+
+function string:count(pattern) -- variant 3
+    local n = 0
+    for _ in gmatch(self,pattern) do
+        n = n + 1
+    end
+    return n
+end
+
+function string:limit(n,sentinel)
+    if #self > n then
+        sentinel = sentinel or " ..."
+        return sub(self,1,(n-#sentinel)) .. sentinel
+    else
+        return self
+    end
+end
+
+--~ function string:strip() -- the .- is quite efficient
+--~  -- return match(self,"^%s*(.-)%s*$") or ""
+--~  -- return match(self,'^%s*(.*%S)') or '' -- posted on lua list
+--~     return find(s,'^%s*$') and '' or match(s,'^%s*(.*%S)')
+--~ end
+
+do -- roberto's variant:
+    local space    = lpeg.S(" \t\v\n")
+    local nospace  = 1 - space
+    local stripper = space^0 * lpeg.C((space^0 * nospace^1)^0)
+    function string.strip(str)
+        return lpegmatch(stripper,str) or ""
+    end
+end
+
+function string:is_empty()
+    return not find(self,"%S")
+end
+
+function string:enhance(pattern,action)
+    local ok, n = true, 0
+    while ok do
+        ok = false
+        self = gsub(self,pattern, function(...)
+            ok, n = true, n + 1
+            return action(...)
+        end)
+    end
+    return self, n
+end
+
+local chr_to_hex, hex_to_chr = { }, { }
+
+for i=0,255 do
+    local c, h = char(i), format("%02X",i)
+    chr_to_hex[c], hex_to_chr[h] = h, c
+end
+
+function string:to_hex()
+    return (gsub(self or "","(.)",chr_to_hex))
+end
+
+function string:from_hex()
+    return (gsub(self or "","(..)",hex_to_chr))
+end
+
+if not string.characters then
+
+    local function nextchar(str, index)
+        index = index + 1
+        return (index <= #str) and index or nil, sub(str,index,index)
+    end
+    function string:characters()
+        return nextchar, self, 0
+    end
+    local function nextbyte(str, index)
+        index = index + 1
+        return (index <= #str) and index or nil, byte(sub(str,index,index))
+    end
+    function string:bytes()
+        return nextbyte, self, 0
+    end
+
+end
+
+-- we can use format for this (neg n)
+
+function string:rpadd(n,chr)
+    local m = n-#self
+    if m > 0 then
+        return self .. rep(chr or " ",m)
+    else
+        return self
+    end
+end
+
+function string:lpadd(n,chr)
+    local m = n-#self
+    if m > 0 then
+        return rep(chr or " ",m) .. self
+    else
+        return self
+    end
+end
+
+string.padd = string.rpadd
+
+function is_number(str) -- tonumber
+    return find(str,"^[%-%+]?[%d]-%.?[%d+]$") == 1
+end
+
+--~ print(is_number("1"))
+--~ print(is_number("1.1"))
+--~ print(is_number(".1"))
+--~ print(is_number("-0.1"))
+--~ print(is_number("+0.1"))
+--~ print(is_number("-.1"))
+--~ print(is_number("+.1"))
+
+function string:split_settings() -- no {} handling, see l-aux for lpeg variant
+    if find(self,"=") then
+        local t = { }
+        for k,v in gmatch(self,"(%a+)=([^%,]*)") do
+            t[k] = v
+        end
+        return t
+    else
+        return nil
+    end
+end
+
+local patterns_escapes = {
+    ["-"] = "%-",
+    ["."] = "%.",
+    ["+"] = "%+",
+    ["*"] = "%*",
+    ["%"] = "%%",
+    ["("] = "%)",
+    [")"] = "%)",
+    ["["] = "%[",
+    ["]"] = "%]",
+}
+
+function string:pattesc()
+    return (gsub(self,".",patterns_escapes))
+end
+
+local simple_escapes = {
+    ["-"] = "%-",
+    ["."] = "%.",
+    ["?"] = ".",
+    ["*"] = ".*",
+}
+
+function string:simpleesc()
+    return (gsub(self,".",simple_escapes))
+end
+
+function string:tohash()
+    local t = { }
+    for s in gmatch(self,"([^, ]+)") do -- lpeg
+        t[s] = true
+    end
+    return t
+end
+
+local pattern = lpeg.Ct(lpeg.C(1)^0)
+
+function string:totable()
+    return lpegmatch(pattern,self)
+end
+
+--~ local t = {
+--~     "1234567123456712345671234567",
+--~     "a\tb\tc",
+--~     "aa\tbb\tcc",
+--~     "aaa\tbbb\tccc",
+--~     "aaaa\tbbbb\tcccc",
+--~     "aaaaa\tbbbbb\tccccc",
+--~     "aaaaaa\tbbbbbb\tcccccc",
+--~ }
+--~ for k,v do
+--~     print(string.tabtospace(t[k]))
+--~ end
+
+function string.tabtospace(str,tab)
+    -- we don't handle embedded newlines
+    while true do
+        local s = find(str,"\t")
+        if s then
+            if not tab then tab = 7 end -- only when found
+            local d = tab-(s-1) % tab
+            if d > 0 then
+                str = gsub(str,"\t",rep(" ",d),1)
+            else
+                str = gsub(str,"\t","",1)
+            end
+        else
+            break
+        end
+    end
+    return str
+end
+
+function string:compactlong() -- strips newlines and leading spaces
+    self = gsub(self,"[\n\r]+ *","")
+    self = gsub(self,"^ *","")
+    return self
+end
+
+function string:striplong() -- strips newlines and leading spaces
+    self = gsub(self,"^%s*","")
+    self = gsub(self,"[\n\r]+ *","\n")
+    return self
+end
+
+function string:topattern(lowercase,strict)
+    if lowercase then
+        self = lower(self)
+    end
+    self = gsub(self,".",simple_escapes)
+    if self == "" then
+        self = ".*"
+    elseif strict then
+        self = "^" .. self .. "$"
+    end
+    return self
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-lpeg'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local lpeg = require("lpeg")
+
+lpeg.patterns  = lpeg.patterns or { } -- so that we can share
+local patterns = lpeg.patterns
+
+local P, R, S, Ct, C, Cs, Cc, V = lpeg.P, lpeg.R, lpeg.S, lpeg.Ct, lpeg.C, lpeg.Cs, lpeg.Cc, lpeg.V
+local match = lpeg.match
+
+local digit, sign      = R('09'), S('+-')
+local cr, lf, crlf     = P("\r"), P("\n"), P("\r\n")
+local utf8byte         = R("\128\191")
+
+patterns.utf8byte      = utf8byte
+patterns.utf8one       = R("\000\127")
+patterns.utf8two       = R("\194\223") * utf8byte
+patterns.utf8three     = R("\224\239") * utf8byte * utf8byte
+patterns.utf8four      = R("\240\244") * utf8byte * utf8byte * utf8byte
+
+patterns.digit         = digit
+patterns.sign          = sign
+patterns.cardinal      = sign^0 * digit^1
+patterns.integer       = sign^0 * digit^1
+patterns.float         = sign^0 * digit^0 * P('.') * digit^1
+patterns.number        = patterns.float + patterns.integer
+patterns.oct           = P("0") * R("07")^1
+patterns.octal         = patterns.oct
+patterns.HEX           = P("0x") * R("09","AF")^1
+patterns.hex           = P("0x") * R("09","af")^1
+patterns.hexadecimal   = P("0x") * R("09","AF","af")^1
+patterns.lowercase     = R("az")
+patterns.uppercase     = R("AZ")
+patterns.letter        = patterns.lowercase + patterns.uppercase
+patterns.space         = S(" ")
+patterns.eol           = S("\n\r")
+patterns.spacer        = S(" \t\f\v")  -- + string.char(0xc2, 0xa0) if we want utf (cf mail roberto)
+patterns.newline       = crlf + cr + lf
+patterns.nonspace      = 1 - patterns.space
+patterns.nonspacer     = 1 - patterns.spacer
+patterns.whitespace    = patterns.eol + patterns.spacer
+patterns.nonwhitespace = 1 - patterns.whitespace
+patterns.utf8          = patterns.utf8one + patterns.utf8two + patterns.utf8three + patterns.utf8four
+patterns.utfbom        = P('\000\000\254\255') + P('\255\254\000\000') + P('\255\254') + P('\254\255') + P('\239\187\191')
+
+function lpeg.anywhere(pattern) --slightly adapted from website
+    return P { P(pattern) + 1 * V(1) } -- why so complex?
+end
+
+function lpeg.splitter(pattern, action)
+    return (((1-P(pattern))^1)/action+1)^0
+end
+
+local spacing  = patterns.spacer^0 * patterns.newline -- sort of strip
+local empty    = spacing * Cc("")
+local nonempty = Cs((1-spacing)^1) * spacing^-1
+local content  = (empty + nonempty)^1
+
+local capture = Ct(content^0)
+
+function string:splitlines()
+    return match(capture,self)
+end
+
+patterns.textline = content
+
+--~ local p = lpeg.splitat("->",false)  print(match(p,"oeps->what->more"))  -- oeps what more
+--~ local p = lpeg.splitat("->",true)   print(match(p,"oeps->what->more"))  -- oeps what->more
+--~ local p = lpeg.splitat("->",false)  print(match(p,"oeps"))              -- oeps
+--~ local p = lpeg.splitat("->",true)   print(match(p,"oeps"))              -- oeps
+
+local splitters_s, splitters_m = { }, { }
+
+local function splitat(separator,single)
+    local splitter = (single and splitters_s[separator]) or splitters_m[separator]
+    if not splitter then
+        separator = P(separator)
+        if single then
+            local other, any = C((1 - separator)^0), P(1)
+            splitter = other * (separator * C(any^0) + "") -- ?
+            splitters_s[separator] = splitter
+        else
+            local other = C((1 - separator)^0)
+            splitter = other * (separator * other)^0
+            splitters_m[separator] = splitter
+        end
+    end
+    return splitter
+end
+
+lpeg.splitat = splitat
+
+local cache = { }
+
+function lpeg.split(separator,str)
+    local c = cache[separator]
+    if not c then
+        c = Ct(splitat(separator))
+        cache[separator] = c
+    end
+    return match(c,str)
+end
+
+function string:split(separator)
+    local c = cache[separator]
+    if not c then
+        c = Ct(splitat(separator))
+        cache[separator] = c
+    end
+    return match(c,self)
+end
+
+lpeg.splitters = cache
+
+local cache = { }
+
+function lpeg.checkedsplit(separator,str)
+    local c = cache[separator]
+    if not c then
+        separator = P(separator)
+        local other = C((1 - separator)^0)
+        c = Ct(separator^0 * other * (separator^1 * other)^0)
+        cache[separator] = c
+    end
+    return match(c,str)
+end
+
+function string:checkedsplit(separator)
+    local c = cache[separator]
+    if not c then
+        separator = P(separator)
+        local other = C((1 - separator)^0)
+        c = Ct(separator^0 * other * (separator^1 * other)^0)
+        cache[separator] = c
+    end
+    return match(c,self)
+end
+
+--~ function lpeg.append(list,pp)
+--~     local p = pp
+--~     for l=1,#list do
+--~         if p then
+--~             p = p + P(list[l])
+--~         else
+--~             p = P(list[l])
+--~         end
+--~     end
+--~     return p
+--~ end
+
+--~ from roberto's site:
+
+local f1 = string.byte
+
+local function f2(s) local c1, c2         = f1(s,1,2) return   c1 * 64 + c2                       -    12416 end
+local function f3(s) local c1, c2, c3     = f1(s,1,3) return  (c1 * 64 + c2) * 64 + c3            -   925824 end
+local function f4(s) local c1, c2, c3, c4 = f1(s,1,4) return ((c1 * 64 + c2) * 64 + c3) * 64 + c4 - 63447168 end
+
+patterns.utf8byte = patterns.utf8one/f1 + patterns.utf8two/f2 + patterns.utf8three/f3 + patterns.utf8four/f4
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-table'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+table.join = table.concat
+
+local concat, sort, insert, remove = table.concat, table.sort, table.insert, table.remove
+local format, find, gsub, lower, dump, match = string.format, string.find, string.gsub, string.lower, string.dump, string.match
+local getmetatable, setmetatable = getmetatable, setmetatable
+local type, next, tostring, tonumber, ipairs = type, next, tostring, tonumber, ipairs
+
+-- Starting with version 5.2 Lua no longer provide ipairs, which makes
+-- sense. As we already used the for loop and # in most places the
+-- impact on ConTeXt was not that large; the remaining ipairs already
+-- have been replaced. In a similar fashio we also hardly used pairs.
+--
+-- Just in case, we provide the fallbacks as discussed in Programming
+-- in Lua (http://www.lua.org/pil/7.3.html):
+
+if not ipairs then
+
+    -- for k, v in ipairs(t) do                ... end
+    -- for k=1,#t            do local v = t[k] ... end
+
+    local function iterate(a,i)
+        i = i + 1
+        local v = a[i]
+        if v ~= nil then
+            return i, v --, nil
+        end
+    end
+
+    function ipairs(a)
+        return iterate, a, 0
+    end
+
+end
+
+if not pairs then
+
+    -- for k, v in pairs(t) do ... end
+    -- for k, v in next, t  do ... end
+
+    function pairs(t)
+        return next, t -- , nil
+    end
+
+end
+
+-- Also, unpack has been moved to the table table, and for compatiility
+-- reasons we provide both now.
+
+if not table.unpack then
+    table.unpack = _G.unpack
+elseif not unpack then
+    _G.unpack = table.unpack
+end
+
+-- extra functions, some might go (when not used)
+
+function table.strip(tab)
+    local lst = { }
+    for i=1,#tab do
+        local s = gsub(tab[i],"^%s*(.-)%s*$","%1")
+        if s == "" then
+            -- skip this one
+        else
+            lst[#lst+1] = s
+        end
+    end
+    return lst
+end
+
+function table.keys(t)
+    local k = { }
+    for key, _ in next, t do
+        k[#k+1] = key
+    end
+    return k
+end
+
+local function compare(a,b)
+    return (tostring(a) < tostring(b))
+end
+
+local function sortedkeys(tab)
+    local srt, kind = { }, 0 -- 0=unknown 1=string, 2=number 3=mixed
+    for key,_ in next, tab do
+        srt[#srt+1] = key
+        if kind == 3 then
+            -- no further check
+        else
+            local tkey = type(key)
+            if tkey == "string" then
+            --  if kind == 2 then kind = 3 else kind = 1 end
+                kind = (kind == 2 and 3) or 1
+            elseif tkey == "number" then
+            --  if kind == 1 then kind = 3 else kind = 2 end
+                kind = (kind == 1 and 3) or 2
+            else
+                kind = 3
+            end
+        end
+    end
+    if kind == 0 or kind == 3 then
+        sort(srt,compare)
+    else
+        sort(srt)
+    end
+    return srt
+end
+
+local function sortedhashkeys(tab) -- fast one
+    local srt = { }
+    for key,_ in next, tab do
+        srt[#srt+1] = key
+    end
+    sort(srt)
+    return srt
+end
+
+table.sortedkeys     = sortedkeys
+table.sortedhashkeys = sortedhashkeys
+
+function table.sortedhash(t)
+    local s = sortedhashkeys(t) -- maybe just sortedkeys
+    local n = 0
+    local function kv(s)
+        n = n + 1
+        local k = s[n]
+        return k, t[k]
+    end
+    return kv, s
+end
+
+table.sortedpairs = table.sortedhash
+
+function table.append(t, list)
+    for _,v in next, list do
+        insert(t,v)
+    end
+end
+
+function table.prepend(t, list)
+    for k,v in next, list do
+        insert(t,k,v)
+    end
+end
+
+function table.merge(t, ...) -- first one is target
+    t = t or {}
+    local lst = {...}
+    for i=1,#lst do
+        for k, v in next, lst[i] do
+            t[k] = v
+        end
+    end
+    return t
+end
+
+function table.merged(...)
+    local tmp, lst = { }, {...}
+    for i=1,#lst do
+        for k, v in next, lst[i] do
+            tmp[k] = v
+        end
+    end
+    return tmp
+end
+
+function table.imerge(t, ...)
+    local lst = {...}
+    for i=1,#lst do
+        local nst = lst[i]
+        for j=1,#nst do
+            t[#t+1] = nst[j]
+        end
+    end
+    return t
+end
+
+function table.imerged(...)
+    local tmp, lst = { }, {...}
+    for i=1,#lst do
+        local nst = lst[i]
+        for j=1,#nst do
+            tmp[#tmp+1] = nst[j]
+        end
+    end
+    return tmp
+end
+
+local function fastcopy(old) -- fast one
+    if old then
+        local new = { }
+        for k,v in next, old do
+            if type(v) == "table" then
+                new[k] = fastcopy(v) -- was just table.copy
+            else
+                new[k] = v
+            end
+        end
+        -- optional second arg
+        local mt = getmetatable(old)
+        if mt then
+            setmetatable(new,mt)
+        end
+        return new
+    else
+        return { }
+    end
+end
+
+local function copy(t, tables) -- taken from lua wiki, slightly adapted
+    tables = tables or { }
+    local tcopy = {}
+    if not tables[t] then
+        tables[t] = tcopy
+    end
+    for i,v in next, t do -- brrr, what happens with sparse indexed
+        if type(i) == "table" then
+            if tables[i] then
+                i = tables[i]
+            else
+                i = copy(i, tables)
+            end
+        end
+        if type(v) ~= "table" then
+            tcopy[i] = v
+        elseif tables[v] then
+            tcopy[i] = tables[v]
+        else
+            tcopy[i] = copy(v, tables)
+        end
+    end
+    local mt = getmetatable(t)
+    if mt then
+        setmetatable(tcopy,mt)
+    end
+    return tcopy
+end
+
+table.fastcopy = fastcopy
+table.copy     = copy
+
+-- roughly: copy-loop : unpack : sub == 0.9 : 0.4 : 0.45 (so in critical apps, use unpack)
+
+function table.sub(t,i,j)
+    return { unpack(t,i,j) }
+end
+
+function table.replace(a,b)
+    for k,v in next, b do
+        a[k] = v
+    end
+end
+
+-- slower than #t on indexed tables (#t only returns the size of the numerically indexed slice)
+
+function table.is_empty(t) -- obolete, use inline code instead
+    return not t or not next(t)
+end
+
+function table.one_entry(t) -- obolete, use inline code instead
+    local n = next(t)
+    return n and not next(t,n)
+end
+
+--~ function table.starts_at(t) -- obsolete, not nice anyway
+--~     return ipairs(t,1)(t,0)
+--~ end
+
+function table.tohash(t,value)
+    local h = { }
+    if t then
+        if value == nil then value = true end
+        for _, v in next, t do -- no ipairs here
+            h[v] = value
+        end
+    end
+    return h
+end
+
+function table.fromhash(t)
+    local h = { }
+    for k, v in next, t do -- no ipairs here
+        if v then h[#h+1] = k end
+    end
+    return h
+end
+
+--~ print(table.serialize(t), "\n")
+--~ print(table.serialize(t,"name"), "\n")
+--~ print(table.serialize(t,false), "\n")
+--~ print(table.serialize(t,true), "\n")
+--~ print(table.serialize(t,"name",true), "\n")
+--~ print(table.serialize(t,"name",true,true), "\n")
+
+table.serialize_functions = true
+table.serialize_compact   = true
+table.serialize_inline    = true
+
+local noquotes, hexify, handle, reduce, compact, inline, functions
+
+local reserved = table.tohash { -- intercept a language flaw, no reserved words as key
+    'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'if',
+    'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while',
+}
+
+local function simple_table(t)
+    if #t > 0 then
+        local n = 0
+        for _,v in next, t do
+            n = n + 1
+        end
+        if n == #t then
+            local tt = { }
+            for i=1,#t do
+                local v = t[i]
+                local tv = type(v)
+                if tv == "number" then
+                    if hexify then
+                        tt[#tt+1] = format("0x%04X",v)
+                    else
+                        tt[#tt+1] = tostring(v) -- tostring not needed
+                    end
+                elseif tv == "boolean" then
+                    tt[#tt+1] = tostring(v)
+                elseif tv == "string" then
+                    tt[#tt+1] = format("%q",v)
+                else
+                    tt = nil
+                    break
+                end
+            end
+            return tt
+        end
+    end
+    return nil
+end
+
+-- Because this is a core function of mkiv I moved some function calls
+-- inline.
+--
+-- twice as fast in a test:
+--
+-- local propername = lpeg.P(lpeg.R("AZ","az","__") * lpeg.R("09","AZ","az", "__")^0 * lpeg.P(-1) )
+
+-- problem: there no good number_to_string converter with the best resolution
+
+local function do_serialize(root,name,depth,level,indexed)
+    if level > 0 then
+        depth = depth .. " "
+        if indexed then
+            handle(format("%s{",depth))
+        elseif name then
+        --~ handle(format("%s%s={",depth,key(name)))
+            if type(name) == "number" then -- or find(k,"^%d+$") then
+                if hexify then
+                    handle(format("%s[0x%04X]={",depth,name))
+                else
+                    handle(format("%s[%s]={",depth,name))
+                end
+            elseif noquotes and not reserved[name] and find(name,"^%a[%w%_]*$") then
+                handle(format("%s%s={",depth,name))
+            else
+                handle(format("%s[%q]={",depth,name))
+            end
+        else
+            handle(format("%s{",depth))
+        end
+    end
+    -- we could check for k (index) being number (cardinal)
+    if root and next(root) then
+        local first, last = nil, 0 -- #root cannot be trusted here (will be ok in 5.2 when ipairs is gone)
+        if compact then
+            -- NOT: for k=1,#root do (we need to quit at nil)
+            for k,v in ipairs(root) do -- can we use next?
+                if not first then first = k end
+                last = last + 1
+            end
+        end
+        local sk = sortedkeys(root)
+        for i=1,#sk do
+            local k = sk[i]
+            local v = root[k]
+            --~ if v == root then
+                -- circular
+            --~ else
+            local t = type(v)
+            if compact and first and type(k) == "number" and k >= first and k <= last then
+                if t == "number" then
+                    if hexify then
+                        handle(format("%s 0x%04X,",depth,v))
+                    else
+                        handle(format("%s %s,",depth,v)) -- %.99g
+                    end
+                elseif t == "string" then
+                    if reduce and tonumber(v) then
+                        handle(format("%s %s,",depth,v))
+                    else
+                        handle(format("%s %q,",depth,v))
+                    end
+                elseif t == "table" then
+                    if not next(v) then
+                        handle(format("%s {},",depth))
+                    elseif inline then -- and #t > 0
+                        local st = simple_table(v)
+                        if st then
+                            handle(format("%s { %s },",depth,concat(st,", ")))
+                        else
+                            do_serialize(v,k,depth,level+1,true)
+                        end
+                    else
+                        do_serialize(v,k,depth,level+1,true)
+                    end
+                elseif t == "boolean" then
+                    handle(format("%s %s,",depth,tostring(v)))
+                elseif t == "function" then
+                    if functions then
+                        handle(format('%s loadstring(%q),',depth,dump(v)))
+                    else
+                        handle(format('%s "function",',depth))
+                    end
+                else
+                    handle(format("%s %q,",depth,tostring(v)))
+                end
+            elseif k == "__p__" then -- parent
+                if false then
+                    handle(format("%s __p__=nil,",depth))
+                end
+            elseif t == "number" then
+            --~ if hexify then
+            --~     handle(format("%s %s=0x%04X,",depth,key(k),v))
+            --~ else
+            --~     handle(format("%s %s=%s,",depth,key(k),v)) -- %.99g
+            --~ end
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s [%s]=%s,",depth,k,v)) -- %.99g
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    if hexify then
+                        handle(format("%s %s=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s %s=%s,",depth,k,v)) -- %.99g
+                    end
+                else
+                    if hexify then
+                        handle(format("%s [%q]=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%s,",depth,k,v)) -- %.99g
+                    end
+                end
+            elseif t == "string" then
+                if reduce and tonumber(v) then
+                --~ handle(format("%s %s=%s,",depth,key(k),v))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=%s,",depth,k,v))
+                        else
+                            handle(format("%s [%s]=%s,",depth,k,v))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=%s,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%s,",depth,k,v))
+                    end
+                else
+                --~ handle(format("%s %s=%q,",depth,key(k),v))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=%q,",depth,k,v))
+                        else
+                            handle(format("%s [%s]=%q,",depth,k,v))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=%q,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%q,",depth,k,v))
+                    end
+                end
+            elseif t == "table" then
+                if not next(v) then
+                    --~ handle(format("%s %s={},",depth,key(k)))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]={},",depth,k))
+                        else
+                            handle(format("%s [%s]={},",depth,k))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s={},",depth,k))
+                    else
+                        handle(format("%s [%q]={},",depth,k))
+                    end
+                elseif inline then
+                    local st = simple_table(v)
+                    if st then
+                    --~ handle(format("%s %s={ %s },",depth,key(k),concat(st,", ")))
+                        if type(k) == "number" then -- or find(k,"^%d+$") then
+                            if hexify then
+                                handle(format("%s [0x%04X]={ %s },",depth,k,concat(st,", ")))
+                            else
+                                handle(format("%s [%s]={ %s },",depth,k,concat(st,", ")))
+                            end
+                        elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                            handle(format("%s %s={ %s },",depth,k,concat(st,", ")))
+                        else
+                            handle(format("%s [%q]={ %s },",depth,k,concat(st,", ")))
+                        end
+                    else
+                        do_serialize(v,k,depth,level+1)
+                    end
+                else
+                    do_serialize(v,k,depth,level+1)
+                end
+            elseif t == "boolean" then
+            --~ handle(format("%s %s=%s,",depth,key(k),tostring(v)))
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=%s,",depth,k,tostring(v)))
+                    else
+                        handle(format("%s [%s]=%s,",depth,k,tostring(v)))
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    handle(format("%s %s=%s,",depth,k,tostring(v)))
+                else
+                    handle(format("%s [%q]=%s,",depth,k,tostring(v)))
+                end
+            elseif t == "function" then
+                if functions then
+                    --~ handle(format('%s %s=loadstring(%q),',depth,key(k),dump(v)))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=loadstring(%q),",depth,k,dump(v)))
+                        else
+                            handle(format("%s [%s]=loadstring(%q),",depth,k,dump(v)))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=loadstring(%q),",depth,k,dump(v)))
+                    else
+                        handle(format("%s [%q]=loadstring(%q),",depth,k,dump(v)))
+                    end
+                end
+            else
+                --~ handle(format("%s %s=%q,",depth,key(k),tostring(v)))
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=%q,",depth,k,tostring(v)))
+                    else
+                        handle(format("%s [%s]=%q,",depth,k,tostring(v)))
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    handle(format("%s %s=%q,",depth,k,tostring(v)))
+                else
+                    handle(format("%s [%q]=%q,",depth,k,tostring(v)))
+                end
+            end
+            --~ end
+        end
+    end
+   if level > 0 then
+        handle(format("%s},",depth))
+    end
+end
+
+-- replacing handle by a direct t[#t+1] = ... (plus test) is not much
+-- faster (0.03 on 1.00 for zapfino.tma)
+
+local function serialize(root,name,_handle,_reduce,_noquotes,_hexify)
+    noquotes = _noquotes
+    hexify = _hexify
+    handle = _handle or print
+    reduce = _reduce or false
+    compact = table.serialize_compact
+    inline  = compact and table.serialize_inline
+    functions = table.serialize_functions
+    local tname = type(name)
+    if tname == "string" then
+        if name == "return" then
+            handle("return {")
+        else
+            handle(name .. "={")
+        end
+    elseif tname == "number" then
+        if hexify then
+            handle(format("[0x%04X]={",name))
+        else
+            handle("[" .. name .. "]={")
+        end
+    elseif tname == "boolean" then
+        if name then
+            handle("return {")
+        else
+            handle("{")
+        end
+    else
+        handle("t={")
+    end
+    if root and next(root) then
+        do_serialize(root,name,"",0,indexed)
+    end
+    handle("}")
+end
+
+--~ name:
+--~
+--~ true     : return     { }
+--~ false    :            { }
+--~ nil      : t        = { }
+--~ string   : string   = { }
+--~ 'return' : return     { }
+--~ number   : [number] = { }
+
+function table.serialize(root,name,reduce,noquotes,hexify)
+    local t = { }
+    local function flush(s)
+        t[#t+1] = s
+    end
+    serialize(root,name,flush,reduce,noquotes,hexify)
+    return concat(t,"\n")
+end
+
+function table.tohandle(handle,root,name,reduce,noquotes,hexify)
+    serialize(root,name,handle,reduce,noquotes,hexify)
+end
+
+-- sometimes tables are real use (zapfino extra pro is some 85M) in which
+-- case a stepwise serialization is nice; actually, we could consider:
+--
+-- for line in table.serializer(root,name,reduce,noquotes) do
+--    ...(line)
+-- end
+--
+-- so this is on the todo list
+
+table.tofile_maxtab = 2*1024
+
+function table.tofile(filename,root,name,reduce,noquotes,hexify)
+    local f = io.open(filename,'w')
+    if f then
+        local maxtab = table.tofile_maxtab
+        if maxtab > 1 then
+            local t = { }
+            local function flush(s)
+                t[#t+1] = s
+                if #t > maxtab then
+                    f:write(concat(t,"\n"),"\n") -- hm, write(sometable) should be nice
+                    t = { }
+                end
+            end
+            serialize(root,name,flush,reduce,noquotes,hexify)
+            f:write(concat(t,"\n"),"\n")
+        else
+            local function flush(s)
+                f:write(s,"\n")
+            end
+            serialize(root,name,flush,reduce,noquotes,hexify)
+        end
+        f:close()
+    end
+end
+
+local function flatten(t,f,complete) -- is this used? meybe a variant with next, ...
+    for i=1,#t do
+        local v = t[i]
+        if type(v) == "table" then
+            if complete or type(v[1]) == "table" then
+                flatten(v,f,complete)
+            else
+                f[#f+1] = v
+            end
+        else
+            f[#f+1] = v
+        end
+    end
+end
+
+function table.flatten(t)
+    local f = { }
+    flatten(t,f,true)
+    return f
+end
+
+function table.unnest(t) -- bad name
+    local f = { }
+    flatten(t,f,false)
+    return f
+end
+
+table.flatten_one_level = table.unnest
+
+-- a better one:
+
+local function flattened(t,f)
+    if not f then
+        f = { }
+    end
+    for k, v in next, t do
+        if type(v) == "table" then
+            flattened(v,f)
+        else
+            f[k] = v
+        end
+    end
+    return f
+end
+
+table.flattened = flattened
+
+-- the next three may disappear
+
+function table.remove_value(t,value) -- todo: n
+    if value then
+        for i=1,#t do
+            if t[i] == value then
+                remove(t,i)
+                -- remove all, so no: return
+            end
+        end
+    end
+end
+
+function table.insert_before_value(t,value,str)
+    if str then
+        if value then
+            for i=1,#t do
+                if t[i] == value then
+                    insert(t,i,str)
+                    return
+                end
+            end
+        end
+        insert(t,1,str)
+    elseif value then
+        insert(t,1,value)
+    end
+end
+
+function table.insert_after_value(t,value,str)
+    if str then
+        if value then
+            for i=1,#t do
+                if t[i] == value then
+                    insert(t,i+1,str)
+                    return
+                end
+            end
+        end
+        t[#t+1] = str
+    elseif value then
+        t[#t+1] = value
+    end
+end
+
+local function are_equal(a,b,n,m) -- indexed
+    if a and b and #a == #b then
+        n = n or 1
+        m = m or #a
+        for i=n,m do
+            local ai, bi = a[i], b[i]
+            if ai==bi then
+                -- same
+            elseif type(ai)=="table" and type(bi)=="table" then
+                if not are_equal(ai,bi) then
+                    return false
+                end
+            else
+                return false
+            end
+        end
+        return true
+    else
+        return false
+    end
+end
+
+local function identical(a,b) -- assumes same structure
+    for ka, va in next, a do
+        local vb = b[k]
+        if va == vb then
+            -- same
+        elseif type(va) == "table" and  type(vb) == "table" then
+            if not identical(va,vb) then
+                return false
+            end
+        else
+            return false
+        end
+    end
+    return true
+end
+
+table.are_equal = are_equal
+table.identical = identical
+
+-- maybe also make a combined one
+
+function table.compact(t)
+    if t then
+        for k,v in next, t do
+            if not next(v) then
+                t[k] = nil
+            end
+        end
+    end
+end
+
+function table.contains(t, v)
+    if t then
+        for i=1, #t do
+            if t[i] == v then
+                return i
+            end
+        end
+    end
+    return false
+end
+
+function table.count(t)
+    local n, e = 0, next(t)
+    while e do
+        n, e = n + 1, next(t,e)
+    end
+    return n
+end
+
+function table.swapped(t)
+    local s = { }
+    for k, v in next, t do
+        s[v] = k
+    end
+    return s
+end
+
+--~ function table.are_equal(a,b)
+--~     return table.serialize(a) == table.serialize(b)
+--~ end
+
+function table.clone(t,p) -- t is optional or nil or table
+    if not p then
+        t, p = { }, t or { }
+    elseif not t then
+        t = { }
+    end
+    setmetatable(t, { __index = function(_,key) return p[key] end }) -- why not __index = p ?
+    return t
+end
+
+function table.hexed(t,seperator)
+    local tt = { }
+    for i=1,#t do tt[i] = format("0x%04X",t[i]) end
+    return concat(tt,seperator or " ")
+end
+
+function table.reverse_hash(h)
+    local r = { }
+    for k,v in next, h do
+        r[v] = lower(gsub(k," ",""))
+    end
+    return r
+end
+
+function table.reverse(t)
+    local tt = { }
+    if #t > 0 then
+        for i=#t,1,-1 do
+            tt[#tt+1] = t[i]
+        end
+    end
+    return tt
+end
+
+function table.insert_before_value(t,value,extra)
+    for i=1,#t do
+        if t[i] == extra then
+            remove(t,i)
+        end
+    end
+    for i=1,#t do
+        if t[i] == value then
+            insert(t,i,extra)
+            return
+        end
+    end
+    insert(t,1,extra)
+end
+
+function table.insert_after_value(t,value,extra)
+    for i=1,#t do
+        if t[i] == extra then
+            remove(t,i)
+        end
+    end
+    for i=1,#t do
+        if t[i] == value then
+            insert(t,i+1,extra)
+            return
+        end
+    end
+    insert(t,#t+1,extra)
+end
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-io'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local byte, find, gsub = string.byte, string.find, string.gsub
+
+if string.find(os.getenv("PATH"),";") then
+    io.fileseparator, io.pathseparator = "\\", ";"
+else
+    io.fileseparator, io.pathseparator = "/" , ":"
+end
+
+function io.loaddata(filename,textmode)
+    local f = io.open(filename,(textmode and 'r') or 'rb')
+    if f then
+    --  collectgarbage("step") -- sometimes makes a big difference in mem consumption
+        local data = f:read('*all')
+    --  garbagecollector.check(data)
+        f:close()
+        return data
+    else
+        return nil
+    end
+end
+
+function io.savedata(filename,data,joiner)
+    local f = io.open(filename,"wb")
+    if f then
+        if type(data) == "table" then
+            f:write(table.join(data,joiner or ""))
+        elseif type(data) == "function" then
+            data(f)
+        else
+            f:write(data or "")
+        end
+        f:close()
+        return true
+    else
+        return false
+    end
+end
+
+function io.exists(filename)
+    local f = io.open(filename)
+    if f == nil then
+        return false
+    else
+        assert(f:close())
+        return true
+    end
+end
+
+function io.size(filename)
+    local f = io.open(filename)
+    if f == nil then
+        return 0
+    else
+        local s = f:seek("end")
+        assert(f:close())
+        return s
+    end
+end
+
+function io.noflines(f)
+    local n = 0
+    for _ in f:lines() do
+        n = n + 1
+    end
+    f:seek('set',0)
+    return n
+end
+
+local nextchar = {
+    [ 4] = function(f)
+        return f:read(1,1,1,1)
+    end,
+    [ 2] = function(f)
+        return f:read(1,1)
+    end,
+    [ 1] = function(f)
+        return f:read(1)
+    end,
+    [-2] = function(f)
+        local a, b = f:read(1,1)
+        return b, a
+    end,
+    [-4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        return d, c, b, a
+    end
+}
+
+function io.characters(f,n)
+    if f then
+        return nextchar[n or 1], f
+    else
+        return nil, nil
+    end
+end
+
+local nextbyte = {
+    [4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        if d then
+            return byte(a), byte(b), byte(c), byte(d)
+        else
+            return nil, nil, nil, nil
+        end
+    end,
+    [2] = function(f)
+        local a, b = f:read(1,1)
+        if b then
+            return byte(a), byte(b)
+        else
+            return nil, nil
+        end
+    end,
+    [1] = function (f)
+        local a = f:read(1)
+        if a then
+            return byte(a)
+        else
+            return nil
+        end
+    end,
+    [-2] = function (f)
+        local a, b = f:read(1,1)
+        if b then
+            return byte(b), byte(a)
+        else
+            return nil, nil
+        end
+    end,
+    [-4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        if d then
+            return byte(d), byte(c), byte(b), byte(a)
+        else
+            return nil, nil, nil, nil
+        end
+    end
+}
+
+function io.bytes(f,n)
+    if f then
+        return nextbyte[n or 1], f
+    else
+        return nil, nil
+    end
+end
+
+function io.ask(question,default,options)
+    while true do
+        io.write(question)
+        if options then
+            io.write(string.format(" [%s]",table.concat(options,"|")))
+        end
+        if default then
+            io.write(string.format(" [%s]",default))
+        end
+        io.write(string.format(" "))
+        local answer = io.read()
+        answer = gsub(answer,"^%s*(.*)%s*$","%1")
+        if answer == "" and default then
+            return default
+        elseif not options then
+            return answer
+        else
+            for k=1,#options do
+                if options[k] == answer then
+                    return answer
+                end
+            end
+            local pattern = "^" .. answer
+            for k=1,#options do
+                local v = options[k]
+                if find(v,pattern) then
+                    return v
+                end
+            end
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-number'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local tostring = tostring
+local format, floor, insert, match = string.format, math.floor, table.insert, string.match
+local lpegmatch = lpeg.match
+
+number = number or { }
+
+-- a,b,c,d,e,f = number.toset(100101)
+
+function number.toset(n)
+    return match(tostring(n),"(.?)(.?)(.?)(.?)(.?)(.?)(.?)(.?)")
+end
+
+function number.toevenhex(n)
+    local s = format("%X",n)
+    if #s % 2 == 0 then
+        return s
+    else
+        return "0" .. s
+    end
+end
+
+-- the lpeg way is slower on 8 digits, but faster on 4 digits, some 7.5%
+-- on
+--
+-- for i=1,1000000 do
+--     local a,b,c,d,e,f,g,h = number.toset(12345678)
+--     local a,b,c,d         = number.toset(1234)
+--     local a,b,c           = number.toset(123)
+-- end
+--
+-- of course dedicated "(.)(.)(.)(.)" matches are even faster
+
+local one = lpeg.C(1-lpeg.S(''))^1
+
+function number.toset(n)
+    return lpegmatch(one,tostring(n))
+end
+
+function number.bits(n,zero)
+    local t, i = { }, (zero and 0) or 1
+    while n > 0 do
+        local m = n % 2
+        if m > 0 then
+            insert(t,1,i)
+        end
+        n = floor(n/2)
+        i = i + 1
+    end
+    return t
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-set'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+set = set or { }
+
+local nums   = { }
+local tabs   = { }
+local concat = table.concat
+local next, type = next, type
+
+set.create = table.tohash
+
+function set.tonumber(t)
+    if next(t) then
+        local s = ""
+    --  we could save mem by sorting, but it slows down
+        for k, v in next, t do
+            if v then
+            --  why bother about the leading space
+                s = s .. " " .. k
+            end
+        end
+        local n = nums[s]
+        if not n then
+            n = #tabs + 1
+            tabs[n] = t
+            nums[s] = n
+        end
+        return n
+    else
+        return 0
+    end
+end
+
+function set.totable(n)
+    if n == 0 then
+        return { }
+    else
+        return tabs[n] or { }
+    end
+end
+
+function set.tolist(n)
+    if n == 0 or not tabs[n] then
+        return ""
+    else
+        local t = { }
+        for k, v in next, tabs[n] do
+            if v then
+                t[#t+1] = k
+            end
+        end
+        return concat(t," ")
+    end
+end
+
+function set.contains(n,s)
+    if type(n) == "table" then
+        return n[s]
+    elseif n == 0 then
+        return false
+    else
+        local t = tabs[n]
+        return t and t[s]
+    end
+end
+
+--~ local c = set.create{'aap','noot','mies'}
+--~ local s = set.tonumber(c)
+--~ local t = set.totable(s)
+--~ print(t['aap'])
+--~ local c = set.create{'zus','wim','jet'}
+--~ local s = set.tonumber(c)
+--~ local t = set.totable(s)
+--~ print(t['aap'])
+--~ print(t['jet'])
+--~ print(set.contains(t,'jet'))
+--~ print(set.contains(t,'aap'))
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-os'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- maybe build io.flush in os.execute
+
+local find, format, gsub = string.find, string.format, string.gsub
+local random, ceil = math.random, math.ceil
+
+local execute, spawn, exec, ioflush = os.execute, os.spawn or os.execute, os.exec or os.execute, io.flush
+
+function os.execute(...) ioflush() return execute(...) end
+function os.spawn  (...) ioflush() return spawn  (...) end
+function os.exec   (...) ioflush() return exec   (...) end
+
+function os.resultof(command)
+    ioflush() -- else messed up logging
+    local handle = io.popen(command,"r")
+    if not handle then
+    --  print("unknown command '".. command .. "' in os.resultof")
+        return ""
+    else
+        return handle:read("*all") or ""
+    end
+end
+
+--~ os.type     : windows | unix (new, we already guessed os.platform)
+--~ os.name     : windows | msdos | linux | macosx | solaris | .. | generic (new)
+--~ os.platform : extended os.name with architecture
+
+if not io.fileseparator then
+    if find(os.getenv("PATH"),";") then
+        io.fileseparator, io.pathseparator, os.type = "\\", ";", os.type or "mswin"
+    else
+        io.fileseparator, io.pathseparator, os.type = "/" , ":", os.type or "unix"
+    end
+end
+
+os.type = os.type or (io.pathseparator == ";"       and "windows") or "unix"
+os.name = os.name or (os.type          == "windows" and "mswin"  ) or "linux"
+
+if os.type == "windows" then
+    os.libsuffix, os.binsuffix = 'dll', 'exe'
+else
+    os.libsuffix, os.binsuffix = 'so', ''
+end
+
+function os.launch(str)
+    if os.type == "windows" then
+        os.execute("start " .. str) -- os.spawn ?
+    else
+        os.execute(str .. " &")     -- os.spawn ?
+    end
+end
+
+if not os.times then
+    -- utime  = user time
+    -- stime  = system time
+    -- cutime = children user time
+    -- cstime = children system time
+    function os.times()
+        return {
+            utime  = os.gettimeofday(), -- user
+            stime  = 0,                 -- system
+            cutime = 0,                 -- children user
+            cstime = 0,                 -- children system
+        }
+    end
+end
+
+os.gettimeofday = os.gettimeofday or os.clock
+
+local startuptime = os.gettimeofday()
+
+function os.runtime()
+    return os.gettimeofday() - startuptime
+end
+
+--~ print(os.gettimeofday()-os.time())
+--~ os.sleep(1.234)
+--~ print (">>",os.runtime())
+--~ print(os.date("%H:%M:%S",os.gettimeofday()))
+--~ print(os.date("%H:%M:%S",os.time()))
+
+-- no need for function anymore as we have more clever code and helpers now
+-- this metatable trickery might as well disappear
+
+os.resolvers = os.resolvers or { }
+
+local resolvers = os.resolvers
+
+local osmt = getmetatable(os) or { __index = function(t,k) t[k] = "unset" return "unset" end } -- maybe nil
+local osix = osmt.__index
+
+osmt.__index = function(t,k)
+    return (resolvers[k] or osix)(t,k)
+end
+
+setmetatable(os,osmt)
+
+if not os.setenv then
+
+    -- we still store them but they won't be seen in
+    -- child processes although we might pass them some day
+    -- using command concatination
+
+    local env, getenv = { }, os.getenv
+
+    function os.setenv(k,v)
+        env[k] = v
+    end
+
+    function os.getenv(k)
+        return env[k] or getenv(k)
+    end
+
+end
+
+-- we can use HOSTTYPE on some platforms
+
+local name, platform = os.name or "linux", os.getenv("MTX_PLATFORM") or ""
+
+local function guess()
+    local architecture = os.resultof("uname -m") or ""
+    if architecture ~= "" then
+        return architecture
+    end
+    architecture = os.getenv("HOSTTYPE") or ""
+    if architecture ~= "" then
+        return architecture
+    end
+    return os.resultof("echo $HOSTTYPE") or ""
+end
+
+if platform ~= "" then
+
+    os.platform = platform
+
+elseif os.type == "windows" then
+
+    -- we could set the variable directly, no function needed here
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.getenv("PROCESSOR_ARCHITECTURE") or ""
+        if find(architecture,"AMD64") then
+            platform = "mswin-64"
+        else
+            platform = "mswin"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "linux" then
+
+    function os.resolvers.platform(t,k)
+        -- we sometims have HOSTTYPE set so let's check that first
+        local platform, architecture = "", os.getenv("HOSTTYPE") or os.resultof("uname -m") or ""
+        if find(architecture,"x86_64") then
+            platform = "linux-64"
+        elseif find(architecture,"ppc") then
+            platform = "linux-ppc"
+        else
+            platform = "linux"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "macosx" then
+
+    --[[
+        Identifying the architecture of OSX is quite a mess and this
+        is the best we can come up with. For some reason $HOSTTYPE is
+        a kind of pseudo environment variable, not known to the current
+        environment. And yes, uname cannot be trusted either, so there
+        is a change that you end up with a 32 bit run on a 64 bit system.
+        Also, some proper 64 bit intel macs are too cheap (low-end) and
+        therefore not permitted to run the 64 bit kernel.
+      ]]--
+
+    function os.resolvers.platform(t,k)
+     -- local platform, architecture = "", os.getenv("HOSTTYPE") or ""
+     -- if architecture == "" then
+     --     architecture = os.resultof("echo $HOSTTYPE") or ""
+     -- end
+        local platform, architecture = "", os.resultof("echo $HOSTTYPE") or ""
+        if architecture == "" then
+         -- print("\nI have no clue what kind of OSX you're running so let's assume an 32 bit intel.\n")
+            platform = "osx-intel"
+        elseif find(architecture,"i386") then
+            platform = "osx-intel"
+        elseif find(architecture,"x86_64") then
+            platform = "osx-64"
+        else
+            platform = "osx-ppc"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "sunos" then
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.resultof("uname -m") or ""
+        if find(architecture,"sparc") then
+            platform = "solaris-sparc"
+        else -- if architecture == 'i86pc'
+            platform = "solaris-intel"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "freebsd" then
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.resultof("uname -m") or ""
+        if find(architecture,"amd64") then
+            platform = "freebsd-amd64"
+        else
+            platform = "freebsd"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "kfreebsd" then
+
+    function os.resolvers.platform(t,k)
+        -- we sometims have HOSTTYPE set so let's check that first
+        local platform, architecture = "", os.getenv("HOSTTYPE") or os.resultof("uname -m") or ""
+        if find(architecture,"x86_64") then
+            platform = "kfreebsd-64"
+        else
+            platform = "kfreebsd-i386"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+else
+
+    -- platform = "linux"
+    -- os.setenv("MTX_PLATFORM",platform)
+    -- os.platform = platform
+
+    function os.resolvers.platform(t,k)
+        local platform = "linux"
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+end
+
+-- beware, we set the randomseed
+
+-- from wikipedia: Version 4 UUIDs use a scheme relying only on random numbers. This algorithm sets the
+-- version number as well as two reserved bits. All other bits are set using a random or pseudorandom
+-- data source. Version 4 UUIDs have the form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx with hexadecimal
+-- digits x and hexadecimal digits 8, 9, A, or B for y. e.g. f47ac10b-58cc-4372-a567-0e02b2c3d479.
+--
+-- as we don't call this function too often there is not so much risk on repetition
+
+local t = { 8, 9, "a", "b" }
+
+function os.uuid()
+    return format("%04x%04x-4%03x-%s%03x-%04x-%04x%04x%04x",
+        random(0xFFFF),random(0xFFFF),
+        random(0x0FFF),
+        t[ceil(random(4))] or 8,random(0x0FFF),
+        random(0xFFFF),
+        random(0xFFFF),random(0xFFFF),random(0xFFFF)
+    )
+end
+
+local d
+
+function os.timezone(delta)
+    d = d or tonumber(tonumber(os.date("%H")-os.date("!%H")))
+    if delta then
+        if d > 0 then
+            return format("+%02i:00",d)
+        else
+            return format("-%02i:00",-d)
+        end
+    else
+        return 1
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-file'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- needs a cleanup
+
+file = file or { }
+
+local concat = table.concat
+local find, gmatch, match, gsub, sub, char = string.find, string.gmatch, string.match, string.gsub, string.sub, string.char
+local lpegmatch = lpeg.match
+
+function file.removesuffix(filename)
+    return (gsub(filename,"%.[%a%d]+$",""))
+end
+
+function file.addsuffix(filename, suffix)
+    if not suffix or suffix == "" then
+        return filename
+    elseif not find(filename,"%.[%a%d]+$") then
+        return filename .. "." .. suffix
+    else
+        return filename
+    end
+end
+
+function file.replacesuffix(filename, suffix)
+    return (gsub(filename,"%.[%a%d]+$","")) .. "." .. suffix
+end
+
+function file.dirname(name,default)
+    return match(name,"^(.+)[/\\].-$") or (default or "")
+end
+
+function file.basename(name)
+    return match(name,"^.+[/\\](.-)$") or name
+end
+
+function file.nameonly(name)
+    return (gsub(match(name,"^.+[/\\](.-)$") or name,"%..*$",""))
+end
+
+function file.extname(name,default)
+    return match(name,"^.+%.([^/\\]-)$") or default or ""
+end
+
+file.suffix = file.extname
+
+--~ function file.join(...)
+--~     local pth = concat({...},"/")
+--~     pth = gsub(pth,"\\","/")
+--~     local a, b = match(pth,"^(.*://)(.*)$")
+--~     if a and b then
+--~         return a .. gsub(b,"//+","/")
+--~     end
+--~     a, b = match(pth,"^(//)(.*)$")
+--~     if a and b then
+--~         return a .. gsub(b,"//+","/")
+--~     end
+--~     return (gsub(pth,"//+","/"))
+--~ end
+
+local trick_1 = char(1)
+local trick_2 = "^" .. trick_1 .. "/+"
+
+function file.join(...)
+    local lst = { ... }
+    local a, b = lst[1], lst[2]
+    if a == "" then
+        lst[1] = trick_1
+    elseif b and find(a,"^/+$") and find(b,"^/") then
+        lst[1] = ""
+        lst[2] = gsub(b,"^/+","")
+    end
+    local pth = concat(lst,"/")
+    pth = gsub(pth,"\\","/")
+    local a, b = match(pth,"^(.*://)(.*)$")
+    if a and b then
+        return a .. gsub(b,"//+","/")
+    end
+    a, b = match(pth,"^(//)(.*)$")
+    if a and b then
+        return a .. gsub(b,"//+","/")
+    end
+    pth = gsub(pth,trick_2,"")
+    return (gsub(pth,"//+","/"))
+end
+
+--~ print(file.join("//","/y"))
+--~ print(file.join("/","/y"))
+--~ print(file.join("","/y"))
+--~ print(file.join("/x/","/y"))
+--~ print(file.join("x/","/y"))
+--~ print(file.join("http://","/y"))
+--~ print(file.join("http://a","/y"))
+--~ print(file.join("http:///a","/y"))
+--~ print(file.join("//nas-1","/y"))
+
+function file.iswritable(name)
+    local a = lfs.attributes(name) or lfs.attributes(file.dirname(name,"."))
+    return a and sub(a.permissions,2,2) == "w"
+end
+
+function file.isreadable(name)
+    local a = lfs.attributes(name)
+    return a and sub(a.permissions,1,1) == "r"
+end
+
+file.is_readable = file.isreadable
+file.is_writable = file.iswritable
+
+-- todo: lpeg
+
+--~ function file.split_path(str)
+--~     local t = { }
+--~     str = gsub(str,"\\", "/")
+--~     str = gsub(str,"(%a):([;/])", "%1\001%2")
+--~     for name in gmatch(str,"([^;:]+)") do
+--~         if name ~= "" then
+--~             t[#t+1] = gsub(name,"\001",":")
+--~         end
+--~     end
+--~     return t
+--~ end
+
+local checkedsplit = string.checkedsplit
+
+function file.split_path(str,separator)
+    str = gsub(str,"\\","/")
+    return checkedsplit(str,separator or io.pathseparator)
+end
+
+function file.join_path(tab)
+    return concat(tab,io.pathseparator) -- can have trailing //
+end
+
+-- we can hash them weakly
+
+function file.collapse_path(str)
+    str = gsub(str,"\\","/")
+    if find(str,"/") then
+        str = gsub(str,"^%./",(gsub(lfs.currentdir(),"\\","/")) .. "/") -- ./xx in qualified
+        str = gsub(str,"/%./","/")
+        local n, m = 1, 1
+        while n > 0 or m > 0 do
+            str, n = gsub(str,"[^/%.]+/%.%.$","")
+            str, m = gsub(str,"[^/%.]+/%.%./","")
+        end
+        str = gsub(str,"([^/])/$","%1")
+    --  str = gsub(str,"^%./","") -- ./xx in qualified
+        str = gsub(str,"/%.$","")
+    end
+    if str == "" then str = "." end
+    return str
+end
+
+--~ print(file.collapse_path("/a"))
+--~ print(file.collapse_path("a/./b/.."))
+--~ print(file.collapse_path("a/aa/../b/bb"))
+--~ print(file.collapse_path("a/../.."))
+--~ print(file.collapse_path("a/.././././b/.."))
+--~ print(file.collapse_path("a/./././b/.."))
+--~ print(file.collapse_path("a/b/c/../.."))
+
+function file.robustname(str)
+    return (gsub(str,"[^%a%d%/%-%.\\]+","-"))
+end
+
+file.readdata = io.loaddata
+file.savedata = io.savedata
+
+function file.copy(oldname,newname)
+    file.savedata(newname,io.loaddata(oldname))
+end
+
+-- lpeg variants, slightly faster, not always
+
+--~ local period    = lpeg.P(".")
+--~ local slashes   = lpeg.S("\\/")
+--~ local noperiod  = 1-period
+--~ local noslashes = 1-slashes
+--~ local name      = noperiod^1
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.C(noperiod^1) * -1
+
+--~ function file.extname(name)
+--~     return lpegmatch(pattern,name) or ""
+--~ end
+
+--~ local pattern = lpeg.Cs(((period * noperiod^1 * -1)/"" + 1)^1)
+
+--~ function file.removesuffix(name)
+--~     return lpegmatch(pattern,name)
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^1 * lpeg.C(noslashes^1) * -1
+
+--~ function file.basename(name)
+--~     return lpegmatch(pattern,name) or name
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^1 * lpeg.Cp() * noslashes^1 * -1
+
+--~ function file.dirname(name)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return sub(name,1,p-2)
+--~     else
+--~         return ""
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.Cp() * noperiod^1 * -1
+
+--~ function file.addsuffix(name, suffix)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return name
+--~     else
+--~         return name .. "." .. suffix
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.Cp() * noperiod^1 * -1
+
+--~ function file.replacesuffix(name,suffix)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return sub(name,1,p-2) .. "." .. suffix
+--~     else
+--~         return name .. "." .. suffix
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * lpeg.Cp() * ((noperiod^1 * period)^1 * lpeg.Cp() + lpeg.P(true)) * noperiod^1 * -1
+
+--~ function file.nameonly(name)
+--~     local a, b = lpegmatch(pattern,name)
+--~     if b then
+--~         return sub(name,a,b-2)
+--~     elseif a then
+--~         return sub(name,a)
+--~     else
+--~         return name
+--~     end
+--~ end
+
+--~ local test = file.extname
+--~ local test = file.basename
+--~ local test = file.dirname
+--~ local test = file.addsuffix
+--~ local test = file.replacesuffix
+--~ local test = file.nameonly
+
+--~ print(1,test("./a/b/c/abd.def.xxx","!!!"))
+--~ print(2,test("./../b/c/abd.def.xxx","!!!"))
+--~ print(3,test("a/b/c/abd.def.xxx","!!!"))
+--~ print(4,test("a/b/c/def.xxx","!!!"))
+--~ print(5,test("a/b/c/def","!!!"))
+--~ print(6,test("def","!!!"))
+--~ print(7,test("def.xxx","!!!"))
+
+--~ local tim = os.clock() for i=1,250000 do local ext = test("abd.def.xxx","!!!") end print(os.clock()-tim)
+
+-- also rewrite previous
+
+local letter    = lpeg.R("az","AZ") + lpeg.S("_-+")
+local separator = lpeg.P("://")
+
+local qualified = lpeg.P(".")^0 * lpeg.P("/") + letter*lpeg.P(":") + letter^1*separator + letter^1 * lpeg.P("/")
+local rootbased = lpeg.P("/") + letter*lpeg.P(":")
+
+-- ./name ../name  /name c: :// name/name
+
+function file.is_qualified_path(filename)
+    return lpegmatch(qualified,filename) ~= nil
+end
+
+function file.is_rootbased_path(filename)
+    return lpegmatch(rootbased,filename) ~= nil
+end
+
+local slash  = lpeg.S("\\/")
+local period = lpeg.P(".")
+local drive  = lpeg.C(lpeg.R("az","AZ")) * lpeg.P(":")
+local path   = lpeg.C(((1-slash)^0 * slash)^0)
+local suffix = period * lpeg.C(lpeg.P(1-period)^0 * lpeg.P(-1))
+local base   = lpeg.C((1-suffix)^0)
+
+local pattern = (drive + lpeg.Cc("")) * (path + lpeg.Cc("")) * (base + lpeg.Cc("")) * (suffix + lpeg.Cc(""))
+
+function file.splitname(str) -- returns drive, path, base, suffix
+    return lpegmatch(pattern,str)
+end
+
+-- function test(t) for k, v in next, t do print(v, "=>", file.splitname(v)) end end
+--
+-- test { "c:", "c:/aa", "c:/aa/bb", "c:/aa/bb/cc", "c:/aa/bb/cc.dd", "c:/aa/bb/cc.dd.ee" }
+-- test { "c:", "c:aa", "c:aa/bb", "c:aa/bb/cc", "c:aa/bb/cc.dd", "c:aa/bb/cc.dd.ee" }
+-- test { "/aa", "/aa/bb", "/aa/bb/cc", "/aa/bb/cc.dd", "/aa/bb/cc.dd.ee" }
+-- test { "aa", "aa/bb", "aa/bb/cc", "aa/bb/cc.dd", "aa/bb/cc.dd.ee" }
+
+--~ -- todo:
+--~
+--~ if os.type == "windows" then
+--~     local currentdir = lfs.currentdir
+--~     function lfs.currentdir()
+--~         return (gsub(currentdir(),"\\","/"))
+--~     end
+--~ end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-md5'] = {
+    version   = 1.001,
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- This also provides file checksums and checkers.
+
+local gsub, format, byte = string.gsub, string.format, string.byte
+
+local function convert(str,fmt)
+    return (gsub(md5.sum(str),".",function(chr) return format(fmt,byte(chr)) end))
+end
+
+if not md5.HEX then function md5.HEX(str) return convert(str,"%02X") end end
+if not md5.hex then function md5.hex(str) return convert(str,"%02x") end end
+if not md5.dec then function md5.dec(str) return convert(str,"%03i") end end
+
+--~ if not md5.HEX then
+--~     local function remap(chr) return format("%02X",byte(chr)) end
+--~     function md5.HEX(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+--~ if not md5.hex then
+--~     local function remap(chr) return format("%02x",byte(chr)) end
+--~     function md5.hex(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+--~ if not md5.dec then
+--~     local function remap(chr) return format("%03i",byte(chr)) end
+--~     function md5.dec(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+
+file.needs_updating_threshold = 1
+
+function file.needs_updating(oldname,newname) -- size modification access change
+    local oldtime = lfs.attributes(oldname, modification)
+    local newtime = lfs.attributes(newname, modification)
+    if newtime >= oldtime then
+        return false
+    elseif oldtime - newtime < file.needs_updating_threshold then
+        return false
+    else
+        return true
+    end
+end
+
+function file.checksum(name)
+    if md5 then
+        local data = io.loaddata(name)
+        if data then
+            return md5.HEX(data)
+        end
+    end
+    return nil
+end
+
+function file.loadchecksum(name)
+    if md5 then
+        local data = io.loaddata(name .. ".md5")
+        return data and (gsub(data,"%s",""))
+    end
+    return nil
+end
+
+function file.savechecksum(name, checksum)
+    if not checksum then checksum = file.checksum(name) end
+    if checksum then
+        io.savedata(name .. ".md5",checksum)
+        return checksum
+    end
+    return nil
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-url'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local char, gmatch, gsub = string.char, string.gmatch, string.gsub
+local tonumber, type = tonumber, type
+local lpegmatch = lpeg.match
+
+-- from the spec (on the web):
+--
+--     foo://example.com:8042/over/there?name=ferret#nose
+--     \_/   \______________/\_________/ \_________/ \__/
+--      |           |            |            |        |
+--   scheme     authority       path        query   fragment
+--      |   _____________________|__
+--     / \ /                        \
+--     urn:example:animal:ferret:nose
+
+url = url or { }
+
+local function tochar(s)
+    return char(tonumber(s,16))
+end
+
+local colon, qmark, hash, slash, percent, endofstring = lpeg.P(":"), lpeg.P("?"), lpeg.P("#"), lpeg.P("/"), lpeg.P("%"), lpeg.P(-1)
+
+local hexdigit  = lpeg.R("09","AF","af")
+local plus      = lpeg.P("+")
+local escaped   = (plus / " ") + (percent * lpeg.C(hexdigit * hexdigit) / tochar)
+
+-- we assume schemes with more than 1 character (in order to avoid problems with windows disks)
+
+local scheme    =                 lpeg.Cs((escaped+(1-colon-slash-qmark-hash))^2) * colon + lpeg.Cc("")
+local authority = slash * slash * lpeg.Cs((escaped+(1-      slash-qmark-hash))^0)         + lpeg.Cc("")
+local path      = slash *         lpeg.Cs((escaped+(1-            qmark-hash))^0)         + lpeg.Cc("")
+local query     = qmark         * lpeg.Cs((escaped+(1-                  hash))^0)         + lpeg.Cc("")
+local fragment  = hash          * lpeg.Cs((escaped+(1-           endofstring))^0)         + lpeg.Cc("")
+
+local parser = lpeg.Ct(scheme * authority * path * query * fragment)
+
+-- todo: reconsider Ct as we can as well have five return values (saves a table)
+-- so we can have two parsers, one with and one without
+
+function url.split(str)
+    return (type(str) == "string" and lpegmatch(parser,str)) or str
+end
+
+-- todo: cache them
+
+function url.hashed(str)
+    local s = url.split(str)
+    local somescheme = s[1] ~= ""
+    return {
+        scheme    = (somescheme and s[1]) or "file",
+        authority = s[2],
+        path      = s[3],
+        query     = s[4],
+        fragment  = s[5],
+        original  = str,
+        noscheme  = not somescheme,
+    }
+end
+
+function url.hasscheme(str)
+    return url.split(str)[1] ~= ""
+end
+
+function url.addscheme(str,scheme)
+    return (url.hasscheme(str) and str) or ((scheme or "file:///") .. str)
+end
+
+function url.construct(hash)
+    local fullurl = hash.sheme .. "://".. hash.authority .. hash.path
+    if hash.query then
+        fullurl = fullurl .. "?".. hash.query
+    end
+    if hash.fragment then
+        fullurl = fullurl .. "?".. hash.fragment
+    end
+    return fullurl
+end
+
+function url.filename(filename)
+    local t = url.hashed(filename)
+    return (t.scheme == "file" and (gsub(t.path,"^/([a-zA-Z])([:|])/)","%1:"))) or filename
+end
+
+function url.query(str)
+    if type(str) == "string" then
+        local t = { }
+        for k, v in gmatch(str,"([^&=]*)=([^&=]*)") do
+            t[k] = v
+        end
+        return t
+    else
+        return str
+    end
+end
+
+--~ print(url.filename("file:///c:/oeps.txt"))
+--~ print(url.filename("c:/oeps.txt"))
+--~ print(url.filename("file:///oeps.txt"))
+--~ print(url.filename("file:///etc/test.txt"))
+--~ print(url.filename("/oeps.txt"))
+
+--~ from the spec on the web (sort of):
+--~
+--~ function test(str)
+--~     print(table.serialize(url.hashed(str)))
+--~ end
+--~
+--~ test("%56pass%20words")
+--~ test("file:///c:/oeps.txt")
+--~ test("file:///c|/oeps.txt")
+--~ test("file:///etc/oeps.txt")
+--~ test("file://./etc/oeps.txt")
+--~ test("file:////etc/oeps.txt")
+--~ test("ftp://ftp.is.co.za/rfc/rfc1808.txt")
+--~ test("http://www.ietf.org/rfc/rfc2396.txt")
+--~ test("ldap://[2001:db8::7]/c=GB?objectClass?one#what")
+--~ test("mailto:John.Doe@example.com")
+--~ test("news:comp.infosystems.www.servers.unix")
+--~ test("tel:+1-816-555-1212")
+--~ test("telnet://192.0.2.16:80/")
+--~ test("urn:oasis:names:specification:docbook:dtd:xml:4.1.2")
+--~ test("/etc/passwords")
+--~ test("http://www.pragma-ade.com/spaced%20name")
+
+--~ test("zip:///oeps/oeps.zip#bla/bla.tex")
+--~ test("zip:///oeps/oeps.zip?bla/bla.tex")
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-dir'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- dir.expand_name will be merged with cleanpath and collapsepath
+
+local type = type
+local find, gmatch, match, gsub = string.find, string.gmatch, string.match, string.gsub
+local lpegmatch = lpeg.match
+
+dir = dir or { }
+
+-- handy
+
+function dir.current()
+    return (gsub(lfs.currentdir(),"\\","/"))
+end
+
+-- optimizing for no string.find (*) does not save time
+
+local attributes = lfs.attributes
+local walkdir    = lfs.dir
+
+local function glob_pattern(path,patt,recurse,action)
+    local ok, scanner
+    if path == "/" then
+        ok, scanner = xpcall(function() return walkdir(path..".") end, function() end) -- kepler safe
+    else
+        ok, scanner = xpcall(function() return walkdir(path)      end, function() end) -- kepler safe
+    end
+    if ok and type(scanner) == "function" then
+        if not find(path,"/$") then path = path .. '/' end
+        for name in scanner do
+            local full = path .. name
+            local mode = attributes(full,'mode')
+            if mode == 'file' then
+                if find(full,patt) then
+                    action(full)
+                end
+            elseif recurse and (mode == "directory") and (name ~= '.') and (name ~= "..") then
+                glob_pattern(full,patt,recurse,action)
+            end
+        end
+    end
+end
+
+dir.glob_pattern = glob_pattern
+
+local function collect_pattern(path,patt,recurse,result)
+    local ok, scanner
+    result = result or { }
+    if path == "/" then
+        ok, scanner = xpcall(function() return walkdir(path..".") end, function() end) -- kepler safe
+    else
+        ok, scanner = xpcall(function() return walkdir(path)      end, function() end) -- kepler safe
+    end
+    if ok and type(scanner) == "function" then
+        if not find(path,"/$") then path = path .. '/' end
+        for name in scanner do
+            local full = path .. name
+            local attr = attributes(full)
+            local mode = attr.mode
+            if mode == 'file' then
+                if find(full,patt) then
+                    result[name] = attr
+                end
+            elseif recurse and (mode == "directory") and (name ~= '.') and (name ~= "..") then
+                attr.list = collect_pattern(full,patt,recurse)
+                result[name] = attr
+            end
+        end
+    end
+    return result
+end
+
+dir.collect_pattern = collect_pattern
+
+local P, S, R, C, Cc, Cs, Ct, Cv, V = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.Cc, lpeg.Cs, lpeg.Ct, lpeg.Cv, lpeg.V
+
+local pattern = Ct {
+    [1] = (C(P(".") + P("/")^1) + C(R("az","AZ") * P(":") * P("/")^0) + Cc("./")) * V(2) * V(3),
+    [2] = C(((1-S("*?/"))^0 * P("/"))^0),
+    [3] = C(P(1)^0)
+}
+
+local filter = Cs ( (
+    P("**") / ".*" +
+    P("*")  / "[^/]*" +
+    P("?")  / "[^/]" +
+    P(".")  / "%%." +
+    P("+")  / "%%+" +
+    P("-")  / "%%-" +
+    P(1)
+)^0 )
+
+local function glob(str,t)
+    if type(t) == "function" then
+        if type(str) == "table" then
+            for s=1,#str do
+                glob(str[s],t)
+            end
+        elseif lfs.isfile(str) then
+            t(str)
+        else
+            local split = lpegmatch(pattern,str)
+            if split then
+                local root, path, base = split[1], split[2], split[3]
+                local recurse = find(base,"%*%*")
+                local start = root .. path
+                local result = lpegmatch(filter,start .. base)
+                glob_pattern(start,result,recurse,t)
+            end
+        end
+    else
+        if type(str) == "table" then
+            local t = t or { }
+            for s=1,#str do
+                glob(str[s],t)
+            end
+            return t
+        elseif lfs.isfile(str) then
+            local t = t or { }
+            t[#t+1] = str
+            return t
+        else
+            local split = lpegmatch(pattern,str)
+            if split then
+                local t = t or { }
+                local action = action or function(name) t[#t+1] = name end
+                local root, path, base = split[1], split[2], split[3]
+                local recurse = find(base,"%*%*")
+                local start = root .. path
+                local result = lpegmatch(filter,start .. base)
+                glob_pattern(start,result,recurse,action)
+                return t
+            else
+                return { }
+            end
+        end
+    end
+end
+
+dir.glob = glob
+
+--~ list = dir.glob("**/*.tif")
+--~ list = dir.glob("/**/*.tif")
+--~ list = dir.glob("./**/*.tif")
+--~ list = dir.glob("oeps/**/*.tif")
+--~ list = dir.glob("/oeps/**/*.tif")
+
+local function globfiles(path,recurse,func,files) -- func == pattern or function
+    if type(func) == "string" then
+        local s = func -- alas, we need this indirect way
+        func = function(name) return find(name,s) end
+    end
+    files = files or { }
+    for name in walkdir(path) do
+        if find(name,"^%.") then
+            --- skip
+        else
+            local mode = attributes(name,'mode')
+            if mode == "directory" then
+                if recurse then
+                    globfiles(path .. "/" .. name,recurse,func,files)
+                end
+            elseif mode == "file" then
+                if func then
+                    if func(name) then
+                        files[#files+1] = path .. "/" .. name
+                    end
+                else
+                    files[#files+1] = path .. "/" .. name
+                end
+            end
+        end
+    end
+    return files
+end
+
+dir.globfiles = globfiles
+
+-- t = dir.glob("c:/data/develop/context/sources/**/????-*.tex")
+-- t = dir.glob("c:/data/develop/tex/texmf/**/*.tex")
+-- t = dir.glob("c:/data/develop/context/texmf/**/*.tex")
+-- t = dir.glob("f:/minimal/tex/**/*")
+-- print(dir.ls("f:/minimal/tex/**/*"))
+-- print(dir.ls("*.tex"))
+
+function dir.ls(pattern)
+    return table.concat(glob(pattern),"\n")
+end
+
+--~ mkdirs("temp")
+--~ mkdirs("a/b/c")
+--~ mkdirs(".","/a/b/c")
+--~ mkdirs("a","b","c")
+
+local make_indeed = true -- false
+
+if string.find(os.getenv("PATH"),";") then -- os.type == "windows"
+
+    function dir.mkdirs(...)
+        local str, pth, t = "", "", { ... }
+        for i=1,#t do
+            local s = t[i]
+            if s ~= "" then
+                if str ~= "" then
+                    str = str .. "/" .. s
+                else
+                    str = s
+                end
+            end
+        end
+        local first, middle, last
+        local drive = false
+        first, middle, last = match(str,"^(//)(//*)(.*)$")
+        if first then
+            -- empty network path == local path
+        else
+            first, last = match(str,"^(//)/*(.-)$")
+            if first then
+                middle, last = match(str,"([^/]+)/+(.-)$")
+                if middle then
+                    pth = "//" .. middle
+                else
+                    pth = "//" .. last
+                    last = ""
+                end
+            else
+                first, middle, last = match(str,"^([a-zA-Z]:)(/*)(.-)$")
+                if first then
+                    pth, drive = first .. middle, true
+                else
+                    middle, last = match(str,"^(/*)(.-)$")
+                    if not middle then
+                        last = str
+                    end
+                end
+            end
+        end
+        for s in gmatch(last,"[^/]+") do
+            if pth == "" then
+                pth = s
+            elseif drive then
+                pth, drive = pth .. s, false
+            else
+                pth = pth .. "/" .. s
+            end
+            if make_indeed and not lfs.isdir(pth) then
+                lfs.mkdir(pth)
+            end
+        end
+        return pth, (lfs.isdir(pth) == true)
+    end
+
+--~         print(dir.mkdirs("","","a","c"))
+--~         print(dir.mkdirs("a"))
+--~         print(dir.mkdirs("a:"))
+--~         print(dir.mkdirs("a:/b/c"))
+--~         print(dir.mkdirs("a:b/c"))
+--~         print(dir.mkdirs("a:/bbb/c"))
+--~         print(dir.mkdirs("/a/b/c"))
+--~         print(dir.mkdirs("/aaa/b/c"))
+--~         print(dir.mkdirs("//a/b/c"))
+--~         print(dir.mkdirs("///a/b/c"))
+--~         print(dir.mkdirs("a/bbb//ccc/"))
+
+    function dir.expand_name(str) -- will be merged with cleanpath and collapsepath
+        local first, nothing, last = match(str,"^(//)(//*)(.*)$")
+        if first then
+            first = dir.current() .. "/"
+        end
+        if not first then
+            first, last = match(str,"^(//)/*(.*)$")
+        end
+        if not first then
+            first, last = match(str,"^([a-zA-Z]:)(.*)$")
+            if first and not find(last,"^/") then
+                local d = lfs.currentdir()
+                if lfs.chdir(first) then
+                    first = dir.current()
+                end
+                lfs.chdir(d)
+            end
+        end
+        if not first then
+            first, last = dir.current(), str
+        end
+        last = gsub(last,"//","/")
+        last = gsub(last,"/%./","/")
+        last = gsub(last,"^/*","")
+        first = gsub(first,"/*$","")
+        if last == "" then
+            return first
+        else
+            return first .. "/" .. last
+        end
+    end
+
+else
+
+    function dir.mkdirs(...)
+        local str, pth, t = "", "", { ... }
+        for i=1,#t do
+            local s = t[i]
+            if s ~= "" then
+                if str ~= "" then
+                    str = str .. "/" .. s
+                else
+                    str = s
+                end
+            end
+        end
+        str = gsub(str,"/+","/")
+        if find(str,"^/") then
+            pth = "/"
+            for s in gmatch(str,"[^/]+") do
+                local first = (pth == "/")
+                if first then
+                    pth = pth .. s
+                else
+                    pth = pth .. "/" .. s
+                end
+                if make_indeed and not first and not lfs.isdir(pth) then
+                    lfs.mkdir(pth)
+                end
+            end
+        else
+            pth = "."
+            for s in gmatch(str,"[^/]+") do
+                pth = pth .. "/" .. s
+                if make_indeed and not lfs.isdir(pth) then
+                    lfs.mkdir(pth)
+                end
+            end
+        end
+        return pth, (lfs.isdir(pth) == true)
+    end
+
+--~         print(dir.mkdirs("","","a","c"))
+--~         print(dir.mkdirs("a"))
+--~         print(dir.mkdirs("/a/b/c"))
+--~         print(dir.mkdirs("/aaa/b/c"))
+--~         print(dir.mkdirs("//a/b/c"))
+--~         print(dir.mkdirs("///a/b/c"))
+--~         print(dir.mkdirs("a/bbb//ccc/"))
+
+    function dir.expand_name(str) -- will be merged with cleanpath and collapsepath
+        if not find(str,"^/") then
+            str = lfs.currentdir() .. "/" .. str
+        end
+        str = gsub(str,"//","/")
+        str = gsub(str,"/%./","/")
+        return str
+    end
+
+end
+
+dir.makedirs = dir.mkdirs
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-boolean'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+boolean = boolean or { }
+
+local type, tonumber = type, tonumber
+
+function boolean.tonumber(b)
+    if b then return 1 else return 0 end
+end
+
+function toboolean(str,tolerant)
+    if tolerant then
+        local tstr = type(str)
+        if tstr == "string" then
+            return str == "true" or str == "yes" or str == "on" or str == "1" or str == "t"
+        elseif tstr == "number" then
+            return tonumber(str) ~= 0
+        elseif tstr == "nil" then
+            return false
+        else
+            return str
+        end
+    elseif str == "true" then
+        return true
+    elseif str == "false" then
+        return false
+    else
+        return str
+    end
+end
+
+function string.is_boolean(str)
+    if type(str) == "string" then
+        if str == "true" or str == "yes" or str == "on" or str == "t" then
+            return true
+        elseif str == "false" or str == "no" or str == "off" or str == "f" then
+            return false
+        end
+    end
+    return nil
+end
+
+function boolean.alwaystrue()
+    return true
+end
+
+function boolean.falsetrue()
+    return false
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-math'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local floor, sin, cos, tan = math.floor, math.sin, math.cos, math.tan
+
+if not math.round then
+    function math.round(x)
+        return floor(x + 0.5)
+    end
+end
+
+if not math.div then
+    function math.div(n,m)
+        return floor(n/m)
+    end
+end
+
+if not math.mod then
+    function math.mod(n,m)
+        return n % m
+    end
+end
+
+local pipi = 2*math.pi/360
+
+function math.sind(d)
+    return sin(d*pipi)
+end
+
+function math.cosd(d)
+    return cos(d*pipi)
+end
+
+function math.tand(d)
+    return tan(d*pipi)
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-utils'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- hm, quite unreadable
+
+local gsub = string.gsub
+local concat = table.concat
+local type, next = type, next
+
+if not utils        then utils        = { } end
+if not utils.merger then utils.merger = { } end
+if not utils.lua    then utils.lua    = { } end
+
+utils.merger.m_begin = "begin library merge"
+utils.merger.m_end   = "end library merge"
+utils.merger.pattern =
+    "%c+" ..
+    "%-%-%s+" .. utils.merger.m_begin ..
+    "%c+(.-)%c+" ..
+    "%-%-%s+" .. utils.merger.m_end ..
+    "%c+"
+
+function utils.merger._self_fake_()
+    return
+        "-- " .. "created merged file" .. "\n\n" ..
+        "-- " .. utils.merger.m_begin  .. "\n\n" ..
+        "-- " .. utils.merger.m_end    .. "\n\n"
+end
+
+function utils.report(...)
+    print(...)
+end
+
+utils.merger.strip_comment = true
+
+function utils.merger._self_load_(name)
+    local f, data = io.open(name), ""
+    if f then
+        utils.report("reading merge from %s",name)
+        data = f:read("*all")
+        f:close()
+    else
+        utils.report("unknown file to merge %s",name)
+    end
+    if data and utils.merger.strip_comment then
+        -- saves some 20K
+        data = gsub(data,"%-%-~[^\n\r]*[\r\n]", "")
+    end
+    return data or ""
+end
+
+function utils.merger._self_save_(name, data)
+    if data ~= "" then
+        local f = io.open(name,'w')
+        if f then
+            utils.report("saving merge from %s",name)
+            f:write(data)
+            f:close()
+        end
+    end
+end
+
+function utils.merger._self_swap_(data,code)
+    if data ~= "" then
+        return (gsub(data,utils.merger.pattern, function(s)
+            return "\n\n" .. "-- "..utils.merger.m_begin .. "\n" .. code .. "\n" .. "-- "..utils.merger.m_end .. "\n\n"
+        end, 1))
+    else
+        return ""
+    end
+end
+
+--~ stripper:
+--~
+--~ data = gsub(data,"%-%-~[^\n]*\n","")
+--~ data = gsub(data,"\n\n+","\n")
+
+function utils.merger._self_libs_(libs,list)
+    local result, f, frozen = { }, nil, false
+    result[#result+1] = "\n"
+    if type(libs) == 'string' then libs = { libs } end
+    if type(list) == 'string' then list = { list } end
+    local foundpath = nil
+    for i=1,#libs do
+        local lib = libs[i]
+        for j=1,#list do
+            local pth = gsub(list[j],"\\","/") -- file.clean_path
+            utils.report("checking library path %s",pth)
+            local name = pth .. "/" .. lib
+            if lfs.isfile(name) then
+                foundpath = pth
+            end
+        end
+        if foundpath then break end
+    end
+    if foundpath then
+        utils.report("using library path %s",foundpath)
+        local right, wrong = { }, { }
+        for i=1,#libs do
+            local lib = libs[i]
+            local fullname = foundpath .. "/" .. lib
+            if lfs.isfile(fullname) then
+            --  right[#right+1] = lib
+                utils.report("merging library %s",fullname)
+                result[#result+1] = "do -- create closure to overcome 200 locals limit"
+                result[#result+1] = io.loaddata(fullname,true)
+                result[#result+1] = "end -- of closure"
+            else
+            --  wrong[#wrong+1] = lib
+                utils.report("no library %s",fullname)
+            end
+        end
+        if #right > 0 then
+            utils.report("merged libraries: %s",concat(right," "))
+        end
+        if #wrong > 0 then
+            utils.report("skipped libraries: %s",concat(wrong," "))
+        end
+    else
+        utils.report("no valid library path found")
+    end
+    return concat(result, "\n\n")
+end
+
+function utils.merger.selfcreate(libs,list,target)
+    if target then
+        utils.merger._self_save_(
+            target,
+            utils.merger._self_swap_(
+                utils.merger._self_fake_(),
+                utils.merger._self_libs_(libs,list)
+            )
+        )
+    end
+end
+
+function utils.merger.selfmerge(name,libs,list,target)
+    utils.merger._self_save_(
+        target or name,
+        utils.merger._self_swap_(
+            utils.merger._self_load_(name),
+            utils.merger._self_libs_(libs,list)
+        )
+    )
+end
+
+function utils.merger.selfclean(name)
+    utils.merger._self_save_(
+        name,
+        utils.merger._self_swap_(
+            utils.merger._self_load_(name),
+            ""
+        )
+    )
+end
+
+function utils.lua.compile(luafile, lucfile, cleanup, strip) -- defaults: cleanup=false strip=true
+ -- utils.report("compiling",luafile,"into",lucfile)
+    os.remove(lucfile)
+    local command = "-o " .. string.quote(lucfile) .. " " .. string.quote(luafile)
+    if strip ~= false then
+        command = "-s " .. command
+    end
+    local done = (os.spawn("texluac " .. command) == 0) or (os.spawn("luac " .. command) == 0)
+    if done and cleanup == true and lfs.isfile(lucfile) and lfs.isfile(luafile) then
+     -- utils.report("removing",luafile)
+        os.remove(luafile)
+    end
+    return done
+end
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-aux'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- for inline, no store split : for s in string.gmatch(str,",* *([^,]+)") do .. end
+
+aux = aux or { }
+
+local concat, format, gmatch = table.concat, string.format, string.gmatch
+local tostring, type = tostring, type
+local lpegmatch = lpeg.match
+
+local P, R, V = lpeg.P, lpeg.R, lpeg.V
+
+local escape, left, right = P("\\"), P('{'), P('}')
+
+lpeg.patterns.balanced = P {
+    [1] = ((escape * (left+right)) + (1 - (left+right)) + V(2))^0,
+    [2] = left * V(1) * right
+}
+
+local space     = lpeg.P(' ')
+local equal     = lpeg.P("=")
+local comma     = lpeg.P(",")
+local lbrace    = lpeg.P("{")
+local rbrace    = lpeg.P("}")
+local nobrace   = 1 - (lbrace+rbrace)
+local nested    = lpeg.P { lbrace * (nobrace + lpeg.V(1))^0 * rbrace }
+local spaces    = space^0
+
+local value     = lpeg.P(lbrace * lpeg.C((nobrace + nested)^0) * rbrace) + lpeg.C((nested + (1-comma))^0)
+
+local key       = lpeg.C((1-equal-comma)^1)
+local pattern_a = (space+comma)^0 * (key * equal * value + key * lpeg.C(""))
+local pattern_c = (space+comma)^0 * (key * equal * value)
+
+local key       = lpeg.C((1-space-equal-comma)^1)
+local pattern_b = spaces * comma^0 * spaces * (key * ((spaces * equal * spaces * value) + lpeg.C("")))
+
+-- "a=1, b=2, c=3, d={a{b,c}d}, e=12345, f=xx{a{b,c}d}xx, g={}" : outer {} removes, leading spaces ignored
+
+local hash = { }
+
+local function set(key,value) -- using Carg is slower here
+    hash[key] = value
+end
+
+local pattern_a_s = (pattern_a/set)^1
+local pattern_b_s = (pattern_b/set)^1
+local pattern_c_s = (pattern_c/set)^1
+
+aux.settings_to_hash_pattern_a = pattern_a_s
+aux.settings_to_hash_pattern_b = pattern_b_s
+aux.settings_to_hash_pattern_c = pattern_c_s
+
+function aux.make_settings_to_hash_pattern(set,how)
+    if how == "strict" then
+        return (pattern_c/set)^1
+    elseif how == "tolerant" then
+        return (pattern_b/set)^1
+    else
+        return (pattern_a/set)^1
+    end
+end
+
+function aux.settings_to_hash(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        if moretolerant then
+            lpegmatch(pattern_b_s,str)
+        else
+            lpegmatch(pattern_a_s,str)
+        end
+        return hash
+    else
+        return { }
+    end
+end
+
+function aux.settings_to_hash_tolerant(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        lpegmatch(pattern_b_s,str)
+        return hash
+    else
+        return { }
+    end
+end
+
+function aux.settings_to_hash_strict(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        lpegmatch(pattern_c_s,str)
+        return next(hash) and hash
+    else
+        return nil
+    end
+end
+
+local separator = comma * space^0
+local value     = lpeg.P(lbrace * lpeg.C((nobrace + nested)^0) * rbrace) + lpeg.C((nested + (1-comma))^0)
+local pattern   = lpeg.Ct(value*(separator*value)^0)
+
+-- "aap, {noot}, mies" : outer {} removes, leading spaces ignored
+
+aux.settings_to_array_pattern = pattern
+
+-- we could use a weak table as cache
+
+function aux.settings_to_array(str)
+    if not str or str == "" then
+        return { }
+    else
+        return lpegmatch(pattern,str)
+    end
+end
+
+local function set(t,v)
+    t[#t+1] = v
+end
+
+local value   = lpeg.P(lpeg.Carg(1)*value) / set
+local pattern = value*(separator*value)^0 * lpeg.Carg(1)
+
+function aux.add_settings_to_array(t,str)
+    return lpegmatch(pattern,str,nil,t)
+end
+
+function aux.hash_to_string(h,separator,yes,no,strict,omit)
+    if h then
+        local t, s = { }, table.sortedkeys(h)
+        omit = omit and table.tohash(omit)
+        for i=1,#s do
+            local key = s[i]
+            if not omit or not omit[key] then
+                local value = h[key]
+                if type(value) == "boolean" then
+                    if yes and no then
+                        if value then
+                            t[#t+1] = key .. '=' .. yes
+                        elseif not strict then
+                            t[#t+1] = key .. '=' .. no
+                        end
+                    elseif value or not strict then
+                        t[#t+1] = key .. '=' .. tostring(value)
+                    end
+                else
+                    t[#t+1] = key .. '=' .. value
+                end
+            end
+        end
+        return concat(t,separator or ",")
+    else
+        return ""
+    end
+end
+
+function aux.array_to_string(a,separator)
+    if a then
+        return concat(a,separator or ",")
+    else
+        return ""
+    end
+end
+
+function aux.settings_to_set(str,t)
+    t = t or { }
+    for s in gmatch(str,"%s*([^,]+)") do
+        t[s] = true
+    end
+    return t
+end
+
+local value     = lbrace * lpeg.C((nobrace + nested)^0) * rbrace
+local pattern   = lpeg.Ct((space + value)^0)
+
+function aux.arguments_to_table(str)
+    return lpegmatch(pattern,str)
+end
+
+-- temporary here
+
+function aux.getparameters(self,class,parentclass,settings)
+    local sc = self[class]
+    if not sc then
+        sc = table.clone(self[parent])
+        self[class] = sc
+    end
+    aux.settings_to_hash(settings,sc)
+end
+
+-- temporary here
+
+local digit         = lpeg.R("09")
+local period        = lpeg.P(".")
+local zero          = lpeg.P("0")
+local trailingzeros = zero^0 * -digit -- suggested by Roberto R
+local case_1        = period * trailingzeros / ""
+local case_2        = period * (digit - trailingzeros)^1 * (trailingzeros / "")
+local number        = digit^1 * (case_1 + case_2)
+local stripper      = lpeg.Cs((number + 1)^0)
+
+--~ local sample = "bla 11.00 bla 11 bla 0.1100 bla 1.00100 bla 0.00 bla 0.001 bla 1.1100 bla 0.100100100 bla 0.00100100100"
+--~ collectgarbage("collect")
+--~ str = string.rep(sample,10000)
+--~ local ts = os.clock()
+--~ lpegmatch(stripper,str)
+--~ print(#str, os.clock()-ts, lpegmatch(stripper,sample))
+
+lpeg.patterns.strip_zeros = stripper
+
+function aux.strip_zeros(str)
+    return lpegmatch(stripper,str)
+end
+
+function aux.definetable(target) -- defines undefined tables
+    local composed, t = nil, { }
+    for name in gmatch(target,"([^%.]+)") do
+        if composed then
+            composed = composed .. "." .. name
+        else
+            composed = name
+        end
+        t[#t+1] = format("%s = %s or { }",composed,composed)
+    end
+    return concat(t,"\n")
+end
+
+function aux.accesstable(target)
+    local t = _G
+    for name in gmatch(target,"([^%.]+)") do
+        t = t[name]
+    end
+    return t
+end
+
+--~ function string.commaseparated(str)
+--~     return gmatch(str,"([^,%s]+)")
+--~ end
+
+-- as we use this a lot ...
+
+--~ function aux.cachefunction(action,weak)
+--~     local cache = { }
+--~     if weak then
+--~         setmetatable(cache, { __mode = "kv" } )
+--~     end
+--~     local function reminder(str)
+--~         local found = cache[str]
+--~         if not found then
+--~             found = action(str)
+--~             cache[str] = found
+--~         end
+--~         return found
+--~     end
+--~     return reminder, cache
+--~ end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-tra'] = {
+    version   = 1.001,
+    comment   = "companion to trac-tra.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- the <anonymous> tag is kind of generic and used for functions that are not
+-- bound to a variable, like node.new, node.copy etc (contrary to for instance
+-- node.has_attribute which is bound to a has_attribute local variable in mkiv)
+
+local debug = require "debug"
+
+local getinfo = debug.getinfo
+local type, next = type, next
+local concat = table.concat
+local format, find, lower, gmatch, gsub = string.format, string.find, string.lower, string.gmatch, string.gsub
+
+debugger = debugger or { }
+
+local counters = { }
+local names = { }
+
+-- one
+
+local function hook()
+    local f = getinfo(2,"f").func
+    local n = getinfo(2,"Sn")
+--  if n.what == "C" and n.name then print (n.namewhat .. ': ' .. n.name) end
+    if f then
+        local cf = counters[f]
+        if cf == nil then
+            counters[f] = 1
+            names[f] = n
+        else
+            counters[f] = cf + 1
+        end
+    end
+end
+local function getname(func)
+    local n = names[func]
+    if n then
+        if n.what == "C" then
+            return n.name or '<anonymous>'
+        else
+            -- source short_src linedefined what name namewhat nups func
+            local name = n.name or n.namewhat or n.what
+            if not name or name == "" then name = "?" end
+            return format("%s : %s : %s", n.short_src or "unknown source", n.linedefined or "--", name)
+        end
+    else
+        return "unknown"
+    end
+end
+function debugger.showstats(printer,threshold)
+    printer   = printer or texio.write or print
+    threshold = threshold or 0
+    local total, grandtotal, functions = 0, 0, 0
+    printer("\n") -- ugly but ok
+ -- table.sort(counters)
+    for func, count in next, counters do
+        if count > threshold then
+            local name = getname(func)
+            if not find(name,"for generator") then
+                printer(format("%8i  %s", count, name))
+                total = total + count
+            end
+        end
+        grandtotal = grandtotal + count
+        functions = functions + 1
+    end
+    printer(format("functions: %s, total: %s, grand total: %s, threshold: %s\n", functions, total, grandtotal, threshold))
+end
+
+-- two
+
+--~ local function hook()
+--~     local n = getinfo(2)
+--~     if n.what=="C" and not n.name then
+--~         local f = tostring(debug.traceback())
+--~         local cf = counters[f]
+--~         if cf == nil then
+--~             counters[f] = 1
+--~             names[f] = n
+--~         else
+--~             counters[f] = cf + 1
+--~         end
+--~     end
+--~ end
+--~ function debugger.showstats(printer,threshold)
+--~     printer   = printer or texio.write or print
+--~     threshold = threshold or 0
+--~     local total, grandtotal, functions = 0, 0, 0
+--~     printer("\n") -- ugly but ok
+--~  -- table.sort(counters)
+--~     for func, count in next, counters do
+--~         if count > threshold then
+--~             printer(format("%8i  %s", count, func))
+--~             total = total + count
+--~         end
+--~         grandtotal = grandtotal + count
+--~         functions = functions + 1
+--~     end
+--~     printer(format("functions: %s, total: %s, grand total: %s, threshold: %s\n", functions, total, grandtotal, threshold))
+--~ end
+
+-- rest
+
+function debugger.savestats(filename,threshold)
+    local f = io.open(filename,'w')
+    if f then
+        debugger.showstats(function(str) f:write(str) end,threshold)
+        f:close()
+    end
+end
+
+function debugger.enable()
+    debug.sethook(hook,"c")
+end
+
+function debugger.disable()
+    debug.sethook()
+--~ counters[debug.getinfo(2,"f").func] = nil
+end
+
+function debugger.tracing()
+    local n = tonumber(os.env['MTX.TRACE.CALLS']) or tonumber(os.env['MTX_TRACE_CALLS']) or 0
+    if n > 0 then
+        function debugger.tracing() return true  end ; return true
+    else
+        function debugger.tracing() return false end ; return false
+    end
+end
+
+--~ debugger.enable()
+
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+
+--~ debugger.disable()
+
+--~ print("")
+--~ debugger.showstats()
+--~ print("")
+--~ debugger.showstats(print,3)
+
+setters      = setters      or { }
+setters.data = setters.data or { }
+
+--~ local function set(t,what,value)
+--~     local data, done = t.data, t.done
+--~     if type(what) == "string" then
+--~         what = aux.settings_to_array(what) -- inefficient but ok
+--~     end
+--~     for i=1,#what do
+--~         local w = what[i]
+--~         for d, f in next, data do
+--~             if done[d] then
+--~                 -- prevent recursion due to wildcards
+--~             elseif find(d,w) then
+--~                 done[d] = true
+--~                 for i=1,#f do
+--~                     f[i](value)
+--~                 end
+--~             end
+--~         end
+--~     end
+--~ end
+
+local function set(t,what,value)
+    local data, done = t.data, t.done
+    if type(what) == "string" then
+        what = aux.settings_to_hash(what) -- inefficient but ok
+    end
+    for w, v in next, what do
+        if v == "" then
+            v = value
+        else
+            v = toboolean(v)
+        end
+        for d, f in next, data do
+            if done[d] then
+                -- prevent recursion due to wildcards
+            elseif find(d,w) then
+                done[d] = true
+                for i=1,#f do
+                    f[i](v)
+                end
+            end
+        end
+    end
+end
+
+local function reset(t)
+    for d, f in next, t.data do
+        for i=1,#f do
+            f[i](false)
+        end
+    end
+end
+
+local function enable(t,what)
+    set(t,what,true)
+end
+
+local function disable(t,what)
+    local data = t.data
+    if not what or what == "" then
+        t.done = { }
+        reset(t)
+    else
+        set(t,what,false)
+    end
+end
+
+function setters.register(t,what,...)
+    local data = t.data
+    what = lower(what)
+    local w = data[what]
+    if not w then
+        w = { }
+        data[what] = w
+    end
+    for _, fnc in next, { ... } do
+        local typ = type(fnc)
+        if typ == "function" then
+            w[#w+1] = fnc
+        elseif typ == "string" then
+            w[#w+1] = function(value) set(t,fnc,value,nesting) end
+        end
+    end
+end
+
+function setters.enable(t,what)
+    local e = t.enable
+    t.enable, t.done = enable, { }
+    enable(t,string.simpleesc(tostring(what)))
+    t.enable, t.done = e, { }
+end
+
+function setters.disable(t,what)
+    local e = t.disable
+    t.disable, t.done = disable, { }
+    disable(t,string.simpleesc(tostring(what)))
+    t.disable, t.done = e, { }
+end
+
+function setters.reset(t)
+    t.done = { }
+    reset(t)
+end
+
+function setters.list(t) -- pattern
+    local list = table.sortedkeys(t.data)
+    local user, system = { }, { }
+    for l=1,#list do
+        local what = list[l]
+        if find(what,"^%*") then
+            system[#system+1] = what
+        else
+            user[#user+1] = what
+        end
+    end
+    return user, system
+end
+
+function setters.show(t)
+    commands.writestatus("","")
+    local list = setters.list(t)
+    for k=1,#list do
+        commands.writestatus(t.name,list[k])
+    end
+    commands.writestatus("","")
+end
+
+-- we could have used a bit of oo and the trackers:enable syntax but
+-- there is already a lot of code around using the singular tracker
+
+-- we could make this into a module
+
+function setters.new(name)
+    local t
+    t = {
+        data     = { },
+        name     = name,
+        enable   = function(...) setters.enable  (t,...) end,
+        disable  = function(...) setters.disable (t,...) end,
+        register = function(...) setters.register(t,...) end,
+        list     = function(...) setters.list    (t,...) end,
+        show     = function(...) setters.show    (t,...) end,
+    }
+    setters.data[name] = t
+    return t
+end
+
+trackers    = setters.new("trackers")
+directives  = setters.new("directives")
+experiments = setters.new("experiments")
+
+-- nice trick: we overload two of the directives related functions with variants that
+-- do tracing (itself using a tracker) .. proof of concept
+
+local trace_directives  = false local trace_directives  = false  trackers.register("system.directives",  function(v) trace_directives  = v end)
+local trace_experiments = false local trace_experiments = false  trackers.register("system.experiments", function(v) trace_experiments = v end)
+
+local e = directives.enable
+local d = directives.disable
+
+function directives.enable(...)
+    commands.writestatus("directives","enabling: %s",concat({...}," "))
+    e(...)
+end
+
+function directives.disable(...)
+    commands.writestatus("directives","disabling: %s",concat({...}," "))
+    d(...)
+end
+
+local e = experiments.enable
+local d = experiments.disable
+
+function experiments.enable(...)
+    commands.writestatus("experiments","enabling: %s",concat({...}," "))
+    e(...)
+end
+
+function experiments.disable(...)
+    commands.writestatus("experiments","disabling: %s",concat({...}," "))
+    d(...)
+end
+
+-- a useful example
+
+directives.register("system.nostatistics", function(v)
+    statistics.enable = not v
+end)
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['lxml-tab'] = {
+    version   = 1.001,
+    comment   = "this module is the basis for the lxml-* ones",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- this module needs a cleanup: check latest lpeg, passing args, (sub)grammar, etc etc
+-- stripping spaces from e.g. cont-en.xml saves .2 sec runtime so it's not worth the
+-- trouble
+
+local trace_entities = false  trackers.register("xml.entities", function(v) trace_entities = v end)
+
+--[[ldx--
+<p>The parser used here is inspired by the variant discussed in the lua book, but
+handles comment and processing instructions, has a different structure, provides
+parent access; a first version used different trickery but was less optimized to we
+went this route. First we had a find based parser, now we have an <l n='lpeg'/> based one.
+The find based parser can be found in l-xml-edu.lua along with other older code.</p>
+
+<p>Beware, the interface may change. For instance at, ns, tg, dt may get more
+verbose names. Once the code is stable we will also remove some tracing and
+optimize the code.</p>
+--ldx]]--
+
+xml = xml or { }
+
+--~ local xml = xml
+
+local concat, remove, insert = table.concat, table.remove, table.insert
+local type, next, setmetatable, getmetatable, tonumber = type, next, setmetatable, getmetatable, tonumber
+local format, lower, find, match, gsub = string.format, string.lower, string.find, string.match, string.gsub
+local utfchar = unicode.utf8.char
+local lpegmatch = lpeg.match
+local P, S, R, C, V, C, Cs = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.V, lpeg.C, lpeg.Cs
+
+--[[ldx--
+<p>First a hack to enable namespace resolving. A namespace is characterized by
+a <l n='url'/>. The following function associates a namespace prefix with a
+pattern. We use <l n='lpeg'/>, which in this case is more than twice as fast as a
+find based solution where we loop over an array of patterns. Less code and
+much cleaner.</p>
+--ldx]]--
+
+xml.xmlns = xml.xmlns or { }
+
+local check = P(false)
+local parse = check
+
+--[[ldx--
+<p>The next function associates a namespace prefix with an <l n='url'/>. This
+normally happens independent of parsing.</p>
+
+<typing>
+xml.registerns("mml","mathml")
+</typing>
+--ldx]]--
+
+function xml.registerns(namespace, pattern) -- pattern can be an lpeg
+    check = check + C(P(lower(pattern))) / namespace
+    parse = P { P(check) + 1 * V(1) }
+end
+
+--[[ldx--
+<p>The next function also registers a namespace, but this time we map a
+given namespace prefix onto a registered one, using the given
+<l n='url'/>. This used for attributes like <t>xmlns:m</t>.</p>
+
+<typing>
+xml.checkns("m","http://www.w3.org/mathml")
+</typing>
+--ldx]]--
+
+function xml.checkns(namespace,url)
+    local ns = lpegmatch(parse,lower(url))
+    if ns and namespace ~= ns then
+        xml.xmlns[namespace] = ns
+    end
+end
+
+--[[ldx--
+<p>Next we provide a way to turn an <l n='url'/> into a registered
+namespace. This used for the <t>xmlns</t> attribute.</p>
+
+<typing>
+resolvedns = xml.resolvens("http://www.w3.org/mathml")
+</typing>
+
+This returns <t>mml</t>.
+--ldx]]--
+
+function xml.resolvens(url)
+     return lpegmatch(parse,lower(url)) or ""
+end
+
+--[[ldx--
+<p>A namespace in an element can be remapped onto the registered
+one efficiently by using the <t>xml.xmlns</t> table.</p>
+--ldx]]--
+
+--[[ldx--
+<p>This version uses <l n='lpeg'/>. We follow the same approach as before, stack and top and
+such. This version is about twice as fast which is mostly due to the fact that
+we don't have to prepare the stream for cdata, doctype etc etc. This variant is
+is dedicated to Luigi Scarso, who challenged me with 40 megabyte <l n='xml'/> files that
+took 12.5 seconds to load (1.5 for file io and the rest for tree building). With
+the <l n='lpeg'/> implementation we got that down to less 7.3 seconds. Loading the 14
+<l n='context'/> interface definition files (2.6 meg) went down from 1.05 seconds to 0.55.</p>
+
+<p>Next comes the parser. The rather messy doctype definition comes in many
+disguises so it is no surprice that later on have to dedicate quite some
+<l n='lpeg'/> code to it.</p>
+
+<typing>
+<!DOCTYPE Something PUBLIC "... ..." "..." [ ... ] >
+<!DOCTYPE Something PUBLIC "... ..." "..." >
+<!DOCTYPE Something SYSTEM "... ..." [ ... ] >
+<!DOCTYPE Something SYSTEM "... ..." >
+<!DOCTYPE Something [ ... ] >
+<!DOCTYPE Something >
+</typing>
+
+<p>The code may look a bit complex but this is mostly due to the fact that we
+resolve namespaces and attach metatables. There is only one public function:</p>
+
+<typing>
+local x = xml.convert(somestring)
+</typing>
+
+<p>An optional second boolean argument tells this function not to create a root
+element.</p>
+
+<p>Valid entities are:</p>
+
+<typing>
+<!ENTITY xxxx SYSTEM "yyyy" NDATA zzzz>
+<!ENTITY xxxx PUBLIC "yyyy" >
+<!ENTITY xxxx "yyyy" >
+</typing>
+--ldx]]--
+
+-- not just one big nested table capture (lpeg overflow)
+
+local nsremap, resolvens = xml.xmlns, xml.resolvens
+
+local stack, top, dt, at, xmlns, errorstr, entities = { }, { }, { }, { }, { }, nil, { }
+local strip, cleanup, utfize, resolve, resolve_predefined, unify_predefined = false, false, false, false, false, false
+local dcache, hcache, acache = { }, { }, { }
+
+local mt = { }
+
+function initialize_mt(root)
+    mt = { __index = root } -- will be redefined later
+end
+
+function xml.setproperty(root,k,v)
+    getmetatable(root).__index[k] = v
+end
+
+function xml.check_error(top,toclose)
+    return ""
+end
+
+local function add_attribute(namespace,tag,value)
+    if cleanup and #value > 0 then
+        value = cleanup(value) -- new
+    end
+    if tag == "xmlns" then
+        xmlns[#xmlns+1] = resolvens(value)
+        at[tag] = value
+    elseif namespace == "" then
+        at[tag] = value
+    elseif namespace == "xmlns" then
+        xml.checkns(tag,value)
+        at["xmlns:" .. tag] = value
+    else
+        -- for the moment this way:
+        at[namespace .. ":" .. tag] = value
+    end
+end
+
+local function add_empty(spacing, namespace, tag)
+    if #spacing > 0 then
+        dt[#dt+1] = spacing
+    end
+    local resolved = (namespace == "" and xmlns[#xmlns]) or nsremap[namespace] or namespace
+    top = stack[#stack]
+    dt = top.dt
+    local t = { ns=namespace or "", rn=resolved, tg=tag, at=at, dt={}, __p__ = top }
+    dt[#dt+1] = t
+    setmetatable(t, mt)
+    if at.xmlns then
+        remove(xmlns)
+    end
+    at = { }
+end
+
+local function add_begin(spacing, namespace, tag)
+    if #spacing > 0 then
+        dt[#dt+1] = spacing
+    end
+    local resolved = (namespace == "" and xmlns[#xmlns]) or nsremap[namespace] or namespace
+    top = { ns=namespace or "", rn=resolved, tg=tag, at=at, dt={}, __p__ = stack[#stack] }
+    setmetatable(top, mt)
+    dt = top.dt
+    stack[#stack+1] = top
+    at = { }
+end
+
+local function add_end(spacing, namespace, tag)
+    if #spacing > 0 then
+        dt[#dt+1] = spacing
+    end
+    local toclose = remove(stack)
+    top = stack[#stack]
+    if #stack < 1 then
+        errorstr = format("nothing to close with %s %s", tag, xml.check_error(top,toclose) or "")
+    elseif toclose.tg ~= tag then -- no namespace check
+        errorstr = format("unable to close %s with %s %s", toclose.tg, tag, xml.check_error(top,toclose) or "")
+    end
+    dt = top.dt
+    dt[#dt+1] = toclose
+ -- dt[0] = top -- nasty circular reference when serializing table
+    if toclose.at.xmlns then
+        remove(xmlns)
+    end
+end
+
+local function add_text(text)
+    if cleanup and #text > 0 then
+        dt[#dt+1] = cleanup(text)
+    else
+        dt[#dt+1] = text
+    end
+end
+
+local function add_special(what, spacing, text)
+    if #spacing > 0 then
+        dt[#dt+1] = spacing
+    end
+    if strip and (what == "@cm@" or what == "@dt@") then
+        -- forget it
+    else
+        dt[#dt+1] = { special=true, ns="", tg=what, dt={ text } }
+    end
+end
+
+local function set_message(txt)
+    errorstr = "garbage at the end of the file: " .. gsub(txt,"([ \n\r\t]*)","")
+end
+
+local reported_attribute_errors = { }
+
+local function attribute_value_error(str)
+    if not reported_attribute_errors[str] then
+        logs.report("xml","invalid attribute value: %q",str)
+        reported_attribute_errors[str] = true
+        at._error_ = str
+    end
+    return str
+end
+local function attribute_specification_error(str)
+    if not reported_attribute_errors[str] then
+        logs.report("xml","invalid attribute specification: %q",str)
+        reported_attribute_errors[str] = true
+        at._error_ = str
+    end
+    return str
+end
+
+function xml.unknown_dec_entity_format(str) return (str == "" and "&error;") or format("&%s;",str) end
+function xml.unknown_hex_entity_format(str) return format("&#x%s;",str) end
+function xml.unknown_any_entity_format(str) return format("&#x%s;",str) end
+
+local function fromhex(s)
+    local n = tonumber(s,16)
+    if n then
+        return utfchar(n)
+    else
+        return format("h:%s",s), true
+    end
+end
+
+local function fromdec(s)
+    local n = tonumber(s)
+    if n then
+        return utfchar(n)
+    else
+        return format("d:%s",s), true
+    end
+end
+
+-- one level expansion (simple case), no checking done
+
+local rest = (1-P(";"))^0
+local many = P(1)^0
+
+local parsedentity =
+    P("&") * (P("#x")*(rest/fromhex) + P("#")*(rest/fromdec)) * P(";") * P(-1) +
+             (P("#x")*(many/fromhex) + P("#")*(many/fromdec))
+
+-- parsing in the xml file
+
+local predefined_unified = {
+    [38] = "&amp;",
+    [42] = "&quot;",
+    [47] = "&apos;",
+    [74] = "&lt;",
+    [76] = "&gr;",
+}
+
+local predefined_simplified = {
+    [38] = "&", amp  = "&",
+    [42] = '"', quot = '"',
+    [47] = "'", apos = "'",
+    [74] = "<", lt   = "<",
+    [76] = ">", gt   = ">",
+}
+
+local function handle_hex_entity(str)
+    local h = hcache[str]
+    if not h then
+        local n = tonumber(str,16)
+        h = unify_predefined and predefined_unified[n]
+        if h then
+            if trace_entities then
+                logs.report("xml","utfize, converting hex entity &#x%s; into %s",str,h)
+            end
+        elseif utfize then
+            h = (n and utfchar(n)) or xml.unknown_hex_entity_format(str) or ""
+            if not n then
+                logs.report("xml","utfize, ignoring hex entity &#x%s;",str)
+            elseif trace_entities then
+                logs.report("xml","utfize, converting hex entity &#x%s; into %s",str,h)
+            end
+        else
+            if trace_entities then
+                logs.report("xml","found entity &#x%s;",str)
+            end
+            h = "&#x" .. str .. ";"
+        end
+        hcache[str] = h
+    end
+    return h
+end
+
+local function handle_dec_entity(str)
+    local d = dcache[str]
+    if not d then
+        local n = tonumber(str)
+        d = unify_predefined and predefined_unified[n]
+        if d then
+            if trace_entities then
+                logs.report("xml","utfize, converting dec entity &#%s; into %s",str,d)
+            end
+        elseif utfize then
+            d = (n and utfchar(n)) or xml.unknown_dec_entity_format(str) or ""
+            if not n then
+                logs.report("xml","utfize, ignoring dec entity &#%s;",str)
+            elseif trace_entities then
+                logs.report("xml","utfize, converting dec entity &#%s; into %s",str,h)
+            end
+        else
+            if trace_entities then
+                logs.report("xml","found entity &#%s;",str)
+            end
+            d = "&#" .. str .. ";"
+        end
+        dcache[str] = d
+    end
+    return d
+end
+
+xml.parsedentitylpeg = parsedentity
+
+local function handle_any_entity(str)
+    if resolve then
+        local a = acache[str] -- per instance ! todo
+        if not a then
+            a = resolve_predefined and predefined_simplified[str]
+            if a then
+                -- one of the predefined
+            elseif type(resolve) == "function" then
+                a = resolve(str) or entities[str]
+            else
+                a = entities[str]
+            end
+            if a then
+                if trace_entities then
+                    logs.report("xml","resolved entity &%s; -> %s (internal)",str,a)
+                end
+                a = lpegmatch(parsedentity,a) or a
+            else
+                if xml.unknown_any_entity_format then
+                    a = xml.unknown_any_entity_format(str) or ""
+                end
+                if a then
+                    if trace_entities then
+                        logs.report("xml","resolved entity &%s; -> %s (external)",str,a)
+                    end
+                else
+                    if trace_entities then
+                        logs.report("xml","keeping entity &%s;",str)
+                    end
+                    if str == "" then
+                        a = "&error;"
+                    else
+                        a = "&" .. str .. ";"
+                    end
+                end
+            end
+            acache[str] = a
+        elseif trace_entities then
+            if not acache[str] then
+                logs.report("xml","converting entity &%s; into %s",str,a)
+                acache[str] = a
+            end
+        end
+        return a
+    else
+        local a = acache[str]
+        if not a then
+            if trace_entities then
+                logs.report("xml","found entity &%s;",str)
+            end
+            a = resolve_predefined and predefined_simplified[str]
+            if a then
+                -- one of the predefined
+                acache[str] = a
+            elseif str == "" then
+                a = "&error;"
+                acache[str] = a
+            else
+                a = "&" .. str .. ";"
+                acache[str] = a
+            end
+        end
+        return a
+    end
+end
+
+local function handle_end_entity(chr)
+    logs.report("xml","error in entity, %q found instead of ';'",chr)
+end
+
+local space            = S(' \r\n\t')
+local open             = P('<')
+local close            = P('>')
+local squote           = S("'")
+local dquote           = S('"')
+local equal            = P('=')
+local slash            = P('/')
+local colon            = P(':')
+local semicolon        = P(';')
+local ampersand        = P('&')
+local valid            = R('az', 'AZ', '09') + S('_-.')
+local name_yes         = C(valid^1) * colon * C(valid^1)
+local name_nop         = C(P(true)) * C(valid^1)
+local name             = name_yes + name_nop
+local utfbom           = lpeg.patterns.utfbom -- no capture
+local spacing          = C(space^0)
+
+----- entitycontent    = (1-open-semicolon)^0
+local anyentitycontent = (1-open-semicolon-space-close)^0
+local hexentitycontent = R("AF","af","09")^0
+local decentitycontent = R("09")^0
+local parsedentity     = P("#")/"" * (
+                                P("x")/"" * (hexentitycontent/handle_hex_entity) +
+                                            (decentitycontent/handle_dec_entity)
+                            ) +             (anyentitycontent/handle_any_entity)
+local entity           = ampersand/"" * parsedentity * ( (semicolon/"") + #(P(1)/handle_end_entity))
+
+local text_unparsed    = C((1-open)^1)
+local text_parsed      = Cs(((1-open-ampersand)^1 + entity)^1)
+
+local somespace        = space^1
+local optionalspace    = space^0
+
+----- value            = (squote * C((1 - squote)^0) * squote) + (dquote * C((1 - dquote)^0) * dquote) -- ampersand and < also invalid in value
+local value            = (squote * Cs((entity + (1 - squote))^0) * squote) + (dquote * Cs((entity + (1 - dquote))^0) * dquote) -- ampersand and < also invalid in value
+
+local endofattributes  = slash * close + close -- recovery of flacky html
+local whatever         = space * name * optionalspace * equal
+local wrongvalue       = C(P(1-whatever-close)^1 + P(1-close)^1) / attribute_value_error
+----- wrongvalue       = C(P(1-whatever-endofattributes)^1 + P(1-endofattributes)^1) / attribute_value_error
+----- wrongvalue       = C(P(1-space-endofattributes)^1) / attribute_value_error
+local wrongvalue       = Cs(P(entity + (1-space-endofattributes))^1) / attribute_value_error
+
+local attributevalue   = value + wrongvalue
+
+local attribute        = (somespace * name * optionalspace * equal * optionalspace * attributevalue) / add_attribute
+----- attributes       = (attribute)^0
+
+local attributes       = (attribute + somespace^-1 * (((1-endofattributes)^1)/attribute_specification_error))^0
+
+local parsedtext       = text_parsed   / add_text
+local unparsedtext     = text_unparsed / add_text
+local balanced         = P { "[" * ((1 - S"[]") + V(1))^0 * "]" } -- taken from lpeg manual, () example
+
+local emptyelement     = (spacing * open         * name * attributes * optionalspace * slash * close) / add_empty
+local beginelement     = (spacing * open         * name * attributes * optionalspace         * close) / add_begin
+local endelement       = (spacing * open * slash * name              * optionalspace         * close) / add_end
+
+local begincomment     = open * P("!--")
+local endcomment       = P("--") * close
+local begininstruction = open * P("?")
+local endinstruction   = P("?") * close
+local begincdata       = open * P("![CDATA[")
+local endcdata         = P("]]") * close
+
+local someinstruction  = C((1 - endinstruction)^0)
+local somecomment      = C((1 - endcomment    )^0)
+local somecdata        = C((1 - endcdata      )^0)
+
+local function normalentity(k,v  ) entities[k] = v end
+local function systementity(k,v,n) entities[k] = v end
+local function publicentity(k,v,n) entities[k] = v end
+
+local begindoctype     = open * P("!DOCTYPE")
+local enddoctype       = close
+local beginset         = P("[")
+local endset           = P("]")
+local doctypename      = C((1-somespace-close)^0)
+local elementdoctype   = optionalspace * P("<!ELEMENT") * (1-close)^0 * close
+
+local normalentitytype = (doctypename * somespace * value)/normalentity
+local publicentitytype = (doctypename * somespace * P("PUBLIC") * somespace * value)/publicentity
+local systementitytype = (doctypename * somespace * P("SYSTEM") * somespace * value * somespace * P("NDATA") * somespace * doctypename)/systementity
+local entitydoctype    = optionalspace * P("<!ENTITY") * somespace * (systementitytype + publicentitytype + normalentitytype) * optionalspace * close
+
+local doctypeset       = beginset * optionalspace * P(elementdoctype + entitydoctype + space)^0 * optionalspace * endset
+local definitiondoctype= doctypename * somespace * doctypeset
+local publicdoctype    = doctypename * somespace * P("PUBLIC") * somespace * value * somespace * value * somespace * doctypeset
+local systemdoctype    = doctypename * somespace * P("SYSTEM") * somespace * value * somespace * doctypeset
+local simpledoctype    = (1-close)^1 -- * balanced^0
+local somedoctype      = C((somespace * (publicdoctype + systemdoctype + definitiondoctype + simpledoctype) * optionalspace)^0)
+
+local instruction      = (spacing * begininstruction * someinstruction * endinstruction) / function(...) add_special("@pi@",...) end
+local comment          = (spacing * begincomment     * somecomment     * endcomment    ) / function(...) add_special("@cm@",...) end
+local cdata            = (spacing * begincdata       * somecdata       * endcdata      ) / function(...) add_special("@cd@",...) end
+local doctype          = (spacing * begindoctype     * somedoctype     * enddoctype    ) / function(...) add_special("@dt@",...) end
+
+--  nicer but slower:
+--
+--  local instruction = (Cc("@pi@") * spacing * begininstruction * someinstruction * endinstruction) / add_special
+--  local comment     = (Cc("@cm@") * spacing * begincomment     * somecomment     * endcomment    ) / add_special
+--  local cdata       = (Cc("@cd@") * spacing * begincdata       * somecdata       * endcdata      ) / add_special
+--  local doctype     = (Cc("@dt@") * spacing * begindoctype     * somedoctype     * enddoctype    ) / add_special
+
+local trailer = space^0 * (text_unparsed/set_message)^0
+
+--  comment + emptyelement + text + cdata + instruction + V("parent"), -- 6.5 seconds on 40 MB database file
+--  text + comment + emptyelement + cdata + instruction + V("parent"), -- 5.8
+--  text + V("parent") + emptyelement + comment + cdata + instruction, -- 5.5
+
+local grammar_parsed_text = P { "preamble",
+    preamble = utfbom^0 * instruction^0 * (doctype + comment + instruction)^0 * V("parent") * trailer,
+    parent   = beginelement * V("children")^0 * endelement,
+    children = parsedtext + V("parent") + emptyelement + comment + cdata + instruction,
+}
+
+local grammar_unparsed_text = P { "preamble",
+    preamble = utfbom^0 * instruction^0 * (doctype + comment + instruction)^0 * V("parent") * trailer,
+    parent   = beginelement * V("children")^0 * endelement,
+    children = unparsedtext + V("parent") + emptyelement + comment + cdata + instruction,
+}
+
+-- maybe we will add settinsg to result as well
+
+local function xmlconvert(data, settings)
+    settings = settings or { } -- no_root strip_cm_and_dt given_entities parent_root error_handler
+    strip = settings.strip_cm_and_dt
+    utfize = settings.utfize_entities
+    resolve = settings.resolve_entities
+    resolve_predefined = settings.resolve_predefined_entities -- in case we have escaped entities
+    unify_predefined = settings.unify_predefined_entities -- &#038; -> &amp;
+    cleanup = settings.text_cleanup
+    stack, top, at, xmlns, errorstr, result, entities = { }, { }, { }, { }, nil, nil, settings.entities or { }
+    acache, hcache, dcache = { }, { }, { } -- not stored
+    reported_attribute_errors = { }
+    if settings.parent_root then
+        mt = getmetatable(settings.parent_root)
+    else
+        initialize_mt(top)
+    end
+    stack[#stack+1] = top
+    top.dt = { }
+    dt = top.dt
+    if not data or data == "" then
+        errorstr = "empty xml file"
+    elseif utfize or resolve then
+        if lpegmatch(grammar_parsed_text,data) then
+            errorstr = ""
+        else
+            errorstr = "invalid xml file - parsed text"
+        end
+    elseif type(data) == "string" then
+        if lpegmatch(grammar_unparsed_text,data) then
+            errorstr = ""
+        else
+            errorstr = "invalid xml file - unparsed text"
+        end
+    else
+        errorstr = "invalid xml file - no text at all"
+    end
+    if errorstr and errorstr ~= "" then
+        result = { dt = { { ns = "", tg = "error", dt = { errorstr }, at={ }, er = true } } }
+        setmetatable(stack, mt)
+        local error_handler = settings.error_handler
+        if error_handler == false then
+            -- no error message
+        else
+            error_handler = error_handler or xml.error_handler
+            if error_handler then
+                xml.error_handler("load",errorstr)
+            end
+        end
+    else
+        result = stack[1]
+    end
+    if not settings.no_root then
+        result = { special = true, ns = "", tg = '@rt@', dt = result.dt, at={ }, entities = entities, settings = settings }
+        setmetatable(result, mt)
+        local rdt = result.dt
+        for k=1,#rdt do
+            local v = rdt[k]
+            if type(v) == "table" and not v.special then -- always table -)
+                result.ri = k -- rootindex
+v.__p__ = result  -- new, experiment, else we cannot go back to settings, we need to test this !
+                break
+            end
+        end
+    end
+    if errorstr and errorstr ~= "" then
+        result.error = true
+    end
+    return result
+end
+
+xml.convert = xmlconvert
+
+function xml.inheritedconvert(data,xmldata)
+    local settings = xmldata.settings
+    settings.parent_root = xmldata -- to be tested
+ -- settings.no_root = true
+    local xc = xmlconvert(data,settings)
+ -- xc.settings = nil
+ -- xc.entities = nil
+ -- xc.special = nil
+ -- xc.ri = nil
+ -- print(xc.tg)
+    return xc
+end
+
+--[[ldx--
+<p>Packaging data in an xml like table is done with the following
+function. Maybe it will go away (when not used).</p>
+--ldx]]--
+
+function xml.is_valid(root)
+    return root and root.dt and root.dt[1] and type(root.dt[1]) == "table" and not root.dt[1].er
+end
+
+function xml.package(tag,attributes,data)
+    local ns, tg = match(tag,"^(.-):?([^:]+)$")
+    local t = { ns = ns, tg = tg, dt = data or "", at = attributes or {} }
+    setmetatable(t, mt)
+    return t
+end
+
+function xml.is_valid(root)
+    return root and not root.error
+end
+
+xml.error_handler = (logs and logs.report) or (input and logs.report) or print
+
+--[[ldx--
+<p>We cannot load an <l n='lpeg'/> from a filehandle so we need to load
+the whole file first. The function accepts a string representing
+a filename or a file handle.</p>
+--ldx]]--
+
+function xml.load(filename,settings)
+    local data = ""
+    if type(filename) == "string" then
+     -- local data = io.loaddata(filename) - -todo: check type in io.loaddata
+        local f = io.open(filename,'r')
+        if f then
+            data = f:read("*all")
+            f:close()
+        end
+    elseif filename then -- filehandle
+        data = filename:read("*all")
+    end
+    return xmlconvert(data,settings)
+end
+
+--[[ldx--
+<p>When we inject new elements, we need to convert strings to
+valid trees, which is what the next function does.</p>
+--ldx]]--
+
+local no_root = { no_root = true }
+
+function xml.toxml(data)
+    if type(data) == "string" then
+        local root = { xmlconvert(data,no_root) }
+        return (#root > 1 and root) or root[1]
+    else
+        return data
+    end
+end
+
+--[[ldx--
+<p>For copying a tree we use a dedicated function instead of the
+generic table copier. Since we know what we're dealing with we
+can speed up things a bit. The second argument is not to be used!</p>
+--ldx]]--
+
+local function copy(old,tables)
+    if old then
+        tables = tables or { }
+        local new = { }
+        if not tables[old] then
+            tables[old] = new
+        end
+        for k,v in next, old do
+            new[k] = (type(v) == "table" and (tables[v] or copy(v, tables))) or v
+        end
+        local mt = getmetatable(old)
+        if mt then
+            setmetatable(new,mt)
+        end
+        return new
+    else
+        return { }
+    end
+end
+
+xml.copy = copy
+
+--[[ldx--
+<p>In <l n='context'/> serializing the tree or parts of the tree is a major
+actitivity which is why the following function is pretty optimized resulting
+in a few more lines of code than needed. The variant that uses the formatting
+function for all components is about 15% slower than the concatinating
+alternative.</p>
+--ldx]]--
+
+-- todo: add <?xml version='1.0' standalone='yes'?> when not present
+
+function xml.checkbom(root) -- can be made faster
+    if root.ri then
+        local dt, found = root.dt, false
+        for k=1,#dt do
+            local v = dt[k]
+            if type(v) == "table" and v.special and v.tg == "@pi@" and find(v.dt[1],"xml.*version=") then
+                found = true
+                break
+            end
+        end
+        if not found then
+            insert(dt, 1, { special=true, ns="", tg="@pi@", dt = { "xml version='1.0' standalone='yes'"} } )
+            insert(dt, 2, "\n" )
+        end
+    end
+end
+
+--[[ldx--
+<p>At the cost of some 25% runtime overhead you can first convert the tree to a string
+and then handle the lot.</p>
+--ldx]]--
+
+-- new experimental reorganized serialize
+
+local function verbose_element(e,handlers)
+    local handle = handlers.handle
+    local serialize = handlers.serialize
+    local ens, etg, eat, edt, ern = e.ns, e.tg, e.at, e.dt, e.rn
+    local ats = eat and next(eat) and { }
+    if ats then
+        for k,v in next, eat do
+            ats[#ats+1] = format('%s=%q',k,v)
+        end
+    end
+    if ern and trace_remap and ern ~= ens then
+        ens = ern
+    end
+    if ens ~= "" then
+        if edt and #edt > 0 then
+            if ats then
+                handle("<",ens,":",etg," ",concat(ats," "),">")
+            else
+                handle("<",ens,":",etg,">")
+            end
+            for i=1,#edt do
+                local e = edt[i]
+                if type(e) == "string" then
+                    handle(e)
+                else
+                    serialize(e,handlers)
+                end
+            end
+            handle("</",ens,":",etg,">")
+        else
+            if ats then
+                handle("<",ens,":",etg," ",concat(ats," "),"/>")
+            else
+                handle("<",ens,":",etg,"/>")
+            end
+        end
+    else
+        if edt and #edt > 0 then
+            if ats then
+                handle("<",etg," ",concat(ats," "),">")
+            else
+                handle("<",etg,">")
+            end
+            for i=1,#edt do
+                local ei = edt[i]
+                if type(ei) == "string" then
+                    handle(ei)
+                else
+                    serialize(ei,handlers)
+                end
+            end
+            handle("</",etg,">")
+        else
+            if ats then
+                handle("<",etg," ",concat(ats," "),"/>")
+            else
+                handle("<",etg,"/>")
+            end
+        end
+    end
+end
+
+local function verbose_pi(e,handlers)
+    handlers.handle("<?",e.dt[1],"?>")
+end
+
+local function verbose_comment(e,handlers)
+    handlers.handle("<!--",e.dt[1],"-->")
+end
+
+local function verbose_cdata(e,handlers)
+    handlers.handle("<![CDATA[", e.dt[1],"]]>")
+end
+
+local function verbose_doctype(e,handlers)
+    handlers.handle("<!DOCTYPE ",e.dt[1],">")
+end
+
+local function verbose_root(e,handlers)
+    handlers.serialize(e.dt,handlers)
+end
+
+local function verbose_text(e,handlers)
+    handlers.handle(e)
+end
+
+local function verbose_document(e,handlers)
+    local serialize = handlers.serialize
+    local functions = handlers.functions
+    for i=1,#e do
+        local ei = e[i]
+        if type(ei) == "string" then
+            functions["@tx@"](ei,handlers)
+        else
+            serialize(ei,handlers)
+        end
+    end
+end
+
+local function serialize(e,handlers,...)
+    local initialize = handlers.initialize
+    local finalize   = handlers.finalize
+    local functions  = handlers.functions
+    if initialize then
+        local state = initialize(...)
+        if not state == true then
+            return state
+        end
+    end
+    local etg = e.tg
+    if etg then
+        (functions[etg] or functions["@el@"])(e,handlers)
+ -- elseif type(e) == "string" then
+ --     functions["@tx@"](e,handlers)
+    else
+        functions["@dc@"](e,handlers)
+    end
+    if finalize then
+        return finalize()
+    end
+end
+
+local function xserialize(e,handlers)
+    local functions = handlers.functions
+    local etg = e.tg
+    if etg then
+        (functions[etg] or functions["@el@"])(e,handlers)
+ -- elseif type(e) == "string" then
+ --     functions["@tx@"](e,handlers)
+    else
+        functions["@dc@"](e,handlers)
+    end
+end
+
+local handlers = { }
+
+local function newhandlers(settings)
+    local t = table.copy(handlers.verbose or { }) -- merge
+    if settings then
+        for k,v in next, settings do
+            if type(v) == "table" then
+                tk = t[k] if not tk then tk = { } t[k] = tk end
+                for kk,vv in next, v do
+                    tk[kk] = vv
+                end
+            else
+                t[k] = v
+            end
+        end
+        if settings.name then
+            handlers[settings.name] = t
+        end
+    end
+    return t
+end
+
+local nofunction = function() end
+
+function xml.sethandlersfunction(handler,name,fnc)
+    handler.functions[name] = fnc or nofunction
+end
+
+function xml.gethandlersfunction(handler,name)
+    return handler.functions[name]
+end
+
+function xml.gethandlers(name)
+    return handlers[name]
+end
+
+newhandlers {
+    name       = "verbose",
+    initialize = false, -- faster than nil and mt lookup
+    finalize   = false, -- faster than nil and mt lookup
+    serialize  = xserialize,
+    handle     = print,
+    functions  = {
+        ["@dc@"]   = verbose_document,
+        ["@dt@"]   = verbose_doctype,
+        ["@rt@"]   = verbose_root,
+        ["@el@"]   = verbose_element,
+        ["@pi@"]   = verbose_pi,
+        ["@cm@"]   = verbose_comment,
+        ["@cd@"]   = verbose_cdata,
+        ["@tx@"]   = verbose_text,
+    }
+}
+
+--[[ldx--
+<p>How you deal with saving data depends on your preferences. For a 40 MB database
+file the timing on a 2.3 Core Duo are as follows (time in seconds):</p>
+
+<lines>
+1.3 : load data from file to string
+6.1 : convert string into tree
+5.3 : saving in file using xmlsave
+6.8 : converting to string using xml.tostring
+3.6 : saving converted string in file
+</lines>
+
+<p>Beware, these were timing with the old routine but measurements will not be that
+much different I guess.</p>
+--ldx]]--
+
+-- maybe this will move to lxml-xml
+
+local result
+
+local xmlfilehandler = newhandlers {
+    name       = "file",
+    initialize = function(name) result = io.open(name,"wb") return result end,
+    finalize   = function() result:close() return true end,
+    handle     = function(...) result:write(...) end,
+}
+
+-- no checking on writeability here but not faster either
+--
+-- local xmlfilehandler = newhandlers {
+--     initialize = function(name) io.output(name,"wb") return true end,
+--     finalize   = function() io.close() return true end,
+--     handle     = io.write,
+-- }
+
+
+function xml.save(root,name)
+    serialize(root,xmlfilehandler,name)
+end
+
+local result
+
+local xmlstringhandler = newhandlers {
+    name       = "string",
+    initialize = function() result = { } return result end,
+    finalize   = function() return concat(result) end,
+    handle     = function(...) result[#result+1] = concat { ... } end
+}
+
+local function xmltostring(root) -- 25% overhead due to collecting
+    if root then
+        if type(root) == 'string' then
+            return root
+        else -- if next(root) then -- next is faster than type (and >0 test)
+            return serialize(root,xmlstringhandler) or ""
+        end
+    end
+    return ""
+end
+
+local function xmltext(root) -- inline
+    return (root and xmltostring(root)) or ""
+end
+
+function initialize_mt(root)
+    mt = { __tostring = xmltext, __index = root }
+end
+
+xml.defaulthandlers = handlers
+xml.newhandlers     = newhandlers
+xml.serialize       = serialize
+xml.tostring        = xmltostring
+
+--[[ldx--
+<p>The next function operated on the content only and needs a handle function
+that accepts a string.</p>
+--ldx]]--
+
+local function xmlstring(e,handle)
+    if not handle or (e.special and e.tg ~= "@rt@") then
+        -- nothing
+    elseif e.tg then
+        local edt = e.dt
+        if edt then
+            for i=1,#edt do
+                xmlstring(edt[i],handle)
+            end
+        end
+    else
+        handle(e)
+    end
+end
+
+xml.string = xmlstring
+
+--[[ldx--
+<p>A few helpers:</p>
+--ldx]]--
+
+--~ xmlsetproperty(root,"settings",settings)
+
+function xml.settings(e)
+    while e do
+        local s = e.settings
+        if s then
+            return s
+        else
+            e = e.__p__
+        end
+    end
+    return nil
+end
+
+function xml.root(e)
+    local r = e
+    while e do
+        e = e.__p__
+        if e then
+            r = e
+        end
+    end
+    return r
+end
+
+function xml.parent(root)
+    return root.__p__
+end
+
+function xml.body(root)
+    return (root.ri and root.dt[root.ri]) or root -- not ok yet
+end
+
+function xml.name(root)
+    if not root then
+        return ""
+    elseif root.ns == "" then
+        return root.tg
+    else
+        return root.ns .. ":" .. root.tg
+    end
+end
+
+--[[ldx--
+<p>The next helper erases an element but keeps the table as it is,
+and since empty strings are not serialized (effectively) it does
+not harm. Copying the table would take more time. Usage:</p>
+--ldx]]--
+
+function xml.erase(dt,k)
+    if dt then
+        if k then
+            dt[k] = ""
+        else for k=1,#dt do
+            dt[1] = { "" }
+        end end
+    end
+end
+
+--[[ldx--
+<p>The next helper assigns a tree (or string). Usage:</p>
+
+<typing>
+dt[k] = xml.assign(root) or xml.assign(dt,k,root)
+</typing>
+--ldx]]--
+
+function xml.assign(dt,k,root)
+    if dt and k then
+        dt[k] = (type(root) == "table" and xml.body(root)) or root
+        return dt[k]
+    else
+        return xml.body(root)
+    end
+end
+
+-- the following helpers may move
+
+--[[ldx--
+<p>The next helper assigns a tree (or string). Usage:</p>
+<typing>
+xml.tocdata(e)
+xml.tocdata(e,"error")
+</typing>
+--ldx]]--
+
+function xml.tocdata(e,wrapper)
+    local whatever = xmltostring(e.dt)
+    if wrapper then
+        whatever = format("<%s>%s</%s>",wrapper,whatever,wrapper)
+    end
+    local t = { special = true, ns = "", tg = "@cd@", at = {}, rn = "", dt = { whatever }, __p__ = e }
+    setmetatable(t,getmetatable(e))
+    e.dt = { t }
+end
+
+function xml.makestandalone(root)
+    if root.ri then
+        local dt = root.dt
+        for k=1,#dt do
+            local v = dt[k]
+            if type(v) == "table" and v.special and v.tg == "@pi@" then
+                local txt = v.dt[1]
+                if find(txt,"xml.*version=") then
+                    v.dt[1] = txt .. " standalone='yes'"
+                    break
+                end
+            end
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['lxml-pth'] = {
+    version   = 1.001,
+    comment   = "this module is the basis for the lxml-* ones",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- e.ni is only valid after a filter run
+
+local concat, remove, insert = table.concat, table.remove, table.insert
+local type, next, tonumber, tostring, setmetatable, loadstring = type, next, tonumber, tostring, setmetatable, loadstring
+local format, upper, lower, gmatch, gsub, find, rep = string.format, string.upper, string.lower, string.gmatch, string.gsub, string.find, string.rep
+local lpegmatch = lpeg.match
+
+-- beware, this is not xpath ... e.g. position is different (currently) and
+-- we have reverse-sibling as reversed preceding sibling
+
+--[[ldx--
+<p>This module can be used stand alone but also inside <l n='mkiv'/> in
+which case it hooks into the tracker code. Therefore we provide a few
+functions that set the tracers. Here we overload a previously defined
+function.</p>
+<p>If I can get in the mood I will make a variant that is XSLT compliant
+but I wonder if it makes sense.</P>
+--ldx]]--
+
+--[[ldx--
+<p>Expecially the lpath code is experimental, we will support some of xpath, but
+only things that make sense for us; as compensation it is possible to hook in your
+own functions. Apart from preprocessing content for <l n='context'/> we also need
+this module for process management, like handling <l n='ctx'/> and <l n='rlx'/>
+files.</p>
+
+<typing>
+a/b/c /*/c
+a/b/c/first() a/b/c/last() a/b/c/index(n) a/b/c/index(-n)
+a/b/c/text() a/b/c/text(1) a/b/c/text(-1) a/b/c/text(n)
+</typing>
+--ldx]]--
+
+local trace_lpath    = false  if trackers then trackers.register("xml.path",    function(v) trace_lpath  = v end) end
+local trace_lparse   = false  if trackers then trackers.register("xml.parse",   function(v) trace_lparse = v end) end
+local trace_lprofile = false  if trackers then trackers.register("xml.profile", function(v) trace_lpath  = v trace_lparse = v trace_lprofile = v end) end
+
+--[[ldx--
+<p>We've now arrived at an interesting part: accessing the tree using a subset
+of <l n='xpath'/> and since we're not compatible we call it <l n='lpath'/>. We
+will explain more about its usage in other documents.</p>
+--ldx]]--
+
+local lpathcalls  = 0  function xml.lpathcalls () return lpathcalls  end
+local lpathcached = 0  function xml.lpathcached() return lpathcached end
+
+xml.functions      = xml.functions      or { } -- internal
+xml.expressions    = xml.expressions    or { } -- in expressions
+xml.finalizers     = xml.finalizers     or { } -- fast do-with ... (with return value other than collection)
+xml.specialhandler = xml.specialhandler or { }
+
+local functions   = xml.functions
+local expressions = xml.expressions
+local finalizers  = xml.finalizers
+
+finalizers.xml = finalizers.xml or { }
+finalizers.tex = finalizers.tex or { }
+
+local function fallback (t, name)
+    local fn = finalizers[name]
+    if fn then
+        t[name] = fn
+    else
+        logs.report("xml","unknown sub finalizer '%s'",tostring(name))
+        fn = function() end
+    end
+    return fn
+end
+
+setmetatable(finalizers.xml, { __index = fallback })
+setmetatable(finalizers.tex, { __index = fallback })
+
+xml.defaultprotocol = "xml"
+
+-- as xsl does not follow xpath completely here we will also
+-- be more liberal especially with regards to the use of | and
+-- the rootpath:
+--
+-- test    : all 'test' under current
+-- /test   : 'test' relative to current
+-- a|b|c   : set of names
+-- (a|b|c) : idem
+-- !       : not
+--
+-- after all, we're not doing transformations but filtering. in
+-- addition we provide filter functions (last bit)
+--
+-- todo: optimizer
+--
+-- .. : parent
+-- *  : all kids
+-- /  : anchor here
+-- // : /**/
+-- ** : all in between
+--
+-- so far we had (more practical as we don't transform)
+--
+-- {/test}   : kids 'test' under current node
+-- {test}    : any kid with tag 'test'
+-- {//test}  : same as above
+
+-- evaluator (needs to be redone, for the moment copied)
+
+-- todo: apply_axis(list,notable) and collection vs single
+
+local apply_axis = { }
+
+apply_axis['root'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        local rt = ll
+        while ll do
+            ll = ll.__p__
+            if ll then
+                rt = ll
+            end
+        end
+        collected[#collected+1] = rt
+    end
+    return collected
+end
+
+apply_axis['self'] = function(list)
+--~     local collected = { }
+--~     for l=1,#list do
+--~         collected[#collected+1] = list[l]
+--~     end
+--~     return collected
+    return list
+end
+
+apply_axis['child'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        local dt = ll.dt
+        local en = 0
+        for k=1,#dt do
+            local dk = dt[k]
+            if dk.tg then
+                collected[#collected+1] = dk
+                dk.ni = k -- refresh
+            en = en + 1
+            dk.ei = en
+            end
+        end
+        ll.en = en
+    end
+    return collected
+end
+
+local function collect(list,collected)
+    local dt = list.dt
+    if dt then
+        local en = 0
+        for k=1,#dt do
+            local dk = dt[k]
+            if dk.tg then
+                collected[#collected+1] = dk
+                dk.ni = k -- refresh
+                en = en + 1
+                dk.ei = en
+                collect(dk,collected)
+            end
+        end
+        list.en = en
+    end
+end
+apply_axis['descendant'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        collect(list[l],collected)
+    end
+    return collected
+end
+
+local function collect(list,collected)
+    local dt = list.dt
+    if dt then
+        local en = 0
+        for k=1,#dt do
+            local dk = dt[k]
+            if dk.tg then
+                collected[#collected+1] = dk
+                dk.ni = k -- refresh
+                en = en + 1
+                dk.ei = en
+                collect(dk,collected)
+            end
+        end
+        list.en = en
+    end
+end
+apply_axis['descendant-or-self'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        if ll.special ~= true then -- catch double root
+            collected[#collected+1] = ll
+        end
+        collect(ll,collected)
+    end
+    return collected
+end
+
+apply_axis['ancestor'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        while ll do
+            ll = ll.__p__
+            if ll then
+                collected[#collected+1] = ll
+            end
+        end
+    end
+    return collected
+end
+
+apply_axis['ancestor-or-self'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        collected[#collected+1] = ll
+        while ll do
+            ll = ll.__p__
+            if ll then
+                collected[#collected+1] = ll
+            end
+        end
+    end
+    return collected
+end
+
+apply_axis['parent'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local pl = list[l].__p__
+        if pl then
+            collected[#collected+1] = pl
+        end
+    end
+    return collected
+end
+
+apply_axis['attribute'] = function(list)
+    return { }
+end
+
+apply_axis['namespace'] = function(list)
+    return { }
+end
+
+apply_axis['following'] = function(list) -- incomplete
+--~     local collected = { }
+--~     for l=1,#list do
+--~         local ll = list[l]
+--~         local p = ll.__p__
+--~         local d = p.dt
+--~         for i=ll.ni+1,#d do
+--~             local di = d[i]
+--~             if type(di) == "table" then
+--~                 collected[#collected+1] = di
+--~                 break
+--~             end
+--~         end
+--~     end
+--~     return collected
+    return { }
+end
+
+apply_axis['preceding'] = function(list) -- incomplete
+--~     local collected = { }
+--~     for l=1,#list do
+--~         local ll = list[l]
+--~         local p = ll.__p__
+--~         local d = p.dt
+--~         for i=ll.ni-1,1,-1 do
+--~             local di = d[i]
+--~             if type(di) == "table" then
+--~                 collected[#collected+1] = di
+--~                 break
+--~             end
+--~         end
+--~     end
+--~     return collected
+    return { }
+end
+
+apply_axis['following-sibling'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        local p = ll.__p__
+        local d = p.dt
+        for i=ll.ni+1,#d do
+            local di = d[i]
+            if type(di) == "table" then
+                collected[#collected+1] = di
+            end
+        end
+    end
+    return collected
+end
+
+apply_axis['preceding-sibling'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        local p = ll.__p__
+        local d = p.dt
+        for i=1,ll.ni-1 do
+            local di = d[i]
+            if type(di) == "table" then
+                collected[#collected+1] = di
+            end
+        end
+    end
+    return collected
+end
+
+apply_axis['reverse-sibling'] = function(list) -- reverse preceding
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        local p = ll.__p__
+        local d = p.dt
+        for i=ll.ni-1,1,-1 do
+            local di = d[i]
+            if type(di) == "table" then
+                collected[#collected+1] = di
+            end
+        end
+    end
+    return collected
+end
+
+apply_axis['auto-descendant-or-self'] = apply_axis['descendant-or-self']
+apply_axis['auto-descendant']         = apply_axis['descendant']
+apply_axis['auto-child']              = apply_axis['child']
+apply_axis['auto-self']               = apply_axis['self']
+apply_axis['initial-child']           = apply_axis['child']
+
+local function apply_nodes(list,directive,nodes)
+    -- todo: nodes[1] etc ... negated node name in set ... when needed
+    -- ... currently ignored
+    local maxn = #nodes
+    if maxn == 3 then --optimized loop
+        local nns, ntg = nodes[2], nodes[3]
+        if not nns and not ntg then -- wildcard
+            if directive then
+                return list
+            else
+                return { }
+            end
+        else
+            local collected, m, p = { }, 0, nil
+            if not nns then -- only check tag
+                for l=1,#list do
+                    local ll = list[l]
+                    local ltg = ll.tg
+                    if ltg then
+                        if directive then
+                            if ntg == ltg then
+                                local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                                collected[#collected+1], ll.mi = ll, m
+                            end
+                        elseif ntg ~= ltg then
+                            local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                            collected[#collected+1], ll.mi = ll, m
+                        end
+                    end
+                end
+            elseif not ntg then -- only check namespace
+                for l=1,#list do
+                    local ll = list[l]
+                    local lns = ll.rn or ll.ns
+                    if lns then
+                        if directive then
+                            if lns == nns then
+                                local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                                collected[#collected+1], ll.mi = ll, m
+                            end
+                        elseif lns ~= nns then
+                            local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                            collected[#collected+1], ll.mi = ll, m
+                        end
+                    end
+                end
+            else -- check both
+                for l=1,#list do
+                    local ll = list[l]
+                    local ltg = ll.tg
+                    if ltg then
+                        local lns = ll.rn or ll.ns
+                        local ok = ltg == ntg and lns == nns
+                        if directive then
+                            if ok then
+                                local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                                collected[#collected+1], ll.mi = ll, m
+                            end
+                        elseif not ok then
+                            local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                            collected[#collected+1], ll.mi = ll, m
+                        end
+                    end
+                end
+            end
+            return collected
+        end
+    else
+        local collected, m, p = { }, 0, nil
+        for l=1,#list do
+            local ll = list[l]
+            local ltg = ll.tg
+            if ltg then
+                local lns = ll.rn or ll.ns
+                local ok = false
+                for n=1,maxn,3 do
+                    local nns, ntg = nodes[n+1], nodes[n+2]
+                    ok = (not ntg or ltg == ntg) and (not nns or lns == nns)
+                    if ok then
+                        break
+                    end
+                end
+                if directive then
+                    if ok then
+                        local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                        collected[#collected+1], ll.mi = ll, m
+                    end
+                elseif not ok then
+                    local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                    collected[#collected+1], ll.mi = ll, m
+                end
+            end
+        end
+        return collected
+    end
+end
+
+local quit_expression = false
+
+local function apply_expression(list,expression,order)
+    local collected = { }
+    quit_expression = false
+    for l=1,#list do
+        local ll = list[l]
+        if expression(list,ll,l,order) then -- nasty, order alleen valid als n=1
+            collected[#collected+1] = ll
+        end
+        if quit_expression then
+            break
+        end
+    end
+    return collected
+end
+
+local P, V, C, Cs, Cc, Ct, R, S, Cg, Cb = lpeg.P, lpeg.V, lpeg.C, lpeg.Cs, lpeg.Cc, lpeg.Ct, lpeg.R, lpeg.S, lpeg.Cg, lpeg.Cb
+
+local spaces     = S(" \n\r\t\f")^0
+local lp_space   = S(" \n\r\t\f")
+local lp_any     = P(1)
+local lp_noequal = P("!=") / "~=" + P("<=") + P(">=") + P("==")
+local lp_doequal = P("=")  / "=="
+local lp_or      = P("|")  / " or "
+local lp_and     = P("&")  / " and "
+
+local lp_builtin = P (
+        P("firstindex")   / "1" +
+        P("lastindex")    / "(#ll.__p__.dt or 1)" +
+        P("firstelement") / "1" +
+        P("lastelement")  / "(ll.__p__.en or 1)" +
+        P("first")        / "1" +
+        P("last")         / "#list" +
+        P("rootposition") / "order" +
+        P("position")     / "l" + -- is element in finalizer
+        P("order")        / "order" +
+        P("element")      / "(ll.ei or 1)" +
+        P("index")        / "(ll.ni or 1)" +
+        P("match")        / "(ll.mi or 1)" +
+        P("text")         / "(ll.dt[1] or '')" +
+    --  P("name")         / "(ll.ns~='' and ll.ns..':'..ll.tg)" +
+        P("name")         / "((ll.ns~='' and ll.ns..':'..ll.tg) or ll.tg)" +
+        P("tag")          / "ll.tg" +
+        P("ns")           / "ll.ns"
+    ) * ((spaces * P("(") * spaces * P(")"))/"")
+
+local lp_attribute = (P("@") + P("attribute::")) / "" * Cc("(ll.at and ll.at['") * R("az","AZ","--","__")^1 * Cc("'])")
+local lp_fastpos_p = ((P("+")^0 * R("09")^1 * P(-1)) / function(s) return "l==" .. s end)
+local lp_fastpos_n = ((P("-")   * R("09")^1 * P(-1)) / function(s) return "(" .. s .. "<0 and (#list+".. s .. "==l))" end)
+local lp_fastpos   = lp_fastpos_n + lp_fastpos_p
+local lp_reserved  = C("and") + C("or") + C("not") + C("div") + C("mod") + C("true") + C("false")
+
+local lp_lua_function  = C(R("az","AZ","__")^1 * (P(".") * R("az","AZ","__")^1)^1) * ("(") / function(t) -- todo: better . handling
+    return t .. "("
+end
+
+local lp_function  = C(R("az","AZ","__")^1) * P("(") / function(t) -- todo: better . handling
+    if expressions[t] then
+        return "expr." .. t .. "("
+    else
+        return "expr.error("
+    end
+end
+
+local lparent  = lpeg.P("(")
+local rparent  = lpeg.P(")")
+local noparent = 1 - (lparent+rparent)
+local nested   = lpeg.P{lparent * (noparent + lpeg.V(1))^0 * rparent}
+local value    = lpeg.P(lparent * lpeg.C((noparent + nested)^0) * rparent) -- lpeg.P{"("*C(((1-S("()"))+V(1))^0)*")"}
+
+local lp_child   = Cc("expr.child(ll,'") * R("az","AZ","--","__")^1 * Cc("')")
+local lp_number  = S("+-") * R("09")^1
+local lp_string  = Cc("'") * R("az","AZ","--","__")^1 * Cc("'")
+local lp_content = (P("'") * (1-P("'"))^0 * P("'") + P('"') * (1-P('"'))^0 * P('"'))
+
+local cleaner
+
+local lp_special = (C(P("name")+P("text")+P("tag")+P("count")+P("child"))) * value / function(t,s)
+    if expressions[t] then
+        s = s and s ~= "" and lpegmatch(cleaner,s)
+        if s and s ~= "" then
+            return "expr." .. t .. "(ll," .. s ..")"
+        else
+            return "expr." .. t .. "(ll)"
+        end
+    else
+        return "expr.error(" .. t .. ")"
+    end
+end
+
+local content =
+    lp_builtin +
+    lp_attribute +
+    lp_special +
+    lp_noequal + lp_doequal +
+    lp_or + lp_and +
+    lp_reserved +
+    lp_lua_function + lp_function +
+    lp_content + -- too fragile
+    lp_child +
+    lp_any
+
+local converter = Cs (
+    lp_fastpos + (P { lparent * (V(1))^0 * rparent + content } )^0
+)
+
+cleaner = Cs ( (
+--~     lp_fastpos +
+    lp_reserved +
+    lp_number +
+    lp_string +
+1 )^1 )
+
+
+--~ expr
+
+local template_e = [[
+    local expr = xml.expressions
+    return function(list,ll,l,order)
+        return %s
+    end
+]]
+
+local template_f_y = [[
+    local finalizer = xml.finalizers['%s']['%s']
+    return function(collection)
+        return finalizer(collection,%s)
+    end
+]]
+
+local template_f_n = [[
+    return xml.finalizers['%s']['%s']
+]]
+
+--
+
+local register_self                    = { kind = "axis", axis = "self"                    } -- , apply = apply_axis["self"]               }
+local register_parent                  = { kind = "axis", axis = "parent"                  } -- , apply = apply_axis["parent"]             }
+local register_descendant              = { kind = "axis", axis = "descendant"              } -- , apply = apply_axis["descendant"]         }
+local register_child                   = { kind = "axis", axis = "child"                   } -- , apply = apply_axis["child"]              }
+local register_descendant_or_self      = { kind = "axis", axis = "descendant-or-self"      } -- , apply = apply_axis["descendant-or-self"] }
+local register_root                    = { kind = "axis", axis = "root"                    } -- , apply = apply_axis["root"]               }
+local register_ancestor                = { kind = "axis", axis = "ancestor"                } -- , apply = apply_axis["ancestor"]           }
+local register_ancestor_or_self        = { kind = "axis", axis = "ancestor-or-self"        } -- , apply = apply_axis["ancestor-or-self"]   }
+local register_attribute               = { kind = "axis", axis = "attribute"               } -- , apply = apply_axis["attribute"]          }
+local register_namespace               = { kind = "axis", axis = "namespace"               } -- , apply = apply_axis["namespace"]          }
+local register_following               = { kind = "axis", axis = "following"               } -- , apply = apply_axis["following"]          }
+local register_following_sibling       = { kind = "axis", axis = "following-sibling"       } -- , apply = apply_axis["following-sibling"]  }
+local register_preceding               = { kind = "axis", axis = "preceding"               } -- , apply = apply_axis["preceding"]          }
+local register_preceding_sibling       = { kind = "axis", axis = "preceding-sibling"       } -- , apply = apply_axis["preceding-sibling"]  }
+local register_reverse_sibling         = { kind = "axis", axis = "reverse-sibling"         } -- , apply = apply_axis["reverse-sibling"]    }
+
+local register_auto_descendant_or_self = { kind = "axis", axis = "auto-descendant-or-self" } -- , apply = apply_axis["auto-descendant-or-self"] }
+local register_auto_descendant         = { kind = "axis", axis = "auto-descendant"         } -- , apply = apply_axis["auto-descendant"] }
+local register_auto_self               = { kind = "axis", axis = "auto-self"               } -- , apply = apply_axis["auto-self"] }
+local register_auto_child              = { kind = "axis", axis = "auto-child"              } -- , apply = apply_axis["auto-child"] }
+
+local register_initial_child           = { kind = "axis", axis = "initial-child"           } -- , apply = apply_axis["initial-child"] }
+
+local register_all_nodes               = { kind = "nodes", nodetest = true, nodes = { true, false, false } }
+
+local skip = { }
+
+local function errorrunner_e(str,cnv)
+    if not skip[str] then
+        logs.report("lpath","error in expression: %s => %s",str,cnv)
+        skip[str] = cnv or str
+    end
+    return false
+end
+local function errorrunner_f(str,arg)
+    logs.report("lpath","error in finalizer: %s(%s)",str,arg or "")
+    return false
+end
+
+local function register_nodes(nodetest,nodes)
+    return { kind = "nodes", nodetest = nodetest, nodes = nodes }
+end
+
+local function register_expression(expression)
+    local converted = lpegmatch(converter,expression)
+    local runner = loadstring(format(template_e,converted))
+    runner = (runner and runner()) or function() errorrunner_e(expression,converted) end
+    return { kind = "expression", expression = expression, converted = converted, evaluator = runner }
+end
+
+local function register_finalizer(protocol,name,arguments)
+    local runner
+    if arguments and arguments ~= "" then
+        runner = loadstring(format(template_f_y,protocol or xml.defaultprotocol,name,arguments))
+    else
+        runner = loadstring(format(template_f_n,protocol or xml.defaultprotocol,name))
+    end
+    runner = (runner and runner()) or function() errorrunner_f(name,arguments) end
+    return { kind = "finalizer", name = name, arguments = arguments, finalizer = runner }
+end
+
+local expression = P { "ex",
+    ex = "[" * C((V("sq") + V("dq") + (1 - S("[]")) + V("ex"))^0) * "]",
+    sq = "'" * (1 - S("'"))^0 * "'",
+    dq = '"' * (1 - S('"'))^0 * '"',
+}
+
+local arguments = P { "ar",
+    ar = "(" * Cs((V("sq") + V("dq") + V("nq") + P(1-P(")")))^0) * ")",
+    nq = ((1 - S("),'\""))^1) / function(s) return format("%q",s) end,
+    sq = P("'") * (1 - P("'"))^0 * P("'"),
+    dq = P('"') * (1 - P('"'))^0 * P('"'),
+}
+
+-- todo: better arg parser
+
+local function register_error(str)
+    return { kind = "error", error = format("unparsed: %s",str) }
+end
+
+-- there is a difference in * and /*/ and so we need to catch a few special cases
+
+local special_1 = P("*")  * Cc(register_auto_descendant) * Cc(register_all_nodes) -- last one not needed
+local special_2 = P("/")  * Cc(register_auto_self)
+local special_3 = P("")   * Cc(register_auto_self)
+
+local parser = Ct { "patterns", -- can be made a bit faster by moving pattern outside
+
+    patterns             = spaces * V("protocol") * spaces * (
+                              ( V("special") * spaces * P(-1)                                                         ) +
+                              ( V("initial") * spaces * V("step") * spaces * (P("/") * spaces * V("step") * spaces)^0 )
+                           ),
+
+    protocol             = Cg(V("letters"),"protocol") * P("://") + Cg(Cc(nil),"protocol"),
+
+ -- the / is needed for // as descendant or self is somewhat special
+ -- step                 = (V("shortcuts") + V("axis") * spaces * V("nodes")^0 + V("error")) * spaces * V("expressions")^0 * spaces * V("finalizer")^0,
+    step                 = ((V("shortcuts") + P("/") + V("axis")) * spaces * V("nodes")^0 + V("error")) * spaces * V("expressions")^0 * spaces * V("finalizer")^0,
+
+    axis                 = V("descendant") + V("child") + V("parent") + V("self") + V("root") + V("ancestor") +
+                           V("descendant_or_self") + V("following_sibling") + V("following") +
+                           V("reverse_sibling") + V("preceding_sibling") + V("preceding") + V("ancestor_or_self") +
+                           #(1-P(-1)) * Cc(register_auto_child),
+
+    special              = special_1 + special_2 + special_3,
+
+    initial              = (P("/") * spaces * Cc(register_initial_child))^-1,
+
+    error                = (P(1)^1) / register_error,
+
+    shortcuts_a          = V("s_descendant_or_self") + V("s_descendant") + V("s_child") + V("s_parent") + V("s_self") + V("s_root") + V("s_ancestor"),
+
+    shortcuts            = V("shortcuts_a") * (spaces * "/" * spaces * V("shortcuts_a"))^0,
+
+    s_descendant_or_self = (P("***/") + P("/"))  * Cc(register_descendant_or_self), --- *** is a bonus
+ -- s_descendant_or_self = P("/")                * Cc(register_descendant_or_self),
+    s_descendant         = P("**")               * Cc(register_descendant),
+    s_child              = P("*") * #(1-P(":"))  * Cc(register_child     ),
+--  s_child              = P("*") * #(P("/")+P(-1)) * Cc(register_child     ),
+    s_parent             = P("..")               * Cc(register_parent    ),
+    s_self               = P("." )               * Cc(register_self      ),
+    s_root               = P("^^")               * Cc(register_root      ),
+    s_ancestor           = P("^")                * Cc(register_ancestor  ),
+
+    descendant           = P("descendant::")         * Cc(register_descendant         ),
+    child                = P("child::")              * Cc(register_child              ),
+    parent               = P("parent::")             * Cc(register_parent             ),
+    self                 = P("self::")               * Cc(register_self               ),
+    root                 = P('root::')               * Cc(register_root               ),
+    ancestor             = P('ancestor::')           * Cc(register_ancestor           ),
+    descendant_or_self   = P('descendant-or-self::') * Cc(register_descendant_or_self ),
+    ancestor_or_self     = P('ancestor-or-self::')   * Cc(register_ancestor_or_self   ),
+ -- attribute            = P('attribute::')          * Cc(register_attribute          ),
+ -- namespace            = P('namespace::')          * Cc(register_namespace          ),
+    following            = P('following::')          * Cc(register_following          ),
+    following_sibling    = P('following-sibling::')  * Cc(register_following_sibling  ),
+    preceding            = P('preceding::')          * Cc(register_preceding          ),
+    preceding_sibling    = P('preceding-sibling::')  * Cc(register_preceding_sibling  ),
+    reverse_sibling      = P('reverse-sibling::')    * Cc(register_reverse_sibling    ),
+
+    nodes                = (V("nodefunction") * spaces * P("(") * V("nodeset") * P(")") + V("nodetest") * V("nodeset")) / register_nodes,
+
+    expressions          = expression / register_expression,
+
+    letters              = R("az")^1,
+    name                 = (1-lpeg.S("/[]()|:*!"))^1,
+    negate               = P("!") * Cc(false),
+
+    nodefunction         = V("negate") + P("not") * Cc(false) + Cc(true),
+    nodetest             = V("negate") + Cc(true),
+    nodename             = (V("negate") + Cc(true)) * spaces * ((V("wildnodename") * P(":") * V("wildnodename")) + (Cc(false) * V("wildnodename"))),
+    wildnodename         = (C(V("name")) + P("*") * Cc(false)) * #(1-P("(")),
+    nodeset              = spaces * Ct(V("nodename") * (spaces * P("|") * spaces * V("nodename"))^0) * spaces,
+
+    finalizer            = (Cb("protocol") * P("/")^-1 * C(V("name")) * arguments * P(-1)) / register_finalizer,
+
+}
+
+local cache = { }
+
+local function nodesettostring(set,nodetest)
+    local t = { }
+    for i=1,#set,3 do
+        local directive, ns, tg = set[i], set[i+1], set[i+2]
+        if not ns or ns == "" then ns = "*" end
+        if not tg or tg == "" then tg = "*" end
+        tg = (tg == "@rt@" and "[root]") or format("%s:%s",ns,tg)
+        t[#t+1] = (directive and tg) or format("not(%s)",tg)
+    end
+    if nodetest == false then
+        return format("not(%s)",concat(t,"|"))
+    else
+        return concat(t,"|")
+    end
+end
+
+local function tagstostring(list)
+    if #list == 0 then
+        return "no elements"
+    else
+        local t = { }
+        for i=1, #list do
+            local li = list[i]
+            local ns, tg = li.ns, li.tg
+            if not ns or ns == "" then ns = "*" end
+            if not tg or tg == "" then tg = "*" end
+            t[#t+1] = (tg == "@rt@" and "[root]") or format("%s:%s",ns,tg)
+        end
+        return concat(t," ")
+    end
+end
+
+xml.nodesettostring = nodesettostring
+
+local parse_pattern -- we have a harmless kind of circular reference
+
+local function lshow(parsed)
+    if type(parsed) == "string" then
+        parsed = parse_pattern(parsed)
+    end
+    local s = table.serialize_functions -- ugly
+    table.serialize_functions = false -- ugly
+    logs.report("lpath","%s://%s => %s",parsed.protocol or xml.defaultprotocol,parsed.pattern,table.serialize(parsed,false))
+    table.serialize_functions = s -- ugly
+end
+
+xml.lshow = lshow
+
+local function add_comment(p,str)
+    local pc = p.comment
+    if not pc then
+        p.comment = { str }
+    else
+        pc[#pc+1] = str
+    end
+end
+
+parse_pattern = function (pattern) -- the gain of caching is rather minimal
+    lpathcalls = lpathcalls + 1
+    if type(pattern) == "table" then
+        return pattern
+    else
+        local parsed = cache[pattern]
+        if parsed then
+            lpathcached = lpathcached + 1
+        else
+            parsed = lpegmatch(parser,pattern)
+            if parsed then
+                parsed.pattern = pattern
+                local np = #parsed
+                if np == 0 then
+                    parsed = { pattern = pattern, register_self, state = "parsing error" }
+                    logs.report("lpath","parsing error in '%s'",pattern)
+                    lshow(parsed)
+                else
+                    -- we could have done this with a more complex parser but this
+                    -- is cleaner
+                    local pi = parsed[1]
+                    if pi.axis == "auto-child" then
+                        if false then
+                            add_comment(parsed, "auto-child replaced by auto-descendant-or-self")
+                            parsed[1] = register_auto_descendant_or_self
+                        else
+                            add_comment(parsed, "auto-child replaced by auto-descendant")
+                            parsed[1] = register_auto_descendant
+                        end
+                    elseif pi.axis == "initial-child" and np > 1 and parsed[2].axis then
+                        add_comment(parsed, "initial-child removed") -- we could also make it a auto-self
+                        remove(parsed,1)
+                    end
+                    local np = #parsed -- can have changed
+                    if np > 1 then
+                        local pnp = parsed[np]
+                        if pnp.kind == "nodes" and pnp.nodetest == true then
+                            local nodes = pnp.nodes
+                            if nodes[1] == true and nodes[2] == false and nodes[3] == false then
+                                add_comment(parsed, "redundant final wildcard filter removed")
+                                remove(parsed,np)
+                            end
+                        end
+                    end
+                end
+            else
+                parsed = { pattern = pattern }
+            end
+            cache[pattern] = parsed
+            if trace_lparse and not trace_lprofile then
+                lshow(parsed)
+            end
+        end
+        return parsed
+    end
+end
+
+-- we can move all calls inline and then merge the trace back
+-- technically we can combine axis and the next nodes which is
+-- what we did before but this a bit cleaner (but slower too)
+-- but interesting is that it's not that much faster when we
+-- go inline
+--
+-- beware: we need to return a collection even when we filter
+-- else the (simple) cache gets messed up
+
+-- caching found lookups saves not that much (max .1 sec on a 8 sec run)
+-- and it also messes up finalizers
+
+-- watch out: when there is a finalizer, it's always called as there
+-- can be cases that a finalizer returns (or does) something in case
+-- there is no match; an example of this is count()
+
+local profiled = { }  xml.profiled = profiled
+
+local function profiled_apply(list,parsed,nofparsed,order)
+    local p = profiled[parsed.pattern]
+    if p then
+        p.tested = p.tested + 1
+    else
+        p = { tested = 1, matched = 0, finalized = 0 }
+        profiled[parsed.pattern] = p
+    end
+    local collected = list
+    for i=1,nofparsed do
+        local pi = parsed[i]
+        local kind = pi.kind
+        if kind == "axis" then
+            collected = apply_axis[pi.axis](collected)
+        elseif kind == "nodes" then
+            collected = apply_nodes(collected,pi.nodetest,pi.nodes)
+        elseif kind == "expression" then
+            collected = apply_expression(collected,pi.evaluator,order)
+        elseif kind == "finalizer" then
+            collected = pi.finalizer(collected)
+            p.matched = p.matched + 1
+            p.finalized = p.finalized + 1
+            return collected
+        end
+        if not collected or #collected == 0 then
+            local pn = i < nofparsed and parsed[nofparsed]
+            if pn and pn.kind == "finalizer" then
+                collected = pn.finalizer(collected)
+                p.finalized = p.finalized + 1
+                return collected
+            end
+            return nil
+        end
+    end
+    if collected then
+        p.matched = p.matched + 1
+    end
+    return collected
+end
+
+local function traced_apply(list,parsed,nofparsed,order)
+    if trace_lparse then
+        lshow(parsed)
+    end
+    logs.report("lpath", "collecting : %s",parsed.pattern)
+    logs.report("lpath", " root tags : %s",tagstostring(list))
+    logs.report("lpath", "     order : %s",order or "unset")
+    local collected = list
+    for i=1,nofparsed do
+        local pi = parsed[i]
+        local kind = pi.kind
+        if kind == "axis" then
+            collected = apply_axis[pi.axis](collected)
+            logs.report("lpath", "% 10i : ax : %s",(collected and #collected) or 0,pi.axis)
+        elseif kind == "nodes" then
+            collected = apply_nodes(collected,pi.nodetest,pi.nodes)
+            logs.report("lpath", "% 10i : ns : %s",(collected and #collected) or 0,nodesettostring(pi.nodes,pi.nodetest))
+        elseif kind == "expression" then
+            collected = apply_expression(collected,pi.evaluator,order)
+            logs.report("lpath", "% 10i : ex : %s -> %s",(collected and #collected) or 0,pi.expression,pi.converted)
+        elseif kind == "finalizer" then
+            collected = pi.finalizer(collected)
+            logs.report("lpath", "% 10i : fi : %s : %s(%s)",(type(collected) == "table" and #collected) or 0,parsed.protocol or xml.defaultprotocol,pi.name,pi.arguments or "")
+            return collected
+        end
+        if not collected or #collected == 0 then
+            local pn = i < nofparsed and parsed[nofparsed]
+            if pn and pn.kind == "finalizer" then
+                collected = pn.finalizer(collected)
+                logs.report("lpath", "% 10i : fi : %s : %s(%s)",(type(collected) == "table" and #collected) or 0,parsed.protocol or xml.defaultprotocol,pn.name,pn.arguments or "")
+                return collected
+            end
+            return nil
+        end
+    end
+    return collected
+end
+
+local function normal_apply(list,parsed,nofparsed,order)
+    local collected = list
+    for i=1,nofparsed do
+        local pi = parsed[i]
+        local kind = pi.kind
+        if kind == "axis" then
+            local axis = pi.axis
+            if axis ~= "self" then
+                collected = apply_axis[axis](collected)
+            end
+        elseif kind == "nodes" then
+            collected = apply_nodes(collected,pi.nodetest,pi.nodes)
+        elseif kind == "expression" then
+            collected = apply_expression(collected,pi.evaluator,order)
+        elseif kind == "finalizer" then
+            return pi.finalizer(collected)
+        end
+        if not collected or #collected == 0 then
+            local pf = i < nofparsed and parsed[nofparsed].finalizer
+            if pf then
+                return pf(collected) -- can be anything
+            end
+            return nil
+        end
+    end
+    return collected
+end
+
+local function parse_apply(list,pattern)
+    -- we avoid an extra call
+    local parsed = cache[pattern]
+    if parsed then
+        lpathcalls = lpathcalls + 1
+        lpathcached = lpathcached + 1
+    elseif type(pattern) == "table" then
+        lpathcalls = lpathcalls + 1
+        parsed = pattern
+    else
+        parsed = parse_pattern(pattern) or pattern
+    end
+    if not parsed then
+        return
+    end
+    local nofparsed = #parsed
+    if nofparsed == 0 then
+        return -- something is wrong
+    end
+    local one = list[1]
+    if not one then
+        return -- something is wrong
+    elseif not trace_lpath then
+        return normal_apply(list,parsed,nofparsed,one.mi)
+    elseif trace_lprofile then
+        return profiled_apply(list,parsed,nofparsed,one.mi)
+    else
+        return traced_apply(list,parsed,nofparsed,one.mi)
+    end
+end
+
+-- internal (parsed)
+
+expressions.child = function(e,pattern)
+    return parse_apply({ e },pattern) -- todo: cache
+end
+expressions.count = function(e,pattern)
+    local collected = parse_apply({ e },pattern) -- todo: cache
+    return (collected and #collected) or 0
+end
+
+-- external
+
+expressions.oneof = function(s,...) -- slow
+    local t = {...} for i=1,#t do if s == t[i] then return true end end return false
+end
+expressions.error = function(str)
+    xml.error_handler("unknown function in lpath expression",tostring(str or "?"))
+    return false
+end
+expressions.undefined = function(s)
+    return s == nil
+end
+
+expressions.quit = function(s)
+    if s or s == nil then
+        quit_expression = true
+    end
+    return true
+end
+
+expressions.print = function(...)
+    print(...)
+    return true
+end
+
+expressions.contains  = find
+expressions.find      = find
+expressions.upper     = upper
+expressions.lower     = lower
+expressions.number    = tonumber
+expressions.boolean   = toboolean
+
+-- user interface
+
+local function traverse(root,pattern,handle)
+    logs.report("xml","use 'xml.selection' instead for '%s'",pattern)
+    local collected = parse_apply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            local r = e.__p__
+            handle(r,r.dt,e.ni)
+        end
+    end
+end
+
+local function selection(root,pattern,handle)
+    local collected = parse_apply({ root },pattern)
+    if collected then
+        if handle then
+            for c=1,#collected do
+                handle(collected[c])
+            end
+        else
+            return collected
+        end
+    end
+end
+
+xml.parse_parser  = parser
+xml.parse_pattern = parse_pattern
+xml.parse_apply   = parse_apply
+xml.traverse      = traverse           -- old method, r, d, k
+xml.selection     = selection          -- new method, simple handle
+
+local lpath = parse_pattern
+
+xml.lpath = lpath
+
+function xml.cached_patterns()
+    return cache
+end
+
+-- generic function finalizer (independant namespace)
+
+local function dofunction(collected,fnc)
+    if collected then
+        local f = functions[fnc]
+        if f then
+            for c=1,#collected do
+                f(collected[c])
+            end
+        else
+            logs.report("xml","unknown function '%s'",fnc)
+        end
+    end
+end
+
+xml.finalizers.xml["function"] = dofunction
+xml.finalizers.tex["function"] = dofunction
+
+-- functions
+
+expressions.text = function(e,n)
+    local rdt = e.__p__.dt
+    return (rdt and rdt[n]) or ""
+end
+
+expressions.name = function(e,n) -- ns + tg
+    local found = false
+    n = tonumber(n) or 0
+    if n == 0 then
+        found = type(e) == "table" and e
+    elseif n < 0 then
+        local d, k = e.__p__.dt, e.ni
+        for i=k-1,1,-1 do
+            local di = d[i]
+            if type(di) == "table" then
+                if n == -1 then
+                    found = di
+                    break
+                else
+                    n = n + 1
+                end
+            end
+        end
+    else
+        local d, k = e.__p__.dt, e.ni
+        for i=k+1,#d,1 do
+            local di = d[i]
+            if type(di) == "table" then
+                if n == 1 then
+                    found = di
+                    break
+                else
+                    n = n - 1
+                end
+            end
+        end
+    end
+    if found then
+        local ns, tg = found.rn or found.ns or "", found.tg
+        if ns ~= "" then
+            return ns .. ":" .. tg
+        else
+            return tg
+        end
+    else
+        return ""
+    end
+end
+
+expressions.tag = function(e,n) -- only tg
+    if not e then
+        return ""
+    else
+        local found = false
+        n = tonumber(n) or 0
+        if n == 0 then
+            found = (type(e) == "table") and e -- seems to fail
+        elseif n < 0 then
+            local d, k = e.__p__.dt, e.ni
+            for i=k-1,1,-1 do
+                local di = d[i]
+                if type(di) == "table" then
+                    if n == -1 then
+                        found = di
+                        break
+                    else
+                        n = n + 1
+                    end
+                end
+            end
+        else
+            local d, k = e.__p__.dt, e.ni
+            for i=k+1,#d,1 do
+                local di = d[i]
+                if type(di) == "table" then
+                    if n == 1 then
+                        found = di
+                        break
+                    else
+                        n = n - 1
+                    end
+                end
+            end
+        end
+        return (found and found.tg) or ""
+    end
+end
+
+--[[ldx--
+<p>This is the main filter function. It returns whatever is asked for.</p>
+--ldx]]--
+
+function xml.filter(root,pattern) -- no longer funny attribute handling here
+    return parse_apply({ root },pattern)
+end
+
+--[[ldx--
+<p>Often using an iterators looks nicer in the code than passing handler
+functions. The <l n='lua'/> book describes how to use coroutines for that
+purpose (<url href='http://www.lua.org/pil/9.3.html'/>). This permits
+code like:</p>
+
+<typing>
+for r, d, k in xml.elements(xml.load('text.xml'),"title") do
+    print(d[k]) -- old method
+end
+for e in xml.collected(xml.load('text.xml'),"title") do
+    print(e) -- new one
+end
+</typing>
+--ldx]]--
+
+local wrap, yield = coroutine.wrap, coroutine.yield
+
+function xml.elements(root,pattern,reverse) -- r, d, k
+    local collected = parse_apply({ root },pattern)
+    if collected then
+        if reverse then
+            return wrap(function() for c=#collected,1,-1 do
+                local e = collected[c] local r = e.__p__ yield(r,r.dt,e.ni)
+            end end)
+        else
+            return wrap(function() for c=1,#collected    do
+                local e = collected[c] local r = e.__p__ yield(r,r.dt,e.ni)
+            end end)
+        end
+    end
+    return wrap(function() end)
+end
+
+function xml.collected(root,pattern,reverse) -- e
+    local collected = parse_apply({ root },pattern)
+    if collected then
+        if reverse then
+            return wrap(function() for c=#collected,1,-1 do yield(collected[c]) end end)
+        else
+            return wrap(function() for c=1,#collected    do yield(collected[c]) end end)
+        end
+    end
+    return wrap(function() end)
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['lxml-mis'] = {
+    version   = 1.001,
+    comment   = "this module is the basis for the lxml-* ones",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local concat = table.concat
+local type, next, tonumber, tostring, setmetatable, loadstring = type, next, tonumber, tostring, setmetatable, loadstring
+local format, gsub, match = string.format, string.gsub, string.match
+local lpegmatch = lpeg.match
+
+--[[ldx--
+<p>The following helper functions best belong to the <t>lxml-ini</t>
+module. Some are here because we need then in the <t>mk</t>
+document and other manuals, others came up when playing with
+this module. Since this module is also used in <l n='mtxrun'/> we've
+put them here instead of loading mode modules there then needed.</p>
+--ldx]]--
+
+local function xmlgsub(t,old,new) -- will be replaced
+    local dt = t.dt
+    if dt then
+        for k=1,#dt do
+            local v = dt[k]
+            if type(v) == "string" then
+                dt[k] = gsub(v,old,new)
+            else
+                xmlgsub(v,old,new)
+            end
+        end
+    end
+end
+
+--~ xml.gsub = xmlgsub
+
+function xml.strip_leading_spaces(dk,d,k) -- cosmetic, for manual
+    if d and k then
+        local dkm = d[k-1]
+        if dkm and type(dkm) == "string" then
+            local s = match(dkm,"\n(%s+)")
+            xmlgsub(dk,"\n"..rep(" ",#s),"\n")
+        end
+    end
+end
+
+--~ xml.escapes   = { ['&'] = '&amp;', ['<'] = '&lt;', ['>'] = '&gt;', ['"'] = '&quot;' }
+--~ xml.unescapes = { } for k,v in next, xml.escapes do xml.unescapes[v] = k end
+
+--~ function xml.escaped  (str) return (gsub(str,"(.)"   , xml.escapes  )) end
+--~ function xml.unescaped(str) return (gsub(str,"(&.-;)", xml.unescapes)) end
+--~ function xml.cleansed (str) return (gsub(str,"<.->"  , ''           )) end -- "%b<>"
+
+local P, S, R, C, V, Cc, Cs = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.V, lpeg.Cc, lpeg.Cs
+
+-- 100 * 2500 * "oeps< oeps> oeps&" : gsub:lpeg|lpeg|lpeg
+--
+-- 1021:0335:0287:0247
+
+-- 10 * 1000 * "oeps< oeps> oeps& asfjhalskfjh alskfjh alskfjh alskfjh ;al J;LSFDJ"
+--
+-- 1559:0257:0288:0190 (last one suggested by roberto)
+
+--    escaped = Cs((S("<&>") / xml.escapes + 1)^0)
+--    escaped = Cs((S("<")/"&lt;" + S(">")/"&gt;" + S("&")/"&amp;" + 1)^0)
+local normal  = (1 - S("<&>"))^0
+local special = P("<")/"&lt;" + P(">")/"&gt;" + P("&")/"&amp;"
+local escaped = Cs(normal * (special * normal)^0)
+
+-- 100 * 1000 * "oeps&lt; oeps&gt; oeps&amp;" : gsub:lpeg == 0153:0280:0151:0080 (last one by roberto)
+
+local normal    = (1 - S"&")^0
+local special   = P("&lt;")/"<" + P("&gt;")/">" + P("&amp;")/"&"
+local unescaped = Cs(normal * (special * normal)^0)
+
+-- 100 * 5000 * "oeps <oeps bla='oeps' foo='bar'> oeps </oeps> oeps " : gsub:lpeg == 623:501 msec (short tags, less difference)
+
+local cleansed = Cs(((P("<") * (1-P(">"))^0 * P(">"))/"" + 1)^0)
+
+xml.escaped_pattern   = escaped
+xml.unescaped_pattern = unescaped
+xml.cleansed_pattern  = cleansed
+
+function xml.escaped  (str) return lpegmatch(escaped,str)   end
+function xml.unescaped(str) return lpegmatch(unescaped,str) end
+function xml.cleansed (str) return lpegmatch(cleansed,str)  end
+
+-- this might move
+
+function xml.fillin(root,pattern,str,check)
+    local e = xml.first(root,pattern)
+    if e then
+        local n = #e.dt
+        if not check or n == 0 or (n == 1 and e.dt[1] == "") then
+            e.dt = { str }
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['lxml-aux'] = {
+    version   = 1.001,
+    comment   = "this module is the basis for the lxml-* ones",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- not all functions here make sense anymore vbut we keep them for
+-- compatibility reasons
+
+local trace_manipulations = false  trackers.register("lxml.manipulations", function(v) trace_manipulations = v end)
+
+local xmlparseapply, xmlconvert, xmlcopy, xmlname = xml.parse_apply, xml.convert, xml.copy, xml.name
+local xmlinheritedconvert = xml.inheritedconvert
+
+local type = type
+local insert, remove = table.insert, table.remove
+local gmatch, gsub = string.gmatch, string.gsub
+
+local function report(what,pattern,c,e)
+    logs.report("xml","%s element '%s' (root: '%s', position: %s, index: %s, pattern: %s)",what,xmlname(e),xmlname(e.__p__),c,e.ni,pattern)
+end
+
+local function withelements(e,handle,depth)
+    if e and handle then
+        local edt = e.dt
+        if edt then
+            depth = depth or 0
+            for i=1,#edt do
+                local e = edt[i]
+                if type(e) == "table" then
+                    handle(e,depth)
+                    withelements(e,handle,depth+1)
+                end
+            end
+        end
+    end
+end
+
+xml.withelements = withelements
+
+function xml.withelement(e,n,handle) -- slow
+    if e and n ~= 0 and handle then
+        local edt = e.dt
+        if edt then
+            if n > 0 then
+                for i=1,#edt do
+                    local ei = edt[i]
+                    if type(ei) == "table" then
+                        if n == 1 then
+                            handle(ei)
+                            return
+                        else
+                            n = n - 1
+                        end
+                    end
+                end
+            elseif n < 0 then
+                for i=#edt,1,-1 do
+                    local ei = edt[i]
+                    if type(ei) == "table" then
+                        if n == -1 then
+                            handle(ei)
+                            return
+                        else
+                            n = n + 1
+                        end
+                    end
+                end
+            end
+        end
+    end
+end
+
+xml.elements_only = xml.collected
+
+function xml.each_element(root,pattern,handle,reverse)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        if reverse then
+            for c=#collected,1,-1 do
+                handle(collected[c])
+            end
+        else
+            for c=1,#collected do
+                handle(collected[c])
+            end
+        end
+        return collected
+    end
+end
+
+xml.process_elements = xml.each_element
+
+function xml.process_attributes(root,pattern,handle)
+    local collected = xmlparseapply({ root },pattern)
+    if collected and handle then
+        for c=1,#collected do
+            handle(collected[c].at)
+        end
+    end
+    return collected
+end
+
+--[[ldx--
+<p>The following functions collect elements and texts.</p>
+--ldx]]--
+
+-- are these still needed -> lxml-cmp.lua
+
+function xml.collect_elements(root, pattern)
+    return xmlparseapply({ root },pattern)
+end
+
+function xml.collect_texts(root, pattern, flatten) -- todo: variant with handle
+    local collected = xmlparseapply({ root },pattern)
+    if collected and flatten then
+        local xmltostring = xml.tostring
+        for c=1,#collected do
+            collected[c] = xmltostring(collected[c].dt)
+        end
+    end
+    return collected or { }
+end
+
+function xml.collect_tags(root, pattern, nonamespace)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        local t = { }
+        for c=1,#collected do
+            local e = collected[c]
+            local ns, tg = e.ns, e.tg
+            if nonamespace then
+                t[#t+1] = tg
+            elseif ns == "" then
+                t[#t+1] = tg
+            else
+                t[#t+1] = ns .. ":" .. tg
+            end
+        end
+        return t
+    end
+end
+
+--[[ldx--
+<p>We've now arrived at the functions that manipulate the tree.</p>
+--ldx]]--
+
+local no_root = { no_root = true }
+
+function xml.redo_ni(d)
+    for k=1,#d do
+        local dk = d[k]
+        if type(dk) == "table" then
+            dk.ni = k
+        end
+    end
+end
+
+local function xmltoelement(whatever,root)
+    if not whatever then
+        return nil
+    end
+    local element
+    if type(whatever) == "string" then
+        element = xmlinheritedconvert(whatever,root)
+    else
+        element = whatever -- we assume a table
+    end
+    if element.error then
+        return whatever -- string
+    end
+    if element then
+    --~ if element.ri then
+    --~     element = element.dt[element.ri].dt
+    --~ else
+    --~     element = element.dt
+    --~ end
+    end
+    return element
+end
+
+xml.toelement = xmltoelement
+
+local function copiedelement(element,newparent)
+    if type(element) == "string" then
+        return element
+    else
+        element = xmlcopy(element).dt
+        if newparent and type(element) == "table" then
+            element.__p__ = newparent
+        end
+        return element
+    end
+end
+
+function xml.delete_element(root,pattern)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            local p = e.__p__
+            if p then
+                if trace_manipulations then
+                    report('deleting',pattern,c,e)
+                end
+                local d = p.dt
+                remove(d,e.ni)
+                xml.redo_ni(d) -- can be made faster and inlined
+            end
+        end
+    end
+end
+
+function xml.replace_element(root,pattern,whatever)
+    local element = root and xmltoelement(whatever,root)
+    local collected = element and xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            local p = e.__p__
+            if p then
+                if trace_manipulations then
+                    report('replacing',pattern,c,e)
+                end
+                local d = p.dt
+                d[e.ni] = copiedelement(element,p)
+                xml.redo_ni(d) -- probably not needed
+            end
+        end
+    end
+end
+
+local function inject_element(root,pattern,whatever,prepend)
+    local element = root and xmltoelement(whatever,root)
+    local collected = element and xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            local r = e.__p__
+            local d, k, rri = r.dt, e.ni, r.ri
+            local edt = (rri and d[rri].dt) or (d and d[k] and d[k].dt)
+            if edt then
+                local be, af
+                local cp = copiedelement(element,e)
+                if prepend then
+                    be, af = cp, edt
+                else
+                    be, af = edt, cp
+                end
+                for i=1,#af do
+                    be[#be+1] = af[i]
+                end
+                if rri then
+                    r.dt[rri].dt = be
+                else
+                    d[k].dt = be
+                end
+                xml.redo_ni(d)
+            end
+        end
+    end
+end
+
+local function insert_element(root,pattern,whatever,before) -- todo: element als functie
+    local element = root and xmltoelement(whatever,root)
+    local collected = element and xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            local r = e.__p__
+            local d, k = r.dt, e.ni
+            if not before then
+                k = k + 1
+            end
+            insert(d,k,copiedelement(element,r))
+            xml.redo_ni(d)
+        end
+    end
+end
+
+xml.insert_element        =                 insert_element
+xml.insert_element_after  =                 insert_element
+xml.insert_element_before = function(r,p,e) insert_element(r,p,e,true) end
+xml.inject_element        =                 inject_element
+xml.inject_element_after  =                 inject_element
+xml.inject_element_before = function(r,p,e) inject_element(r,p,e,true) end
+
+local function include(xmldata,pattern,attribute,recursive,loaddata)
+    -- parse="text" (default: xml), encoding="" (todo)
+    -- attribute = attribute or 'href'
+    pattern = pattern or 'include'
+    loaddata = loaddata or io.loaddata
+    local collected = xmlparseapply({ xmldata },pattern)
+    if collected then
+        for c=1,#collected do
+            local ek = collected[c]
+            local name = nil
+            local ekdt = ek.dt
+            local ekat = ek.at
+            local epdt = ek.__p__.dt
+            if not attribute or attribute == "" then
+                name = (type(ekdt) == "table" and ekdt[1]) or ekdt -- ckeck, probably always tab or str
+            end
+            if not name then
+                for a in gmatch(attribute or "href","([^|]+)") do
+                    name = ekat[a]
+                    if name then break end
+                end
+            end
+            local data = (name and name ~= "" and loaddata(name)) or ""
+            if data == "" then
+                epdt[ek.ni] = "" -- xml.empty(d,k)
+            elseif ekat["parse"] == "text" then
+                -- for the moment hard coded
+                epdt[ek.ni] = xml.escaped(data) -- d[k] = xml.escaped(data)
+            else
+--~                 local settings = xmldata.settings
+--~                 settings.parent_root = xmldata -- to be tested
+--~                 local xi = xmlconvert(data,settings)
+                local xi = xmlinheritedconvert(data,xmldata)
+                if not xi then
+                    epdt[ek.ni] = "" -- xml.empty(d,k)
+                else
+                    if recursive then
+                        include(xi,pattern,attribute,recursive,loaddata)
+                    end
+                    epdt[ek.ni] = xml.body(xi) -- xml.assign(d,k,xi)
+                end
+            end
+        end
+    end
+end
+
+xml.include = include
+
+--~ local function manipulate(xmldata,pattern,manipulator) -- untested and might go away
+--~     local collected = xmlparseapply({ xmldata },pattern)
+--~     if collected then
+--~         local xmltostring = xml.tostring
+--~         for c=1,#collected do
+--~             local e = collected[c]
+--~             local data = manipulator(xmltostring(e))
+--~             if data == "" then
+--~                 epdt[e.ni] = ""
+--~             else
+--~                 local xi = xmlinheritedconvert(data,xmldata)
+--~                 if not xi then
+--~                     epdt[e.ni] = ""
+--~                 else
+--~                     epdt[e.ni] = xml.body(xi) -- xml.assign(d,k,xi)
+--~                 end
+--~             end
+--~         end
+--~     end
+--~ end
+
+--~ xml.manipulate = manipulate
+
+function xml.strip_whitespace(root, pattern, nolines) -- strips all leading and trailing space !
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for i=1,#collected do
+            local e = collected[i]
+            local edt = e.dt
+            if edt then
+                local t = { }
+                for i=1,#edt do
+                    local str = edt[i]
+                    if type(str) == "string" then
+                        if str == "" then
+                            -- stripped
+                        else
+                            if nolines then
+                                str = gsub(str,"[ \n\r\t]+"," ")
+                            end
+                            if str == "" then
+                                -- stripped
+                            else
+                                t[#t+1] = str
+                            end
+                        end
+                    else
+        --~                         str.ni = i
+                        t[#t+1] = str
+                    end
+                end
+                e.dt = t
+            end
+        end
+    end
+end
+
+function xml.strip_whitespace(root, pattern, nolines, anywhere) -- strips all leading and trailing spacing
+    local collected = xmlparseapply({ root },pattern) -- beware, indices no longer are valid now
+    if collected then
+        for i=1,#collected do
+            local e = collected[i]
+            local edt = e.dt
+            if edt then
+                if anywhere then
+                    local t = { }
+                    for e=1,#edt do
+                        local str = edt[e]
+                        if type(str) ~= "string" then
+                            t[#t+1] = str
+                        elseif str ~= "" then
+                            -- todo: lpeg for each case
+                            if nolines then
+                                str = gsub(str,"%s+"," ")
+                            end
+                            str = gsub(str,"^%s*(.-)%s*$","%1")
+                            if str ~= "" then
+                                t[#t+1] = str
+                            end
+                        end
+                    end
+                    e.dt = t
+                else
+                    -- we can assume a regular sparse xml table with no successive strings
+                    -- otherwise we should use a while loop
+                    if #edt > 0 then
+                        -- strip front
+                        local str = edt[1]
+                        if type(str) ~= "string" then
+                            -- nothing
+                        elseif str == "" then
+                            remove(edt,1)
+                        else
+                            if nolines then
+                                str = gsub(str,"%s+"," ")
+                            end
+                            str = gsub(str,"^%s+","")
+                            if str == "" then
+                                remove(edt,1)
+                            else
+                                edt[1] = str
+                            end
+                        end
+                    end
+                    if #edt > 1 then
+                        -- strip end
+                        local str = edt[#edt]
+                        if type(str) ~= "string" then
+                            -- nothing
+                        elseif str == "" then
+                            remove(edt)
+                        else
+                            if nolines then
+                                str = gsub(str,"%s+"," ")
+                            end
+                            str = gsub(str,"%s+$","")
+                            if str == "" then
+                                remove(edt)
+                            else
+                                edt[#edt] = str
+                            end
+                        end
+                    end
+                end
+            end
+        end
+    end
+end
+
+local function rename_space(root, oldspace, newspace) -- fast variant
+    local ndt = #root.dt
+    for i=1,ndt or 0 do
+        local e = root[i]
+        if type(e) == "table" then
+            if e.ns == oldspace then
+                e.ns = newspace
+                if e.rn then
+                    e.rn = newspace
+                end
+            end
+            local edt = e.dt
+            if edt then
+                rename_space(edt, oldspace, newspace)
+            end
+        end
+    end
+end
+
+xml.rename_space = rename_space
+
+function xml.remap_tag(root, pattern, newtg)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            collected[c].tg = newtg
+        end
+    end
+end
+
+function xml.remap_namespace(root, pattern, newns)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            collected[c].ns = newns
+        end
+    end
+end
+
+function xml.check_namespace(root, pattern, newns)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            if (not e.rn or e.rn == "") and e.ns == "" then
+                e.rn = newns
+            end
+        end
+    end
+end
+
+function xml.remap_name(root, pattern, newtg, newns, newrn)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            e.tg, e.ns, e.rn = newtg, newns, newrn
+        end
+    end
+end
+
+--[[ldx--
+<p>Here are a few synonyms.</p>
+--ldx]]--
+
+xml.each     = xml.each_element
+xml.process  = xml.process_element
+xml.strip    = xml.strip_whitespace
+xml.collect  = xml.collect_elements
+xml.all      = xml.collect_elements
+
+xml.insert   = xml.insert_element_after
+xml.inject   = xml.inject_element_after
+xml.after    = xml.insert_element_after
+xml.before   = xml.insert_element_before
+xml.delete   = xml.delete_element
+xml.replace  = xml.replace_element
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['lxml-xml'] = {
+    version   = 1.001,
+    comment   = "this module is the basis for the lxml-* ones",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local finalizers   = xml.finalizers.xml
+local xmlfilter    = xml.filter -- we could inline this one for speed
+local xmltostring  = xml.tostring
+local xmlserialize = xml.serialize
+
+local function first(collected) -- wrong ?
+    return collected and collected[1]
+end
+
+local function last(collected)
+    return collected and collected[#collected]
+end
+
+local function all(collected)
+    return collected
+end
+
+local function reverse(collected)
+    if collected then
+        local reversed = { }
+        for c=#collected,1,-1 do
+            reversed[#reversed+1] = collected[c]
+        end
+        return reversed
+    end
+end
+
+local function attribute(collected,name)
+    if collected and #collected > 0 then
+        local at = collected[1].at
+        return at and at[name]
+    end
+end
+
+local function att(id,name)
+    local at = id.at
+    return at and at[name]
+end
+
+local function count(collected)
+    return (collected and #collected) or 0
+end
+
+local function position(collected,n)
+    if collected then
+        n = tonumber(n) or 0
+        if n < 0 then
+            return collected[#collected + n + 1]
+        elseif n > 0 then
+            return collected[n]
+        else
+            return collected[1].mi or 0
+        end
+    end
+end
+
+local function match(collected)
+    return (collected and collected[1].mi) or 0 -- match
+end
+
+local function index(collected)
+    if collected then
+        return collected[1].ni
+    end
+end
+
+local function attributes(collected,arguments)
+    if collected then
+        local at = collected[1].at
+        if arguments then
+            return at[arguments]
+        elseif next(at) then
+            return at -- all of them
+        end
+    end
+end
+
+local function chainattribute(collected,arguments) -- todo: optional levels
+    if collected then
+        local e = collected[1]
+        while e do
+            local at = e.at
+            if at then
+                local a = at[arguments]
+                if a then
+                    return a
+                end
+            else
+                break -- error
+            end
+            e = e.__p__
+        end
+    end
+    return ""
+end
+
+local function raw(collected) -- hybrid
+    if collected then
+        local e = collected[1] or collected
+        return (e and xmlserialize(e)) or "" -- only first as we cannot concat function
+    else
+        return ""
+    end
+end
+
+local function text(collected) -- hybrid
+    if collected then
+        local e = collected[1] or collected
+        return (e and xmltostring(e.dt)) or ""
+    else
+        return ""
+    end
+end
+
+local function texts(collected)
+    if collected then
+        local t = { }
+        for c=1,#collected do
+            local e = collection[c]
+            if e and e.dt then
+                t[#t+1] = e.dt
+            end
+        end
+        return t
+    end
+end
+
+local function tag(collected,n)
+    if collected then
+        local c
+        if n == 0 or not n then
+            c = collected[1]
+        elseif n > 1 then
+            c = collected[n]
+        else
+            c = collected[#collected-n+1]
+        end
+        return c and c.tg
+    end
+end
+
+local function name(collected,n)
+    if collected then
+        local c
+        if n == 0 or not n then
+            c = collected[1]
+        elseif n > 1 then
+            c = collected[n]
+        else
+            c = collected[#collected-n+1]
+        end
+        if c then
+            if c.ns == "" then
+                return c.tg
+            else
+                return c.ns .. ":" .. c.tg
+            end
+        end
+    end
+end
+
+local function tags(collected,nonamespace)
+    if collected then
+        local t = { }
+        for c=1,#collected do
+            local e = collected[c]
+            local ns, tg = e.ns, e.tg
+            if nonamespace or ns == "" then
+                t[#t+1] = tg
+            else
+                t[#t+1] = ns .. ":" .. tg
+            end
+        end
+        return t
+    end
+end
+
+local function empty(collected)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            if e then
+                local edt = e.dt
+                if edt then
+                    local n = #edt
+                    if n == 1 then
+                        local edk = edt[1]
+                        local typ = type(edk)
+                        if typ == "table" then
+                            return false
+                        elseif edk ~= "" then -- maybe an extra tester for spacing only
+                            return false
+                        end
+                    elseif n > 1 then
+                        return false
+                    end
+                end
+            end
+        end
+    end
+    return true
+end
+
+finalizers.first          = first
+finalizers.last           = last
+finalizers.all            = all
+finalizers.reverse        = reverse
+finalizers.elements       = all
+finalizers.default        = all
+finalizers.attribute      = attribute
+finalizers.att            = att
+finalizers.count          = count
+finalizers.position       = position
+finalizers.match          = match
+finalizers.index          = index
+finalizers.attributes     = attributes
+finalizers.chainattribute = chainattribute
+finalizers.text           = text
+finalizers.texts          = texts
+finalizers.tag            = tag
+finalizers.name           = name
+finalizers.tags           = tags
+finalizers.empty          = empty
+
+-- shortcuts -- we could support xmlfilter(id,pattern,first)
+
+function xml.first(id,pattern)
+    return first(xmlfilter(id,pattern))
+end
+
+function xml.last(id,pattern)
+    return last(xmlfilter(id,pattern))
+end
+
+function xml.count(id,pattern)
+    return count(xmlfilter(id,pattern))
+end
+
+function xml.attribute(id,pattern,a,default)
+    return attribute(xmlfilter(id,pattern),a,default)
+end
+
+function xml.raw(id,pattern)
+    if pattern then
+        return raw(xmlfilter(id,pattern))
+    else
+        return raw(id)
+    end
+end
+
+function xml.text(id,pattern)
+    if pattern then
+     -- return text(xmlfilter(id,pattern))
+        local collected = xmlfilter(id,pattern)
+        return (collected and xmltostring(collected[1].dt)) or ""
+    elseif id then
+     -- return text(id)
+        return xmltostring(id.dt) or ""
+    else
+        return ""
+    end
+end
+
+xml.content = text
+
+function xml.position(id,pattern,n) -- element
+    return position(xmlfilter(id,pattern),n)
+end
+
+function xml.match(id,pattern) -- number
+    return match(xmlfilter(id,pattern))
+end
+
+function xml.empty(id,pattern)
+    return empty(xmlfilter(id,pattern))
+end
+
+xml.all    = xml.filter
+xml.index  = xml.position
+xml.found  = xml.filter
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['luat-env'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- A former version provided functionality for non embeded core
+-- scripts i.e. runtime library loading. Given the amount of
+-- Lua code we use now, this no longer makes sense. Much of this
+-- evolved before bytecode arrays were available and so a lot of
+-- code has disappeared already.
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+local format, sub, match, gsub, find = string.format, string.sub, string.match, string.gsub, string.find
+local unquote, quote = string.unquote, string.quote
+
+-- precautions
+
+os.setlocale(nil,nil) -- useless feature and even dangerous in luatex
+
+function os.setlocale()
+    -- no way you can mess with it
+end
+
+-- dirty tricks
+
+if arg and (arg[0] == 'luatex' or arg[0] == 'luatex.exe') and arg[1] == "--luaonly" then
+    arg[-1]=arg[0] arg[0]=arg[2] for k=3,#arg do arg[k-2]=arg[k] end arg[#arg]=nil arg[#arg]=nil
+end
+
+if profiler and os.env["MTX_PROFILE_RUN"] == "YES" then
+    profiler.start("luatex-profile.log")
+end
+
+-- environment
+
+environment             = environment or { }
+environment.arguments   = { }
+environment.files       = { }
+environment.sortedflags = nil
+
+if not environment.jobname or environment.jobname == "" then if tex then environment.jobname = tex.jobname end end
+if not environment.version or environment.version == "" then             environment.version = "unknown"   end
+if not environment.jobname                              then             environment.jobname = "unknown"   end
+
+function environment.initialize_arguments(arg)
+    local arguments, files = { }, { }
+    environment.arguments, environment.files, environment.sortedflags = arguments, files, nil
+    for index=1,#arg do
+        local argument = arg[index]
+        if index > 0 then
+            local flag, value = match(argument,"^%-+(.-)=(.-)$")
+            if flag then
+                arguments[flag] = unquote(value or "")
+            else
+                flag = match(argument,"^%-+(.+)")
+                if flag then
+                    arguments[flag] = true
+                else
+                    files[#files+1] = argument
+                end
+            end
+        end
+    end
+    environment.ownname = environment.ownname or arg[0] or 'unknown.lua'
+end
+
+function environment.setargument(name,value)
+    environment.arguments[name] = value
+end
+
+-- todo: defaults, better checks e.g on type (boolean versus string)
+--
+-- tricky: too many hits when we support partials unless we add
+-- a registration of arguments so from now on we have 'partial'
+
+function environment.argument(name,partial)
+    local arguments, sortedflags = environment.arguments, environment.sortedflags
+    if arguments[name] then
+        return arguments[name]
+    elseif partial then
+        if not sortedflags then
+            sortedflags = table.sortedkeys(arguments)
+            for k=1,#sortedflags do
+                sortedflags[k] = "^" .. sortedflags[k]
+            end
+            environment.sortedflags = sortedflags
+        end
+        -- example of potential clash: ^mode ^modefile
+        for k=1,#sortedflags do
+            local v = sortedflags[k]
+            if find(name,v) then
+                return arguments[sub(v,2,#v)]
+            end
+        end
+    end
+    return nil
+end
+
+environment.argument("x",true)
+
+function environment.split_arguments(separator) -- rather special, cut-off before separator
+    local done, before, after = false, { }, { }
+    local original_arguments = environment.original_arguments
+    for k=1,#original_arguments do
+        local v = original_arguments[k]
+        if not done and v == separator then
+            done = true
+        elseif done then
+            after[#after+1] = v
+        else
+            before[#before+1] = v
+        end
+    end
+    return before, after
+end
+
+function environment.reconstruct_commandline(arg,noquote)
+    arg = arg or environment.original_arguments
+    if noquote and #arg == 1 then
+        local a = arg[1]
+        a = resolvers.resolve(a)
+        a = unquote(a)
+        return a
+    elseif #arg > 0 then
+        local result = { }
+        for i=1,#arg do
+            local a = arg[i]
+            a = resolvers.resolve(a)
+            a = unquote(a)
+            a = gsub(a,'"','\\"') -- tricky
+            if find(a," ") then
+                result[#result+1] = quote(a)
+            else
+                result[#result+1] = a
+            end
+        end
+        return table.join(result," ")
+    else
+        return ""
+    end
+end
+
+if arg then
+
+    -- new, reconstruct quoted snippets (maybe better just remove the " then and add them later)
+    local newarg, instring = { }, false
+
+    for index=1,#arg do
+        local argument = arg[index]
+        if find(argument,"^\"") then
+            newarg[#newarg+1] = gsub(argument,"^\"","")
+            if not find(argument,"\"$") then
+                instring = true
+            end
+        elseif find(argument,"\"$") then
+            newarg[#newarg] = newarg[#newarg] .. " " .. gsub(argument,"\"$","")
+            instring = false
+        elseif instring then
+            newarg[#newarg] = newarg[#newarg] .. " " .. argument
+        else
+            newarg[#newarg+1] = argument
+        end
+    end
+    for i=1,-5,-1 do
+        newarg[i] = arg[i]
+    end
+
+    environment.initialize_arguments(newarg)
+    environment.original_arguments = newarg
+    environment.raw_arguments = arg
+
+    arg = { } -- prevent duplicate handling
+
+end
+
+-- weird place ... depends on a not yet loaded module
+
+function environment.texfile(filename)
+    return resolvers.find_file(filename,'tex')
+end
+
+function environment.luafile(filename)
+    local resolved = resolvers.find_file(filename,'tex') or ""
+    if resolved ~= "" then
+        return resolved
+    end
+    resolved = resolvers.find_file(filename,'texmfscripts') or ""
+    if resolved ~= "" then
+        return resolved
+    end
+    return resolvers.find_file(filename,'luatexlibs') or ""
+end
+
+environment.loadedluacode = loadfile -- can be overloaded
+
+--~ function environment.loadedluacode(name)
+--~     if os.spawn("texluac -s -o texluac.luc " .. name) == 0 then
+--~         local chunk = loadstring(io.loaddata("texluac.luc"))
+--~         os.remove("texluac.luc")
+--~         return chunk
+--~     else
+--~         environment.loadedluacode = loadfile -- can be overloaded
+--~         return loadfile(name)
+--~     end
+--~ end
+
+function environment.luafilechunk(filename) -- used for loading lua bytecode in the format
+    filename = file.replacesuffix(filename, "lua")
+    local fullname = environment.luafile(filename)
+    if fullname and fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading file %s", fullname)
+        end
+        return environment.loadedluacode(fullname)
+    else
+        if trace_locating then
+            logs.report("fileio","unknown file %s", filename)
+        end
+        return nil
+    end
+end
+
+-- the next ones can use the previous ones / combine
+
+function environment.loadluafile(filename, version)
+    local lucname, luaname, chunk
+    local basename = file.removesuffix(filename)
+    if basename == filename then
+        lucname, luaname = basename .. ".luc",  basename .. ".lua"
+    else
+        lucname, luaname = nil, basename -- forced suffix
+    end
+    -- when not overloaded by explicit suffix we look for a luc file first
+    local fullname = (lucname and environment.luafile(lucname)) or ""
+    if fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading %s", fullname)
+        end
+        chunk = loadfile(fullname) -- this way we don't need a file exists check
+    end
+    if chunk then
+        assert(chunk)()
+        if version then
+            -- we check of the version number of this chunk matches
+            local v = version -- can be nil
+            if modules and modules[filename] then
+                v = modules[filename].version -- new method
+            elseif versions and versions[filename] then
+                v = versions[filename]        -- old method
+            end
+            if v == version then
+                return true
+            else
+                if trace_locating then
+                    logs.report("fileio","version mismatch for %s: lua=%s, luc=%s", filename, v, version)
+                end
+                environment.loadluafile(filename)
+            end
+        else
+            return true
+        end
+    end
+    fullname = (luaname and environment.luafile(luaname)) or ""
+    if fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading %s", fullname)
+        end
+        chunk = loadfile(fullname) -- this way we don't need a file exists check
+        if not chunk then
+            if trace_locating then
+                logs.report("fileio","unknown file %s", filename)
+            end
+        else
+            assert(chunk)()
+            return true
+        end
+    end
+    return false
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-inf'] = {
+    version   = 1.001,
+    comment   = "companion to trac-inf.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format = string.format
+
+local statusinfo, n, registered = { }, 0, { }
+
+statistics = statistics or { }
+
+statistics.enable    = true
+statistics.threshold = 0.05
+
+-- timing functions
+
+local clock = os.gettimeofday or os.clock
+
+local notimer
+
+function statistics.hastimer(instance)
+    return instance and instance.starttime
+end
+
+function statistics.resettiming(instance)
+    if not instance then
+        notimer = { timing = 0, loadtime = 0 }
+    else
+        instance.timing, instance.loadtime = 0, 0
+    end
+end
+
+function statistics.starttiming(instance)
+    if not instance then
+        notimer = { }
+        instance = notimer
+    end
+    local it = instance.timing
+    if not it then
+        it = 0
+    end
+    if it == 0 then
+        instance.starttime = clock()
+        if not instance.loadtime then
+            instance.loadtime = 0
+        end
+    else
+--~         logs.report("system","nested timing (%s)",tostring(instance))
+    end
+    instance.timing = it + 1
+end
+
+function statistics.stoptiming(instance, report)
+    if not instance then
+        instance = notimer
+    end
+    if instance then
+        local it = instance.timing
+        if it > 1 then
+            instance.timing = it - 1
+        else
+            local starttime = instance.starttime
+            if starttime then
+                local stoptime = clock()
+                local loadtime = stoptime - starttime
+                instance.stoptime = stoptime
+                instance.loadtime = instance.loadtime + loadtime
+                if report then
+                    statistics.report("load time %0.3f",loadtime)
+                end
+                instance.timing = 0
+                return loadtime
+            end
+        end
+    end
+    return 0
+end
+
+function statistics.elapsedtime(instance)
+    if not instance then
+        instance = notimer
+    end
+    return format("%0.3f",(instance and instance.loadtime) or 0)
+end
+
+function statistics.elapsedindeed(instance)
+    if not instance then
+        instance = notimer
+    end
+    local t = (instance and instance.loadtime) or 0
+    return t > statistics.threshold
+end
+
+function statistics.elapsedseconds(instance,rest) -- returns nil if 0 seconds
+    if statistics.elapsedindeed(instance) then
+        return format("%s seconds %s", statistics.elapsedtime(instance),rest or "")
+    end
+end
+
+-- general function
+
+function statistics.register(tag,fnc)
+    if statistics.enable and type(fnc) == "function" then
+        local rt = registered[tag] or (#statusinfo + 1)
+        statusinfo[rt] = { tag, fnc }
+        registered[tag] = rt
+        if #tag > n then n = #tag end
+    end
+end
+
+function statistics.show(reporter)
+    if statistics.enable then
+        if not reporter then reporter = function(tag,data,n) texio.write_nl(tag .. " " .. data) end end
+        -- this code will move
+        local register = statistics.register
+        register("luatex banner", function()
+            return string.lower(status.banner)
+        end)
+        register("control sequences", function()
+            return format("%s of %s", status.cs_count, status.hash_size+status.hash_extra)
+        end)
+        register("callbacks", function()
+            local total, indirect = status.callbacks or 0, status.indirect_callbacks or 0
+            return format("direct: %s, indirect: %s, total: %s", total-indirect, indirect, total)
+        end)
+        register("current memory usage", statistics.memused)
+        register("runtime",statistics.runtime)
+--         --
+        for i=1,#statusinfo do
+            local s = statusinfo[i]
+            local r = s[2]()
+            if r then
+                reporter(s[1],r,n)
+            end
+        end
+        texio.write_nl("") -- final newline
+        statistics.enable = false
+    end
+end
+
+function statistics.show_job_stat(tag,data,n)
+    texio.write_nl(format("%-15s: %s - %s","mkiv lua stats",tag:rpadd(n," "),data))
+end
+
+function statistics.memused() -- no math.round yet -)
+    local round = math.round or math.floor
+    return format("%s MB (ctx: %s MB)",round(collectgarbage("count")/1000), round(status.luastate_bytes/1000000))
+end
+
+if statistics.runtime then
+    -- already loaded and set
+elseif luatex and luatex.starttime then
+    statistics.starttime = luatex.starttime
+    statistics.loadtime = 0
+    statistics.timing = 0
+else
+    statistics.starttiming(statistics)
+end
+
+function statistics.runtime()
+    statistics.stoptiming(statistics)
+    return statistics.formatruntime(statistics.elapsedtime(statistics))
+end
+
+function statistics.formatruntime(runtime)
+    return format("%s seconds", statistics.elapsedtime(statistics))
+end
+
+function statistics.timed(action,report)
+    local timer = { }
+    report = report or logs.simple
+    statistics.starttiming(timer)
+    action()
+    statistics.stoptiming(timer)
+    report("total runtime: %s",statistics.elapsedtime(timer))
+end
+
+-- where, not really the best spot for this:
+
+commands = commands or { }
+
+local timer
+
+function commands.resettimer()
+    statistics.resettiming(timer)
+    statistics.starttiming(timer)
+end
+
+function commands.elapsedtime()
+    statistics.stoptiming(timer)
+    tex.sprint(statistics.elapsedtime(timer))
+end
+
+commands.resettimer()
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-log'] = {
+    version   = 1.001,
+    comment   = "companion to trac-log.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- this is old code that needs an overhaul
+
+--~ io.stdout:setvbuf("no")
+--~ io.stderr:setvbuf("no")
+
+local write_nl, write = texio.write_nl or print, texio.write or io.write
+local format, gmatch = string.format, string.gmatch
+local texcount = tex and tex.count
+
+if texlua then
+    write_nl = print
+    write    = io.write
+end
+
+--[[ldx--
+<p>This is a prelude to a more extensive logging module. For the sake
+of parsing log files, in addition to the standard logging we will
+provide an <l n='xml'/> structured file. Actually, any logging that
+is hooked into callbacks will be \XML\ by default.</p>
+--ldx]]--
+
+logs     = logs     or { }
+logs.xml = logs.xml or { }
+logs.tex = logs.tex or { }
+
+--[[ldx--
+<p>This looks pretty ugly but we need to speed things up a bit.</p>
+--ldx]]--
+
+logs.moreinfo = [[
+more information about ConTeXt and the tools that come with it can be found at:
+
+maillist : ntg-context@ntg.nl / http://www.ntg.nl/mailman/listinfo/ntg-context
+webpage  : http://www.pragma-ade.nl / http://tex.aanhet.net
+wiki     : http://contextgarden.net
+]]
+
+logs.levels = {
+    ['error']   = 1,
+    ['warning'] = 2,
+    ['info']    = 3,
+    ['debug']   = 4,
+}
+
+logs.functions = {
+    'report', 'start', 'stop', 'push', 'pop', 'line', 'direct',
+    'start_run', 'stop_run',
+    'start_page_number', 'stop_page_number',
+    'report_output_pages', 'report_output_log',
+    'report_tex_stat', 'report_job_stat',
+    'show_open', 'show_close', 'show_load',
+}
+
+logs.tracers = {
+}
+
+logs.level = 0
+logs.mode  = string.lower((os.getenv("MTX.LOG.MODE") or os.getenv("MTX_LOG_MODE") or "tex"))
+
+function logs.set_level(level)
+    logs.level = logs.levels[level] or level
+end
+
+function logs.set_method(method)
+    for _, v in next, logs.functions do
+        logs[v] = logs[method][v] or function() end
+    end
+end
+
+-- tex logging
+
+function logs.tex.report(category,fmt,...) -- new
+    if fmt then
+        write_nl(category .. " | " .. format(fmt,...))
+    else
+        write_nl(category .. " |")
+    end
+end
+
+function logs.tex.line(fmt,...) -- new
+    if fmt then
+        write_nl(format(fmt,...))
+    else
+        write_nl("")
+    end
+end
+
+--~ function logs.tex.start_page_number()
+--~     local real, user, sub = texcount.realpageno, texcount.userpageno, texcount.subpageno
+--~     if real > 0 then
+--~         if user > 0 then
+--~             if sub > 0 then
+--~                 write(format("[%s.%s.%s",real,user,sub))
+--~             else
+--~                 write(format("[%s.%s",real,user))
+--~             end
+--~         else
+--~             write(format("[%s",real))
+--~         end
+--~     else
+--~         write("[-")
+--~     end
+--~ end
+
+--~ function logs.tex.stop_page_number()
+--~     write("]")
+--~ end
+
+local real, user, sub
+
+function logs.tex.start_page_number()
+    real, user, sub = texcount.realpageno, texcount.userpageno, texcount.subpageno
+end
+
+function logs.tex.stop_page_number()
+    if real > 0 then
+        if user > 0 then
+            if sub > 0 then
+                logs.report("pages", "flushing realpage %s, userpage %s, subpage %s",real,user,sub)
+            else
+                logs.report("pages", "flushing realpage %s, userpage %s",real,user)
+            end
+        else
+            logs.report("pages", "flushing realpage %s",real)
+        end
+    else
+        logs.report("pages", "flushing page")
+    end
+    io.flush()
+end
+
+logs.tex.report_job_stat = statistics.show_job_stat
+
+-- xml logging
+
+function logs.xml.report(category,fmt,...) -- new
+    if fmt then
+        write_nl(format("<r category='%s'>%s</r>",category,format(fmt,...)))
+    else
+        write_nl(format("<r category='%s'/>",category))
+    end
+end
+function logs.xml.line(fmt,...) -- new
+    if fmt then
+        write_nl(format("<r>%s</r>",format(fmt,...)))
+    else
+        write_nl("<r/>")
+    end
+end
+
+function logs.xml.start() if logs.level > 0 then tw("<%s>" ) end end
+function logs.xml.stop () if logs.level > 0 then tw("</%s>") end end
+function logs.xml.push () if logs.level > 0 then tw("<!-- ") end end
+function logs.xml.pop  () if logs.level > 0 then tw(" -->" ) end end
+
+function logs.xml.start_run()
+    write_nl("<?xml version='1.0' standalone='yes'?>")
+    write_nl("<job>") --  xmlns='www.pragma-ade.com/luatex/schemas/context-job.rng'
+    write_nl("")
+end
+
+function logs.xml.stop_run()
+    write_nl("</job>")
+end
+
+function logs.xml.start_page_number()
+    write_nl(format("<p real='%s' page='%s' sub='%s'", texcount.realpageno, texcount.userpageno, texcount.subpageno))
+end
+
+function logs.xml.stop_page_number()
+    write("/>")
+    write_nl("")
+end
+
+function logs.xml.report_output_pages(p,b)
+    write_nl(format("<v k='pages' v='%s'/>", p))
+    write_nl(format("<v k='bytes' v='%s'/>", b))
+    write_nl("")
+end
+
+function logs.xml.report_output_log()
+end
+
+function logs.xml.report_tex_stat(k,v)
+    texiowrite_nl("log","<v k='"..k.."'>"..tostring(v).."</v>")
+end
+
+local level = 0
+
+function logs.xml.show_open(name)
+    level = level + 1
+    texiowrite_nl(format("<f l='%s' n='%s'>",level,name))
+end
+
+function logs.xml.show_close(name)
+    texiowrite("</f> ")
+    level = level - 1
+end
+
+function logs.xml.show_load(name)
+    texiowrite_nl(format("<f l='%s' n='%s'/>",level+1,name))
+end
+
+--
+
+local name, banner = 'report', 'context'
+
+local function report(category,fmt,...)
+    if fmt then
+        write_nl(format("%s | %s: %s",name,category,format(fmt,...)))
+    elseif category then
+        write_nl(format("%s | %s",name,category))
+    else
+        write_nl(format("%s |",name))
+    end
+end
+
+local function simple(fmt,...)
+    if fmt then
+        write_nl(format("%s | %s",name,format(fmt,...)))
+    else
+        write_nl(format("%s |",name))
+    end
+end
+
+function logs.setprogram(_name_,_banner_,_verbose_)
+    name, banner = _name_, _banner_
+    if _verbose_ then
+        trackers.enable("resolvers.locating")
+    end
+    logs.set_method("tex")
+    logs.report = report -- also used in libraries
+    logs.simple = simple -- only used in scripts !
+    if utils then
+        utils.report = simple
+    end
+    logs.verbose = _verbose_
+end
+
+function logs.setverbose(what)
+    if what then
+        trackers.enable("resolvers.locating")
+    else
+        trackers.disable("resolvers.locating")
+    end
+    logs.verbose = what or false
+end
+
+function logs.extendbanner(_banner_,_verbose_)
+    banner = banner .. " | ".. _banner_
+    if _verbose_ ~= nil then
+        logs.setverbose(what)
+    end
+end
+
+logs.verbose = false
+logs.report  = logs.tex.report
+logs.simple  = logs.tex.report
+
+function logs.reportlines(str) -- todo: <lines></lines>
+    for line in gmatch(str,"(.-)[\n\r]") do
+        logs.report(line)
+    end
+end
+
+function logs.reportline() -- for scripts too
+    logs.report()
+end
+
+logs.simpleline = logs.reportline
+
+function logs.reportbanner() -- for scripts too
+    logs.report(banner)
+end
+
+function logs.help(message,option)
+    logs.reportbanner()
+    logs.reportline()
+    logs.reportlines(message)
+    local moreinfo = logs.moreinfo or ""
+    if moreinfo ~= "" and option ~= "nomoreinfo" then
+        logs.reportline()
+        logs.reportlines(moreinfo)
+    end
+end
+
+logs.set_level('error')
+logs.set_method('tex')
+
+function logs.system(whereto,process,jobname,category,...)
+    for i=1,10 do
+        local f = io.open(whereto,"a")
+        if f then
+            f:write(format("%s %s => %s => %s => %s\r",os.date("%d/%m/%y %H:%m:%S"),process,jobname,category,format(...)))
+            f:close()
+            break
+        else
+            sleep(0.1)
+        end
+    end
+end
+
+--~ local syslogname = "oeps.xxx"
+--~
+--~ for i=1,10 do
+--~     logs.system(syslogname,"context","test","fonts","font %s recached due to newer version (%s)","blabla","123")
+--~ end
+
+function logs.fatal(where,...)
+    logs.report(where,"fatal error: %s, aborting now",format(...))
+    os.exit()
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-inp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files",
+}
+
+-- After a few years using the code the large luat-inp.lua file
+-- has been split up a bit. In the process some functionality was
+-- dropped:
+--
+-- * support for reading lsr files
+-- * selective scanning (subtrees)
+-- * some public auxiliary functions were made private
+--
+-- TODO: os.getenv -> os.env[]
+-- TODO: instances.[hashes,cnffiles,configurations,522]
+-- TODO: check escaping in find etc, too much, too slow
+
+-- This lib is multi-purpose and can be loaded again later on so that
+-- additional functionality becomes available. We will split thislogs.report("fileio",
+-- module in components once we're done with prototyping. This is the
+-- first code I wrote for LuaTeX, so it needs some cleanup. Before changing
+-- something in this module one can best check with Taco or Hans first; there
+-- is some nasty trickery going on that relates to traditional kpse support.
+
+-- To be considered: hash key lowercase, first entry in table filename
+-- (any case), rest paths (so no need for optimization). Or maybe a
+-- separate table that matches lowercase names to mixed case when
+-- present. In that case the lower() cases can go away. I will do that
+-- only when we run into problems with names ... well ... Iwona-Regular.
+
+-- Beware, loading and saving is overloaded in luat-tmp!
+
+local format, gsub, find, lower, upper, match, gmatch = string.format, string.gsub, string.find, string.lower, string.upper, string.match, string.gmatch
+local concat, insert, sortedkeys = table.concat, table.insert, table.sortedkeys
+local next, type = next, type
+local lpegmatch = lpeg.match
+
+local trace_locating, trace_detail, trace_expansions = false, false, false
+
+trackers.register("resolvers.locating",   function(v) trace_locating   = v end)
+trackers.register("resolvers.details",    function(v) trace_detail     = v end)
+trackers.register("resolvers.expansions", function(v) trace_expansions = v end) -- todo
+
+if not resolvers then
+    resolvers = {
+        suffixes     = { },
+        formats      = { },
+        dangerous    = { },
+        suffixmap    = { },
+        alternatives = { },
+        locators     = { },  -- locate databases
+        hashers      = { },  -- load databases
+        generators   = { },  -- generate databases
+    }
+end
+
+local resolvers = resolvers
+
+resolvers.locators  .notfound = { nil }
+resolvers.hashers   .notfound = { nil }
+resolvers.generators.notfound = { nil }
+
+resolvers.cacheversion = '1.0.1'
+resolvers.cnfname      = 'texmf.cnf'
+resolvers.luaname      = 'texmfcnf.lua'
+resolvers.homedir      = os.env[os.type == "windows" and 'USERPROFILE'] or os.env['HOME'] or '~'
+resolvers.cnfdefault   = '{$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,.local,}/web2c}'
+
+local dummy_path_expr = "^!*unset/*$"
+
+local formats      = resolvers.formats
+local suffixes     = resolvers.suffixes
+local dangerous    = resolvers.dangerous
+local suffixmap    = resolvers.suffixmap
+local alternatives = resolvers.alternatives
+
+formats['afm'] = 'AFMFONTS'       suffixes['afm'] = { 'afm' }
+formats['enc'] = 'ENCFONTS'       suffixes['enc'] = { 'enc' }
+formats['fmt'] = 'TEXFORMATS'     suffixes['fmt'] = { 'fmt' }
+formats['map'] = 'TEXFONTMAPS'    suffixes['map'] = { 'map' }
+formats['mp']  = 'MPINPUTS'       suffixes['mp']  = { 'mp' }
+formats['ocp'] = 'OCPINPUTS'      suffixes['ocp'] = { 'ocp' }
+formats['ofm'] = 'OFMFONTS'       suffixes['ofm'] = { 'ofm', 'tfm' }
+formats['otf'] = 'OPENTYPEFONTS'  suffixes['otf'] = { 'otf' } -- 'ttf'
+formats['opl'] = 'OPLFONTS'       suffixes['opl'] = { 'opl' }
+formats['otp'] = 'OTPINPUTS'      suffixes['otp'] = { 'otp' }
+formats['ovf'] = 'OVFFONTS'       suffixes['ovf'] = { 'ovf', 'vf' }
+formats['ovp'] = 'OVPFONTS'       suffixes['ovp'] = { 'ovp' }
+formats['tex'] = 'TEXINPUTS'      suffixes['tex'] = { 'tex' }
+formats['tfm'] = 'TFMFONTS'       suffixes['tfm'] = { 'tfm' }
+formats['ttf'] = 'TTFONTS'        suffixes['ttf'] = { 'ttf', 'ttc', 'dfont' }
+formats['pfb'] = 'T1FONTS'        suffixes['pfb'] = { 'pfb', 'pfa' }
+formats['vf']  = 'VFFONTS'        suffixes['vf']  = { 'vf' }
+
+formats['fea'] = 'FONTFEATURES'   suffixes['fea'] = { 'fea' }
+formats['cid'] = 'FONTCIDMAPS'    suffixes['cid'] = { 'cid', 'cidmap' }
+
+formats ['texmfscripts'] = 'TEXMFSCRIPTS' -- new
+suffixes['texmfscripts'] = { 'rb', 'pl', 'py' } -- 'lua'
+
+formats ['lua'] = 'LUAINPUTS' -- new
+suffixes['lua'] = { 'lua', 'luc', 'tma', 'tmc' }
+
+-- backward compatible ones
+
+alternatives['map files']            = 'map'
+alternatives['enc files']            = 'enc'
+alternatives['cid maps']             = 'cid' -- great, why no cid files
+alternatives['font feature files']   = 'fea' -- and fea files here
+alternatives['opentype fonts']       = 'otf'
+alternatives['truetype fonts']       = 'ttf'
+alternatives['truetype collections'] = 'ttc'
+alternatives['truetype dictionary']  = 'dfont'
+alternatives['type1 fonts']          = 'pfb'
+
+-- obscure ones
+
+formats ['misc fonts'] = ''
+suffixes['misc fonts'] = { }
+
+formats     ['sfd']                      = 'SFDFONTS'
+suffixes    ['sfd']                      = { 'sfd' }
+alternatives['subfont definition files'] = 'sfd'
+
+-- lib paths
+
+formats ['lib'] = 'CLUAINPUTS' -- new (needs checking)
+suffixes['lib'] = (os.libsuffix and { os.libsuffix }) or { 'dll', 'so' }
+
+-- In practice we will work within one tds tree, but i want to keep
+-- the option open to build tools that look at multiple trees, which is
+-- why we keep the tree specific data in a table. We used to pass the
+-- instance but for practical pusposes we now avoid this and use a
+-- instance variable.
+
+-- here we catch a few new thingies (todo: add these paths to context.tmf)
+--
+-- FONTFEATURES  = .;$TEXMF/fonts/fea//
+-- FONTCIDMAPS   = .;$TEXMF/fonts/cid//
+
+-- we always have one instance active
+
+resolvers.instance = resolvers.instance or nil -- the current one (slow access)
+local instance = resolvers.instance or nil -- the current one (fast access)
+
+function resolvers.newinstance()
+
+    -- store once, freeze and faster (once reset we can best use
+    -- instance.environment) maybe better have a register suffix
+    -- function
+
+    for k, v in next, suffixes do
+        for i=1,#v do
+            local vi = v[i]
+            if vi then
+                suffixmap[vi] = k
+            end
+        end
+    end
+
+    -- because vf searching is somewhat dangerous, we want to prevent
+    -- too liberal searching esp because we do a lookup on the current
+    -- path anyway; only tex (or any) is safe
+
+    for k, v in next, formats do
+        dangerous[k] = true
+    end
+    dangerous.tex = nil
+
+    -- the instance
+
+    local newinstance = {
+        rootpath        = '',
+        treepath        = '',
+        progname        = 'context',
+        engine          = 'luatex',
+        format          = '',
+        environment     = { },
+        variables       = { },
+        expansions      = { },
+        files           = { },
+        remap           = { },
+        configuration   = { },
+        setup           = { },
+        order           = { },
+        found           = { },
+        foundintrees    = { },
+        kpsevars        = { },
+        hashes          = { },
+        cnffiles        = { },
+        luafiles        = { },
+        lists           = { },
+        remember        = true,
+        diskcache       = true,
+        renewcache      = false,
+        scandisk        = true,
+        cachepath       = nil,
+        loaderror       = false,
+        sortdata        = false,
+        savelists       = true,
+        cleanuppaths    = true,
+        allresults      = false,
+        pattern         = nil, -- lists
+        data            = { }, -- only for loading
+        force_suffixes  = true,
+        fakepaths       = { },
+    }
+
+    local ne = newinstance.environment
+
+    for k,v in next, os.env do
+        ne[k] = resolvers.bare_variable(v)
+    end
+
+    return newinstance
+
+end
+
+function resolvers.setinstance(someinstance)
+    instance = someinstance
+    resolvers.instance = someinstance
+    return someinstance
+end
+
+function resolvers.reset()
+    return resolvers.setinstance(resolvers.newinstance())
+end
+
+local function reset_hashes()
+    instance.lists = { }
+    instance.found = { }
+end
+
+local function check_configuration() -- not yet ok, no time for debugging now
+    local ie, iv = instance.environment, instance.variables
+    local function fix(varname,default)
+        local proname = varname .. "." .. instance.progname or "crap"
+        local p, v = ie[proname], ie[varname] or iv[varname]
+        if not ((p and p ~= "") or (v and v ~= "")) then
+            iv[varname] = default -- or environment?
+        end
+    end
+    local name = os.name
+    if name == "windows" then
+        fix("OSFONTDIR", "c:/windows/fonts//")
+    elseif name == "macosx" then
+        fix("OSFONTDIR", "$HOME/Library/Fonts//;/Library/Fonts//;/System/Library/Fonts//")
+    else
+        -- bad luck
+    end
+    fix("LUAINPUTS"   , ".;$TEXINPUTS;$TEXMFSCRIPTS") -- no progname, hm
+    -- this will go away some day
+    fix("FONTFEATURES", ".;$TEXMF/fonts/{data,fea}//;$OPENTYPEFONTS;$TTFONTS;$T1FONTS;$AFMFONTS")
+    fix("FONTCIDMAPS" , ".;$TEXMF/fonts/{data,cid}//;$OPENTYPEFONTS;$TTFONTS;$T1FONTS;$AFMFONTS")
+    --
+    fix("LUATEXLIBS"  , ".;$TEXMF/luatex/lua//")
+end
+
+function resolvers.bare_variable(str) -- assumes str is a string
+    return (gsub(str,"\s*([\"\']?)(.+)%1\s*", "%2"))
+end
+
+function resolvers.settrace(n) -- no longer number but: 'locating' or 'detail'
+    if n then
+        trackers.disable("resolvers.*")
+        trackers.enable("resolvers."..n)
+    end
+end
+
+resolvers.settrace(os.getenv("MTX_INPUT_TRACE"))
+
+function resolvers.osenv(key)
+    local ie = instance.environment
+    local value = ie[key]
+    if value == nil then
+     -- local e = os.getenv(key)
+        local e = os.env[key]
+        if e == nil then
+         -- value = "" -- false
+        else
+            value = resolvers.bare_variable(e)
+        end
+        ie[key] = value
+    end
+    return value or ""
+end
+
+function resolvers.env(key)
+    return instance.environment[key] or resolvers.osenv(key)
+end
+
+--
+
+local function expand_vars(lst) -- simple vars
+    local variables, env = instance.variables, resolvers.env
+    local function resolve(a)
+        return variables[a] or env(a)
+    end
+    for k=1,#lst do
+        lst[k] = gsub(lst[k],"%$([%a%d%_%-]+)",resolve)
+    end
+end
+
+local function expanded_var(var) -- simple vars
+    local function resolve(a)
+        return instance.variables[a] or resolvers.env(a)
+    end
+    return (gsub(var,"%$([%a%d%_%-]+)",resolve))
+end
+
+local function entry(entries,name)
+    if name and (name ~= "") then
+        name = gsub(name,'%$','')
+        local result = entries[name..'.'..instance.progname] or entries[name]
+        if result then
+            return result
+        else
+            result = resolvers.env(name)
+            if result then
+                instance.variables[name] = result
+                resolvers.expand_variables()
+                return instance.expansions[name] or ""
+            end
+        end
+    end
+    return ""
+end
+
+local function is_entry(entries,name)
+    if name and name ~= "" then
+        name = gsub(name,'%$','')
+        return (entries[name..'.'..instance.progname] or entries[name]) ~= nil
+    else
+        return false
+    end
+end
+
+-- {a,b,c,d}
+-- a,b,c/{p,q,r},d
+-- a,b,c/{p,q,r}/d/{x,y,z}//
+-- a,b,c/{p,q/{x,y,z},r},d/{p,q,r}
+-- a,b,c/{p,q/{x,y,z},r},d/{p,q,r}
+-- a{b,c}{d,e}f
+-- {a,b,c,d}
+-- {a,b,c/{p,q,r},d}
+-- {a,b,c/{p,q,r}/d/{x,y,z}//}
+-- {a,b,c/{p,q/{x,y,z}},d/{p,q,r}}
+-- {a,b,c/{p,q/{x,y,z},w}v,d/{p,q,r}}
+-- {$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,.local,}/web2c}
+
+-- this one is better and faster, but it took me a while to realize
+-- that this kind of replacement is cleaner than messy parsing and
+-- fuzzy concatenating we can probably gain a bit with selectively
+-- applying lpeg, but experiments with lpeg parsing this proved not to
+-- work that well; the parsing is ok, but dealing with the resulting
+-- table is a pain because we need to work inside-out recursively
+
+local function do_first(a,b)
+    local t = { }
+    for s in gmatch(b,"[^,]+") do t[#t+1] = a .. s end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_second(a,b)
+    local t = { }
+    for s in gmatch(a,"[^,]+") do t[#t+1] = s .. b end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_both(a,b)
+    local t = { }
+    for sa in gmatch(a,"[^,]+") do
+        for sb in gmatch(b,"[^,]+") do
+            t[#t+1] = sa .. sb
+        end
+    end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_three(a,b,c)
+    return a .. b.. c
+end
+
+local function splitpathexpr(str, t, validate)
+    -- no need for further optimization as it is only called a
+    -- few times, we can use lpeg for the sub
+    if trace_expansions then
+        logs.report("fileio","expanding variable '%s'",str)
+    end
+    t = t or { }
+    str = gsub(str,",}",",@}")
+    str = gsub(str,"{,","{@,")
+ -- str = "@" .. str .. "@"
+    local ok, done
+    while true do
+        done = false
+        while true do
+            str, ok = gsub(str,"([^{},]+){([^{}]+)}",do_first)
+            if ok > 0 then done = true else break end
+        end
+        while true do
+            str, ok = gsub(str,"{([^{}]+)}([^{},]+)",do_second)
+            if ok > 0 then done = true else break end
+        end
+        while true do
+            str, ok = gsub(str,"{([^{}]+)}{([^{}]+)}",do_both)
+            if ok > 0 then done = true else break end
+        end
+        str, ok = gsub(str,"({[^{}]*){([^{}]+)}([^{}]*})",do_three)
+        if ok > 0 then done = true end
+        if not done then break end
+    end
+    str = gsub(str,"[{}]", "")
+    str = gsub(str,"@","")
+    if validate then
+        for s in gmatch(str,"[^,]+") do
+            s = validate(s)
+            if s then t[#t+1] = s end
+        end
+    else
+        for s in gmatch(str,"[^,]+") do
+            t[#t+1] = s
+        end
+    end
+    if trace_expansions then
+        for k=1,#t do
+            logs.report("fileio","% 4i: %s",k,t[k])
+        end
+    end
+    return t
+end
+
+local function expanded_path_from_list(pathlist) -- maybe not a list, just a path
+    -- a previous version fed back into pathlist
+    local newlist, ok = { }, false
+    for k=1,#pathlist do
+        if find(pathlist[k],"[{}]") then
+            ok = true
+            break
+        end
+    end
+    if ok then
+        local function validate(s)
+            s = file.collapse_path(s)
+            return s ~= "" and not find(s,dummy_path_expr) and s
+        end
+        for k=1,#pathlist do
+            splitpathexpr(pathlist[k],newlist,validate)
+        end
+    else
+        for k=1,#pathlist do
+            for p in gmatch(pathlist[k],"([^,]+)") do
+                p = file.collapse_path(p)
+                if p ~= "" then newlist[#newlist+1] = p end
+            end
+        end
+    end
+    return newlist
+end
+
+-- we follow a rather traditional approach:
+--
+-- (1) texmf.cnf given in TEXMFCNF
+-- (2) texmf.cnf searched in default variable
+--
+-- also we now follow the stupid route: if not set then just assume *one*
+-- cnf file under texmf (i.e. distribution)
+
+local args = environment and environment.original_arguments or arg -- this needs a cleanup
+
+resolvers.ownbin = resolvers.ownbin or args[-2] or arg[-2] or args[-1] or arg[-1] or arg[0] or "luatex"
+resolvers.ownbin = gsub(resolvers.ownbin,"\\","/")
+
+function resolvers.getownpath()
+    local ownpath = resolvers.ownpath or os.selfdir
+    if not ownpath or ownpath == "" or ownpath == "unset" then
+        ownpath = args[-1] or arg[-1]
+        ownpath = ownpath and file.dirname(gsub(ownpath,"\\","/"))
+        if not ownpath or ownpath == "" then
+            ownpath = args[-0] or arg[-0]
+            ownpath = ownpath and file.dirname(gsub(ownpath,"\\","/"))
+        end
+        local binary = resolvers.ownbin
+        if not ownpath or ownpath == "" then
+            ownpath = ownpath and file.dirname(binary)
+        end
+        if not ownpath or ownpath == "" then
+            if os.binsuffix ~= "" then
+                binary = file.replacesuffix(binary,os.binsuffix)
+            end
+            for p in gmatch(os.getenv("PATH"),"[^"..io.pathseparator.."]+") do
+                local b = file.join(p,binary)
+                if lfs.isfile(b) then
+                    -- we assume that after changing to the path the currentdir function
+                    -- resolves to the real location and use this side effect here; this
+                    -- trick is needed because on the mac installations use symlinks in the
+                    -- path instead of real locations
+                    local olddir = lfs.currentdir()
+                    if lfs.chdir(p) then
+                        local pp = lfs.currentdir()
+                        if trace_locating and p ~= pp then
+                            logs.report("fileio","following symlink '%s' to '%s'",p,pp)
+                        end
+                        ownpath = pp
+                        lfs.chdir(olddir)
+                    else
+                        if trace_locating then
+                            logs.report("fileio","unable to check path '%s'",p)
+                        end
+                        ownpath =  p
+                    end
+                    break
+                end
+            end
+        end
+        if not ownpath or ownpath == "" then
+            ownpath = "."
+            logs.report("fileio","forcing fallback ownpath .")
+        elseif trace_locating then
+            logs.report("fileio","using ownpath '%s'",ownpath)
+        end
+    end
+    resolvers.ownpath = ownpath
+    function resolvers.getownpath()
+        return resolvers.ownpath
+    end
+    return ownpath
+end
+
+local own_places = { "SELFAUTOLOC", "SELFAUTODIR", "SELFAUTOPARENT", "TEXMFCNF" }
+
+local function identify_own()
+    local ownpath = resolvers.getownpath() or dir.current()
+    local ie = instance.environment
+    if ownpath then
+        if resolvers.env('SELFAUTOLOC')    == "" then os.env['SELFAUTOLOC']    = file.collapse_path(ownpath) end
+        if resolvers.env('SELFAUTODIR')    == "" then os.env['SELFAUTODIR']    = file.collapse_path(ownpath .. "/..") end
+        if resolvers.env('SELFAUTOPARENT') == "" then os.env['SELFAUTOPARENT'] = file.collapse_path(ownpath .. "/../..") end
+    else
+        logs.report("fileio","error: unable to locate ownpath")
+        os.exit()
+    end
+    if resolvers.env('TEXMFCNF') == "" then os.env['TEXMFCNF'] = resolvers.cnfdefault end
+    if resolvers.env('TEXOS')    == "" then os.env['TEXOS']    = resolvers.env('SELFAUTODIR') end
+    if resolvers.env('TEXROOT')  == "" then os.env['TEXROOT']  = resolvers.env('SELFAUTOPARENT') end
+    if trace_locating then
+        for i=1,#own_places do
+            local v = own_places[i]
+            logs.report("fileio","variable '%s' set to '%s'",v,resolvers.env(v) or "unknown")
+        end
+    end
+    identify_own = function() end
+end
+
+function resolvers.identify_cnf()
+    if #instance.cnffiles == 0 then
+        -- fallback
+        identify_own()
+        -- the real search
+        resolvers.expand_variables()
+        local t = resolvers.split_path(resolvers.env('TEXMFCNF'))
+        t = expanded_path_from_list(t)
+        expand_vars(t) -- redundant
+        local function locate(filename,list)
+            for i=1,#t do
+                local ti = t[i]
+                local texmfcnf = file.collapse_path(file.join(ti,filename))
+                if lfs.isfile(texmfcnf) then
+                    list[#list+1] = texmfcnf
+                end
+            end
+        end
+        locate(resolvers.luaname,instance.luafiles)
+        locate(resolvers.cnfname,instance.cnffiles)
+    end
+end
+
+local function load_cnf_file(fname)
+    fname = resolvers.clean_path(fname)
+    local lname = file.replacesuffix(fname,'lua')
+    if lfs.isfile(lname) then
+        local dname = file.dirname(fname) -- fname ?
+        if not instance.configuration[dname] then
+            resolvers.load_data(dname,'configuration',lname and file.basename(lname))
+            instance.order[#instance.order+1] = instance.configuration[dname]
+        end
+    else
+        f = io.open(fname)
+        if f then
+            if trace_locating then
+                logs.report("fileio","loading configuration file %s", fname)
+            end
+            local line, data, n, k, v
+            local dname = file.dirname(fname)
+            if not instance.configuration[dname] then
+                instance.configuration[dname] = { }
+                instance.order[#instance.order+1] = instance.configuration[dname]
+            end
+            local data = instance.configuration[dname]
+            while true do
+                local line, n = f:read(), 0
+                if line then
+                    while true do -- join lines
+                        line, n = gsub(line,"\\%s*$", "")
+                        if n > 0 then
+                            line = line .. f:read()
+                        else
+                            break
+                        end
+                    end
+                    if not find(line,"^[%%#]") then
+                        local l = gsub(line,"%s*%%.*$","")
+                        local k, v = match(l,"%s*(.-)%s*=%s*(.-)%s*$")
+                        if k and v and not data[k] then
+                            v = gsub(v,"[%%#].*",'')
+                            data[k] = gsub(v,"~","$HOME")
+                            instance.kpsevars[k] = true
+                        end
+                    end
+                else
+                    break
+                end
+            end
+            f:close()
+        elseif trace_locating then
+            logs.report("fileio","skipping configuration file '%s'", fname)
+        end
+    end
+end
+
+local function collapse_cnf_data() -- potential optimization: pass start index (setup and configuration are shared)
+    local order = instance.order
+    for i=1,#order do
+        local c = order[i]
+        for k,v in next, c do
+            if not instance.variables[k] then
+                if instance.environment[k] then
+                    instance.variables[k] = instance.environment[k]
+                else
+                    instance.kpsevars[k] = true
+                    instance.variables[k] = resolvers.bare_variable(v)
+                end
+            end
+        end
+    end
+end
+
+function resolvers.load_cnf()
+    local function loadoldconfigdata()
+        local cnffiles = instance.cnffiles
+        for i=1,#cnffiles do
+            load_cnf_file(cnffiles[i])
+        end
+    end
+    -- instance.cnffiles contain complete names now !
+    -- we still use a funny mix of cnf and new but soon
+    -- we will switch to lua exclusively as we only use
+    -- the file to collect the tree roots
+    if #instance.cnffiles == 0 then
+        if trace_locating then
+            logs.report("fileio","no cnf files found (TEXMFCNF may not be set/known)")
+        end
+    else
+        local cnffiles = instance.cnffiles
+        instance.rootpath = cnffiles[1]
+        for k=1,#cnffiles do
+            instance.cnffiles[k] = file.collapse_path(cnffiles[k])
+        end
+        for i=1,3 do
+            instance.rootpath = file.dirname(instance.rootpath)
+        end
+        instance.rootpath = file.collapse_path(instance.rootpath)
+        if instance.diskcache and not instance.renewcache then
+            resolvers.loadoldconfig(instance.cnffiles)
+            if instance.loaderror then
+                loadoldconfigdata()
+                resolvers.saveoldconfig()
+            end
+        else
+            loadoldconfigdata()
+            if instance.renewcache then
+                resolvers.saveoldconfig()
+            end
+        end
+        collapse_cnf_data()
+    end
+    check_configuration()
+end
+
+function resolvers.load_lua()
+    if #instance.luafiles == 0 then
+        -- yet harmless
+    else
+        instance.rootpath = instance.luafiles[1]
+        local luafiles = instance.luafiles
+        for k=1,#luafiles do
+            instance.luafiles[k] = file.collapse_path(luafiles[k])
+        end
+        for i=1,3 do
+            instance.rootpath = file.dirname(instance.rootpath)
+        end
+        instance.rootpath = file.collapse_path(instance.rootpath)
+        resolvers.loadnewconfig()
+        collapse_cnf_data()
+    end
+    check_configuration()
+end
+
+-- database loading
+
+function resolvers.load_hash()
+    resolvers.locatelists()
+    if instance.diskcache and not instance.renewcache then
+        resolvers.loadfiles()
+        if instance.loaderror then
+            resolvers.loadlists()
+            resolvers.savefiles()
+        end
+    else
+        resolvers.loadlists()
+        if instance.renewcache then
+            resolvers.savefiles()
+        end
+    end
+end
+
+function resolvers.append_hash(type,tag,name)
+    if trace_locating then
+        logs.report("fileio","hash '%s' appended",tag)
+    end
+    insert(instance.hashes, { ['type']=type, ['tag']=tag, ['name']=name } )
+end
+
+function resolvers.prepend_hash(type,tag,name)
+    if trace_locating then
+        logs.report("fileio","hash '%s' prepended",tag)
+    end
+    insert(instance.hashes, 1, { ['type']=type, ['tag']=tag, ['name']=name } )
+end
+
+function resolvers.extend_texmf_var(specification) -- crap, we could better prepend the hash
+--  local t = resolvers.expanded_path_list('TEXMF') -- full expansion
+    local t = resolvers.split_path(resolvers.env('TEXMF'))
+    insert(t,1,specification)
+    local newspec = concat(t,";")
+    if instance.environment["TEXMF"] then
+        instance.environment["TEXMF"] = newspec
+    elseif instance.variables["TEXMF"] then
+        instance.variables["TEXMF"] = newspec
+    else
+        -- weird
+    end
+    resolvers.expand_variables()
+    reset_hashes()
+end
+
+-- locators
+
+function resolvers.locatelists()
+    local texmfpaths = resolvers.clean_path_list('TEXMF')
+    for i=1,#texmfpaths do
+        local path = texmfpaths[i]
+        if trace_locating then
+            logs.report("fileio","locating list of '%s'",path)
+        end
+        resolvers.locatedatabase(file.collapse_path(path))
+    end
+end
+
+function resolvers.locatedatabase(specification)
+    return resolvers.methodhandler('locators', specification)
+end
+
+function resolvers.locators.tex(specification)
+    if specification and specification ~= '' and lfs.isdir(specification) then
+        if trace_locating then
+            logs.report("fileio","tex locator '%s' found",specification)
+        end
+        resolvers.append_hash('file',specification,filename)
+    elseif trace_locating then
+        logs.report("fileio","tex locator '%s' not found",specification)
+    end
+end
+
+-- hashers
+
+function resolvers.hashdatabase(tag,name)
+    return resolvers.methodhandler('hashers',tag,name)
+end
+
+function resolvers.loadfiles()
+    instance.loaderror = false
+    instance.files = { }
+    if not instance.renewcache then
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            resolvers.hashdatabase(hash.tag,hash.name)
+            if instance.loaderror then break end
+        end
+    end
+end
+
+function resolvers.hashers.tex(tag,name)
+    resolvers.load_data(tag,'files')
+end
+
+-- generators:
+
+function resolvers.loadlists()
+    local hashes = instance.hashes
+    for i=1,#hashes do
+        resolvers.generatedatabase(hashes[i].tag)
+    end
+end
+
+function resolvers.generatedatabase(specification)
+    return resolvers.methodhandler('generators', specification)
+end
+
+-- starting with . or .. etc or funny char
+
+local weird = lpeg.P(".")^1 + lpeg.anywhere(lpeg.S("~`!#$%^&*()={}[]:;\"\'||<>,?\n\r\t"))
+
+--~ local l_forbidden = lpeg.S("~`!#$%^&*()={}[]:;\"\'||\\/<>,?\n\r\t")
+--~ local l_confusing = lpeg.P(" ")
+--~ local l_character = lpeg.patterns.utf8
+--~ local l_dangerous = lpeg.P(".")
+
+--~ local l_normal = (l_character - l_forbidden - l_confusing - l_dangerous) * (l_character - l_forbidden - l_confusing^2)^0 * lpeg.P(-1)
+--~ ----- l_normal = l_normal * lpeg.Cc(true) + lpeg.Cc(false)
+
+--~ local function test(str)
+--~     print(str,lpeg.match(l_normal,str))
+--~ end
+--~ test("ヒラギノ明朝 Pro W3")
+--~ test("..ヒラギノ明朝 Pro W3")
+--~ test(":ヒラギノ明朝 Pro W3;")
+--~ test("ヒラギノ明朝 /Pro W3;")
+--~ test("ヒラギノ明朝 Pro  W3")
+
+function resolvers.generators.tex(specification)
+    local tag = specification
+    if trace_locating then
+        logs.report("fileio","scanning path '%s'",specification)
+    end
+    instance.files[tag] = { }
+    local files = instance.files[tag]
+    local n, m, r = 0, 0, 0
+    local spec = specification .. '/'
+    local attributes = lfs.attributes
+    local directory = lfs.dir
+    local function action(path)
+        local full
+        if path then
+            full = spec .. path .. '/'
+        else
+            full = spec
+        end
+        for name in directory(full) do
+            if not lpegmatch(weird,name) then
+         -- if lpegmatch(l_normal,name) then
+                local mode = attributes(full..name,'mode')
+                if mode == 'file' then
+                    if path then
+                        n = n + 1
+                        local f = files[name]
+                        if f then
+                            if type(f) == 'string' then
+                                files[name] = { f, path }
+                            else
+                                f[#f+1] = path
+                            end
+                        else -- probably unique anyway
+                            files[name] = path
+                            local lower = lower(name)
+                            if name ~= lower then
+                                files["remap:"..lower] = name
+                                r = r + 1
+                            end
+                        end
+                    end
+                elseif mode == 'directory' then
+                    m = m + 1
+                    if path then
+                        action(path..'/'..name)
+                    else
+                        action(name)
+                    end
+                end
+            end
+        end
+    end
+    action()
+    if trace_locating then
+        logs.report("fileio","%s files found on %s directories with %s uppercase remappings",n,m,r)
+    end
+end
+
+-- savers, todo
+
+function resolvers.savefiles()
+    resolvers.save_data('files')
+end
+
+-- A config (optionally) has the paths split in tables. Internally
+-- we join them and split them after the expansion has taken place. This
+-- is more convenient.
+
+--~ local checkedsplit = string.checkedsplit
+
+local cache = { }
+
+local splitter = lpeg.Ct(lpeg.splitat(lpeg.S(os.type == "windows" and ";" or ":;")))
+
+local function split_kpse_path(str) -- beware, this can be either a path or a {specification}
+    local found = cache[str]
+    if not found then
+        if str == "" then
+            found = { }
+        else
+            str = gsub(str,"\\","/")
+--~             local split = (find(str,";") and checkedsplit(str,";")) or checkedsplit(str,io.pathseparator)
+local split = lpegmatch(splitter,str)
+            found = { }
+            for i=1,#split do
+                local s = split[i]
+                if not find(s,"^{*unset}*") then
+                    found[#found+1] = s
+                end
+            end
+            if trace_expansions then
+                logs.report("fileio","splitting path specification '%s'",str)
+                for k=1,#found do
+                    logs.report("fileio","% 4i: %s",k,found[k])
+                end
+            end
+            cache[str] = found
+        end
+    end
+    return found
+end
+
+resolvers.split_kpse_path = split_kpse_path
+
+function resolvers.splitconfig()
+    for i=1,#instance do
+        local c = instance[i]
+        for k,v in next, c do
+            if type(v) == 'string' then
+                local t = split_kpse_path(v)
+                if #t > 1 then
+                    c[k] = t
+                end
+            end
+        end
+    end
+end
+
+function resolvers.joinconfig()
+    local order = instance.order
+    for i=1,#order do
+        local c = order[i]
+        for k,v in next, c do -- indexed?
+            if type(v) == 'table' then
+                c[k] = file.join_path(v)
+            end
+        end
+    end
+end
+
+function resolvers.split_path(str)
+    if type(str) == 'table' then
+        return str
+    else
+        return split_kpse_path(str)
+    end
+end
+
+function resolvers.join_path(str)
+    if type(str) == 'table' then
+        return file.join_path(str)
+    else
+        return str
+    end
+end
+
+function resolvers.splitexpansions()
+    local ie = instance.expansions
+    for k,v in next, ie do
+        local t, h, p = { }, { }, split_kpse_path(v)
+        for kk=1,#p do
+            local vv = p[kk]
+            if vv ~= "" and not h[vv] then
+                t[#t+1] = vv
+                h[vv] = true
+            end
+        end
+        if #t > 1 then
+            ie[k] = t
+        else
+            ie[k] = t[1]
+        end
+    end
+end
+
+-- end of split/join code
+
+function resolvers.saveoldconfig()
+    resolvers.splitconfig()
+    resolvers.save_data('configuration')
+    resolvers.joinconfig()
+end
+
+resolvers.configbanner = [[
+-- This is a Luatex configuration file created by 'luatools.lua' or
+-- 'luatex.exe' directly. For comment, suggestions and questions you can
+-- contact the ConTeXt Development Team. This configuration file is
+-- not copyrighted. [HH & TH]
+]]
+
+function resolvers.serialize(files)
+    -- This version is somewhat optimized for the kind of
+    -- tables that we deal with, so it's much faster than
+    -- the generic serializer. This makes sense because
+    -- luatools and mtxtools are called frequently. Okay,
+    -- we pay a small price for properly tabbed tables.
+    local t = { }
+    local function dump(k,v,m) -- could be moved inline
+        if type(v) == 'string' then
+            return m .. "['" .. k .. "']='" .. v .. "',"
+        elseif #v == 1 then
+            return m .. "['" .. k .. "']='" .. v[1] .. "',"
+        else
+            return m .. "['" .. k .. "']={'" .. concat(v,"','").. "'},"
+        end
+    end
+    t[#t+1] = "return {"
+    if instance.sortdata then
+	local sortedfiles = sortedkeys(files)
+	for i=1,#sortedfiles do
+	    local k = sortedfiles[i]
+            local fk  = files[k]
+            if type(fk) == 'table' then
+                t[#t+1] = "\t['" .. k .. "']={"
+		local sortedfk = sortedkeys(fk)
+        	for j=1,#sortedfk do
+                    local kk = sortedfk[j]
+                    t[#t+1] = dump(kk,fk[kk],"\t\t")
+                end
+                t[#t+1] = "\t},"
+            else
+                t[#t+1] = dump(k,fk,"\t")
+            end
+        end
+    else
+        for k, v in next, files do
+            if type(v) == 'table' then
+                t[#t+1] = "\t['" .. k .. "']={"
+                for kk,vv in next, v do
+                    t[#t+1] = dump(kk,vv,"\t\t")
+                end
+                t[#t+1] = "\t},"
+            else
+                t[#t+1] = dump(k,v,"\t")
+            end
+        end
+    end
+    t[#t+1] = "}"
+    return concat(t,"\n")
+end
+
+local data_state = { }
+
+function resolvers.data_state()
+    return data_state or { }
+end
+
+function resolvers.save_data(dataname, makename) -- untested without cache overload
+    for cachename, files in next, instance[dataname] do
+        local name = (makename or file.join)(cachename,dataname)
+        local luaname, lucname = name .. ".lua", name .. ".luc"
+        if trace_locating then
+            logs.report("fileio","preparing '%s' for '%s'",dataname,cachename)
+        end
+        for k, v in next, files do
+            if type(v) == "table" and #v == 1 then
+                files[k] = v[1]
+            end
+        end
+        local data = {
+            type    = dataname,
+            root    = cachename,
+            version = resolvers.cacheversion,
+            date    = os.date("%Y-%m-%d"),
+            time    = os.date("%H:%M:%S"),
+            content = files,
+            uuid    = os.uuid(),
+        }
+        local ok = io.savedata(luaname,resolvers.serialize(data))
+        if ok then
+            if trace_locating then
+                logs.report("fileio","'%s' saved in '%s'",dataname,luaname)
+            end
+            if utils.lua.compile(luaname,lucname,false,true) then -- no cleanup but strip
+                if trace_locating then
+                    logs.report("fileio","'%s' compiled to '%s'",dataname,lucname)
+                end
+            else
+                if trace_locating then
+                    logs.report("fileio","compiling failed for '%s', deleting file '%s'",dataname,lucname)
+                end
+                os.remove(lucname)
+            end
+        elseif trace_locating then
+            logs.report("fileio","unable to save '%s' in '%s' (access error)",dataname,luaname)
+        end
+    end
+end
+
+function resolvers.load_data(pathname,dataname,filename,makename) -- untested without cache overload
+    filename = ((not filename or (filename == "")) and dataname) or filename
+    filename = (makename and makename(dataname,filename)) or file.join(pathname,filename)
+    local blob = loadfile(filename .. ".luc") or loadfile(filename .. ".lua")
+    if blob then
+        local data = blob()
+        if data and data.content and data.type == dataname and data.version == resolvers.cacheversion then
+            data_state[#data_state+1] = data.uuid
+            if trace_locating then
+                logs.report("fileio","loading '%s' for '%s' from '%s'",dataname,pathname,filename)
+            end
+            instance[dataname][pathname] = data.content
+        else
+            if trace_locating then
+                logs.report("fileio","skipping '%s' for '%s' from '%s'",dataname,pathname,filename)
+            end
+            instance[dataname][pathname] = { }
+            instance.loaderror = true
+        end
+    elseif trace_locating then
+        logs.report("fileio","skipping '%s' for '%s' from '%s'",dataname,pathname,filename)
+    end
+end
+
+-- some day i'll use the nested approach, but not yet (actually we even drop
+-- engine/progname support since we have only luatex now)
+--
+-- first texmfcnf.lua files are located, next the cached texmf.cnf files
+--
+-- return {
+--     TEXMFBOGUS = 'effe checken of dit werkt',
+-- }
+
+function resolvers.resetconfig()
+    identify_own()
+    instance.configuration, instance.setup, instance.order, instance.loaderror = { }, { }, { }, false
+end
+
+function resolvers.loadnewconfig()
+    local luafiles = instance.luafiles
+    for i=1,#luafiles do
+        local cnf = luafiles[i]
+        local pathname = file.dirname(cnf)
+        local filename = file.join(pathname,resolvers.luaname)
+        local blob = loadfile(filename)
+        if blob then
+            local data = blob()
+            if data then
+                if trace_locating then
+                    logs.report("fileio","loading configuration file '%s'",filename)
+                end
+                if true then
+                    -- flatten to variable.progname
+                    local t = { }
+                    for k, v in next, data do -- v = progname
+                        if type(v) == "string" then
+                            t[k] = v
+                        else
+                            for kk, vv in next, v do -- vv = variable
+                                if type(vv) == "string" then
+                                    t[vv.."."..v] = kk
+                                end
+                            end
+                        end
+                    end
+                    instance['setup'][pathname] = t
+                else
+                    instance['setup'][pathname] = data
+                end
+            else
+                if trace_locating then
+                    logs.report("fileio","skipping configuration file '%s'",filename)
+                end
+                instance['setup'][pathname] = { }
+                instance.loaderror = true
+            end
+        elseif trace_locating then
+            logs.report("fileio","skipping configuration file '%s'",filename)
+        end
+        instance.order[#instance.order+1] = instance.setup[pathname]
+        if instance.loaderror then break end
+    end
+end
+
+function resolvers.loadoldconfig()
+    if not instance.renewcache then
+        local cnffiles = instance.cnffiles
+        for i=1,#cnffiles do
+            local cnf = cnffiles[i]
+            local dname = file.dirname(cnf)
+            resolvers.load_data(dname,'configuration')
+            instance.order[#instance.order+1] = instance.configuration[dname]
+            if instance.loaderror then break end
+        end
+    end
+    resolvers.joinconfig()
+end
+
+function resolvers.expand_variables()
+    local expansions, environment, variables = { }, instance.environment, instance.variables
+    local env = resolvers.env
+    instance.expansions = expansions
+    if instance.engine   ~= "" then environment['engine']   = instance.engine   end
+    if instance.progname ~= "" then environment['progname'] = instance.progname end
+    for k,v in next, environment do
+        local a, b = match(k,"^(%a+)%_(.*)%s*$")
+        if a and b then
+            expansions[a..'.'..b] = v
+        else
+            expansions[k] = v
+        end
+    end
+    for k,v in next, environment do -- move environment to expansions
+        if not expansions[k] then expansions[k] = v end
+    end
+    for k,v in next, variables do -- move variables to expansions
+        if not expansions[k] then expansions[k] = v end
+    end
+    local busy = false
+    local function resolve(a)
+        busy = true
+        return expansions[a] or env(a)
+    end
+    while true do
+        busy = false
+        for k,v in next, expansions do
+            local s, n = gsub(v,"%$([%a%d%_%-]+)",resolve)
+            local s, m = gsub(s,"%$%{([%a%d%_%-]+)%}",resolve)
+            if n > 0 or m > 0 then
+                expansions[k]= s
+            end
+        end
+        if not busy then break end
+    end
+    for k,v in next, expansions do
+        expansions[k] = gsub(v,"\\", '/')
+    end
+end
+
+function resolvers.variable(name)
+    return entry(instance.variables,name)
+end
+
+function resolvers.expansion(name)
+    return entry(instance.expansions,name)
+end
+
+function resolvers.is_variable(name)
+    return is_entry(instance.variables,name)
+end
+
+function resolvers.is_expansion(name)
+    return is_entry(instance.expansions,name)
+end
+
+function resolvers.unexpanded_path_list(str)
+    local pth = resolvers.variable(str)
+    local lst = resolvers.split_path(pth)
+    return expanded_path_from_list(lst)
+end
+
+function resolvers.unexpanded_path(str)
+    return file.join_path(resolvers.unexpanded_path_list(str))
+end
+
+do -- no longer needed
+
+    local done = { }
+
+    function resolvers.reset_extra_path()
+        local ep = instance.extra_paths
+        if not ep then
+            ep, done = { }, { }
+            instance.extra_paths = ep
+        elseif #ep > 0 then
+            instance.lists, done = { }, { }
+        end
+    end
+
+    function resolvers.register_extra_path(paths,subpaths)
+        local ep = instance.extra_paths or { }
+        local n = #ep
+        if paths and paths ~= "" then
+            if subpaths and subpaths ~= "" then
+                for p in gmatch(paths,"[^,]+") do
+                    -- we gmatch each step again, not that fast, but used seldom
+                    for s in gmatch(subpaths,"[^,]+") do
+                        local ps = p .. "/" .. s
+                        if not done[ps] then
+                            ep[#ep+1] = resolvers.clean_path(ps)
+                            done[ps] = true
+                        end
+                    end
+                end
+            else
+                for p in gmatch(paths,"[^,]+") do
+                    if not done[p] then
+                        ep[#ep+1] = resolvers.clean_path(p)
+                        done[p] = true
+                    end
+                end
+            end
+        elseif subpaths and subpaths ~= "" then
+            for i=1,n do
+                -- we gmatch each step again, not that fast, but used seldom
+                for s in gmatch(subpaths,"[^,]+") do
+                    local ps = ep[i] .. "/" .. s
+                    if not done[ps] then
+                        ep[#ep+1] = resolvers.clean_path(ps)
+                        done[ps] = true
+                    end
+                end
+            end
+        end
+        if #ep > 0 then
+            instance.extra_paths = ep -- register paths
+        end
+        if #ep > n then
+            instance.lists = { } -- erase the cache
+        end
+    end
+
+end
+
+local function made_list(instance,list)
+    local ep = instance.extra_paths
+    if not ep or #ep == 0 then
+        return list
+    else
+        local done, new = { }, { }
+        -- honour . .. ../.. but only when at the start
+        for k=1,#list do
+            local v = list[k]
+            if not done[v] then
+                if find(v,"^[%.%/]$") then
+                    done[v] = true
+                    new[#new+1] = v
+                else
+                    break
+                end
+            end
+        end
+        -- first the extra paths
+        for k=1,#ep do
+            local v = ep[k]
+            if not done[v] then
+                done[v] = true
+                new[#new+1] = v
+            end
+        end
+        -- next the formal paths
+        for k=1,#list do
+            local v = list[k]
+            if not done[v] then
+                done[v] = true
+                new[#new+1] = v
+            end
+        end
+        return new
+    end
+end
+
+function resolvers.clean_path_list(str)
+    local t = resolvers.expanded_path_list(str)
+    if t then
+        for i=1,#t do
+            t[i] = file.collapse_path(resolvers.clean_path(t[i]))
+        end
+    end
+    return t
+end
+
+function resolvers.expand_path(str)
+    return file.join_path(resolvers.expanded_path_list(str))
+end
+
+function resolvers.expanded_path_list(str)
+    if not str then
+        return ep or { } -- ep ?
+    elseif instance.savelists then
+        -- engine+progname hash
+        str = gsub(str,"%$","")
+        if not instance.lists[str] then -- cached
+            local lst = made_list(instance,resolvers.split_path(resolvers.expansion(str)))
+            instance.lists[str] = expanded_path_from_list(lst)
+        end
+        return instance.lists[str]
+    else
+        local lst = resolvers.split_path(resolvers.expansion(str))
+        return made_list(instance,expanded_path_from_list(lst))
+    end
+end
+
+function resolvers.expanded_path_list_from_var(str) -- brrr
+    local tmp = resolvers.var_of_format_or_suffix(gsub(str,"%$",""))
+    if tmp ~= "" then
+        return resolvers.expanded_path_list(tmp)
+    else
+        return resolvers.expanded_path_list(str)
+    end
+end
+
+function resolvers.expand_path_from_var(str)
+    return file.join_path(resolvers.expanded_path_list_from_var(str))
+end
+
+function resolvers.format_of_var(str)
+    return formats[str] or formats[alternatives[str]] or ''
+end
+function resolvers.format_of_suffix(str)
+    return suffixmap[file.extname(str)] or 'tex'
+end
+
+function resolvers.variable_of_format(str)
+    return formats[str] or formats[alternatives[str]] or ''
+end
+
+function resolvers.var_of_format_or_suffix(str)
+    local v = formats[str]
+    if v then
+        return v
+    end
+    v = formats[alternatives[str]]
+    if v then
+        return v
+    end
+    v = suffixmap[file.extname(str)]
+    if v then
+        return formats[isf]
+    end
+    return ''
+end
+
+function resolvers.expand_braces(str) -- output variable and brace expansion of STRING
+    local ori = resolvers.variable(str)
+    local pth = expanded_path_from_list(resolvers.split_path(ori))
+    return file.join_path(pth)
+end
+
+resolvers.isreadable = { }
+
+function resolvers.isreadable.file(name)
+    local readable = lfs.isfile(name) -- brrr
+    if trace_detail then
+        if readable then
+            logs.report("fileio","file '%s' is readable",name)
+        else
+            logs.report("fileio","file '%s' is not readable", name)
+        end
+    end
+    return readable
+end
+
+resolvers.isreadable.tex = resolvers.isreadable.file
+
+-- name
+-- name/name
+
+local function collect_files(names)
+    local filelist = { }
+    for k=1,#names do
+        local fname = names[k]
+        if trace_detail then
+            logs.report("fileio","checking name '%s'",fname)
+        end
+        local bname = file.basename(fname)
+        local dname = file.dirname(fname)
+        if dname == "" or find(dname,"^%.") then
+            dname = false
+        else
+            dname = "/" .. dname .. "$"
+        end
+        local hashes = instance.hashes
+        for h=1,#hashes do
+            local hash = hashes[h]
+            local blobpath = hash.tag
+            local files = blobpath and instance.files[blobpath]
+            if files then
+                if trace_detail then
+                    logs.report("fileio","deep checking '%s' (%s)",blobpath,bname)
+                end
+                local blobfile = files[bname]
+                if not blobfile then
+                    local rname = "remap:"..bname
+                    blobfile = files[rname]
+                    if blobfile then
+                        bname = files[rname]
+                        blobfile = files[bname]
+                    end
+                end
+                if blobfile then
+                    if type(blobfile) == 'string' then
+                        if not dname or find(blobfile,dname) then
+                            filelist[#filelist+1] = {
+                                hash.type,
+                                file.join(blobpath,blobfile,bname), -- search
+                                resolvers.concatinators[hash.type](blobpath,blobfile,bname) -- result
+                            }
+                        end
+                    else
+                        for kk=1,#blobfile do
+                            local vv = blobfile[kk]
+                            if not dname or find(vv,dname) then
+                                filelist[#filelist+1] = {
+                                    hash.type,
+                                    file.join(blobpath,vv,bname), -- search
+                                    resolvers.concatinators[hash.type](blobpath,vv,bname) -- result
+                                }
+                            end
+                        end
+                    end
+                end
+            elseif trace_locating then
+                logs.report("fileio","no match in '%s' (%s)",blobpath,bname)
+            end
+        end
+    end
+    if #filelist > 0 then
+        return filelist
+    else
+        return nil
+    end
+end
+
+function resolvers.suffix_of_format(str)
+    if suffixes[str] then
+        return suffixes[str][1]
+    else
+        return ""
+    end
+end
+
+function resolvers.suffixes_of_format(str)
+    if suffixes[str] then
+        return suffixes[str]
+    else
+        return {}
+    end
+end
+
+function resolvers.register_in_trees(name)
+    if not find(name,"^%.") then
+        instance.foundintrees[name] = (instance.foundintrees[name] or 0) + 1 -- maybe only one
+    end
+end
+
+-- split the next one up for readability (bu this module needs a cleanup anyway)
+
+local function can_be_dir(name) -- can become local
+    local fakepaths = instance.fakepaths
+    if not fakepaths[name] then
+        if lfs.isdir(name) then
+            fakepaths[name] = 1 -- directory
+        else
+            fakepaths[name] = 2 -- no directory
+        end
+    end
+    return (fakepaths[name] == 1)
+end
+
+local function collect_instance_files(filename,collected) -- todo : plugin (scanners, checkers etc)
+    local result = collected or { }
+    local stamp  = nil
+    filename = file.collapse_path(filename)
+    -- speed up / beware: format problem
+    if instance.remember then
+        stamp = filename .. "--" .. instance.engine .. "--" .. instance.progname .. "--" .. instance.format
+        if instance.found[stamp] then
+            if trace_locating then
+                logs.report("fileio","remembering file '%s'",filename)
+            end
+            return instance.found[stamp]
+        end
+    end
+    if not dangerous[instance.format or "?"] then
+        if resolvers.isreadable.file(filename) then
+            if trace_detail then
+                logs.report("fileio","file '%s' found directly",filename)
+            end
+            instance.found[stamp] = { filename }
+            return { filename }
+        end
+    end
+    if find(filename,'%*') then
+        if trace_locating then
+            logs.report("fileio","checking wildcard '%s'", filename)
+        end
+        result = resolvers.find_wildcard_files(filename)
+    elseif file.is_qualified_path(filename) then
+        if resolvers.isreadable.file(filename) then
+            if trace_locating then
+                logs.report("fileio","qualified name '%s'", filename)
+            end
+            result = { filename }
+        else
+            local forcedname, ok, suffix = "", false, file.extname(filename)
+            if suffix == "" then -- why
+                if instance.format == "" then
+                    forcedname = filename .. ".tex"
+                    if resolvers.isreadable.file(forcedname) then
+                        if trace_locating then
+                            logs.report("fileio","no suffix, forcing standard filetype 'tex'")
+                        end
+                        result, ok = { forcedname }, true
+                    end
+                else
+                    local suffixes = resolvers.suffixes_of_format(instance.format)
+                    for _, s in next, suffixes do
+                        forcedname = filename .. "." .. s
+                        if resolvers.isreadable.file(forcedname) then
+                            if trace_locating then
+                                logs.report("fileio","no suffix, forcing format filetype '%s'", s)
+                            end
+                            result, ok = { forcedname }, true
+                            break
+                        end
+                    end
+                end
+            end
+            if not ok and suffix ~= "" then
+                -- try to find in tree (no suffix manipulation), here we search for the
+                -- matching last part of the name
+                local basename = file.basename(filename)
+                local pattern = gsub(filename .. "$","([%.%-])","%%%1")
+                local savedformat = instance.format
+                local format = savedformat or ""
+                if format == "" then
+                    instance.format = resolvers.format_of_suffix(suffix)
+                end
+                if not format then
+                    instance.format = "othertextfiles" -- kind of everything, maybe texinput is better
+                end
+                --
+                if basename ~= filename then
+                    local resolved = collect_instance_files(basename)
+                    if #result == 0 then
+                        local lowered = lower(basename)
+                        if filename ~= lowered then
+                            resolved = collect_instance_files(lowered)
+                        end
+                    end
+                    resolvers.format = savedformat
+                    --
+                    for r=1,#resolved do
+                        local rr = resolved[r]
+                        if find(rr,pattern) then
+                            result[#result+1], ok = rr, true
+                        end
+                    end
+                end
+                -- a real wildcard:
+                --
+                -- if not ok then
+                --     local filelist = collect_files({basename})
+                --     for f=1,#filelist do
+                --         local ff = filelist[f][3] or ""
+                --         if find(ff,pattern) then
+                --             result[#result+1], ok = ff, true
+                --         end
+                --     end
+                -- end
+            end
+            if not ok and trace_locating then
+                logs.report("fileio","qualified name '%s'", filename)
+            end
+        end
+    else
+        -- search spec
+        local filetype, extra, done, wantedfiles, ext = '', nil, false, { }, file.extname(filename)
+        if ext == "" then
+            if not instance.force_suffixes then
+                wantedfiles[#wantedfiles+1] = filename
+            end
+        else
+            wantedfiles[#wantedfiles+1] = filename
+        end
+        if instance.format == "" then
+            if ext == "" then
+                local forcedname = filename .. '.tex'
+                wantedfiles[#wantedfiles+1] = forcedname
+                filetype = resolvers.format_of_suffix(forcedname)
+                if trace_locating then
+                    logs.report("fileio","forcing filetype '%s'",filetype)
+                end
+            else
+                filetype = resolvers.format_of_suffix(filename)
+                if trace_locating then
+                    logs.report("fileio","using suffix based filetype '%s'",filetype)
+                end
+            end
+        else
+            if ext == "" then
+                local suffixes = resolvers.suffixes_of_format(instance.format)
+                for _, s in next, suffixes do
+                    wantedfiles[#wantedfiles+1] = filename .. "." .. s
+                end
+            end
+            filetype = instance.format
+            if trace_locating then
+                logs.report("fileio","using given filetype '%s'",filetype)
+            end
+        end
+        local typespec = resolvers.variable_of_format(filetype)
+        local pathlist = resolvers.expanded_path_list(typespec)
+        if not pathlist or #pathlist == 0 then
+            -- no pathlist, access check only / todo == wildcard
+            if trace_detail then
+                logs.report("fileio","checking filename '%s', filetype '%s', wanted files '%s'",filename, filetype or '?',concat(wantedfiles," | "))
+            end
+            for k=1,#wantedfiles do
+                local fname = wantedfiles[k]
+                if fname and resolvers.isreadable.file(fname) then
+                    filename, done = fname, true
+                    result[#result+1] = file.join('.',fname)
+                    break
+                end
+            end
+            -- this is actually 'other text files' or 'any' or 'whatever'
+            local filelist = collect_files(wantedfiles)
+            local fl = filelist and filelist[1]
+            if fl then
+                filename = fl[3]
+                result[#result+1] = filename
+                done = true
+            end
+        else
+            -- list search
+            local filelist = collect_files(wantedfiles)
+            local dirlist = { }
+            if filelist then
+                for i=1,#filelist do
+                    dirlist[i] = file.dirname(filelist[i][2]) .. "/"
+                end
+            end
+            if trace_detail then
+                logs.report("fileio","checking filename '%s'",filename)
+            end
+            -- a bit messy ... esp the doscan setting here
+            local doscan
+            for k=1,#pathlist do
+                local path = pathlist[k]
+                if find(path,"^!!") then doscan  = false else doscan  = true  end
+                local pathname = gsub(path,"^!+", '')
+                done = false
+                -- using file list
+                if filelist then
+                    local expression
+                    -- compare list entries with permitted pattern -- /xx /xx//
+                    if not find(pathname,"/$") then
+                        expression = pathname .. "/"
+                    else
+                        expression = pathname
+                    end
+                    expression = gsub(expression,"([%-%.])","%%%1") -- this also influences
+                    expression = gsub(expression,"//+$", '/.*')     -- later usage of pathname
+                    expression = gsub(expression,"//", '/.-/')      -- not ok for /// but harmless
+                    expression = "^" .. expression .. "$"
+                    if trace_detail then
+                        logs.report("fileio","using pattern '%s' for path '%s'",expression,pathname)
+                    end
+                    for k=1,#filelist do
+                        local fl = filelist[k]
+                        local f = fl[2]
+                        local d = dirlist[k]
+                        if find(d,expression) then
+                            --- todo, test for readable
+                            result[#result+1] = fl[3]
+                            resolvers.register_in_trees(f) -- for tracing used files
+                            done = true
+                            if instance.allresults then
+                                if trace_detail then
+                                    logs.report("fileio","match in hash for file '%s' on path '%s', continue scanning",f,d)
+                                end
+                            else
+                                if trace_detail then
+                                    logs.report("fileio","match in hash for file '%s' on path '%s', quit scanning",f,d)
+                                end
+                                break
+                            end
+                        elseif trace_detail then
+                            logs.report("fileio","no match in hash for file '%s' on path '%s'",f,d)
+                        end
+                    end
+                end
+                if not done and doscan then
+                    -- check if on disk / unchecked / does not work at all / also zips
+                    if resolvers.splitmethod(pathname).scheme == 'file' then -- ?
+                        local pname = gsub(pathname,"%.%*$",'')
+                        if not find(pname,"%*") then
+                            local ppname = gsub(pname,"/+$","")
+                            if can_be_dir(ppname) then
+                                for k=1,#wantedfiles do
+                                    local w = wantedfiles[k]
+                                    local fname = file.join(ppname,w)
+                                    if resolvers.isreadable.file(fname) then
+                                        if trace_detail then
+                                            logs.report("fileio","found '%s' by scanning",fname)
+                                        end
+                                        result[#result+1] = fname
+                                        done = true
+                                        if not instance.allresults then break end
+                                    end
+                                end
+                            else
+                                -- no access needed for non existing path, speedup (esp in large tree with lots of fake)
+                            end
+                        end
+                    end
+                end
+                if not done and doscan then
+                    -- todo: slow path scanning
+                end
+                if done and not instance.allresults then break end
+            end
+        end
+    end
+    for k=1,#result do
+        result[k] = file.collapse_path(result[k])
+    end
+    if instance.remember then
+        instance.found[stamp] = result
+    end
+    return result
+end
+
+if not resolvers.concatinators  then resolvers.concatinators = { } end
+
+resolvers.concatinators.tex  = file.join
+resolvers.concatinators.file = resolvers.concatinators.tex
+
+function resolvers.find_files(filename,filetype,mustexist)
+    if type(mustexist) == boolean then
+        -- all set
+    elseif type(filetype) == 'boolean' then
+        filetype, mustexist = nil, false
+    elseif type(filetype) ~= 'string' then
+        filetype, mustexist = nil, false
+    end
+    instance.format = filetype or ''
+    local result = collect_instance_files(filename)
+    if #result == 0 then
+        local lowered = lower(filename)
+        if filename ~= lowered then
+            return collect_instance_files(lowered)
+        end
+    end
+    instance.format = ''
+    return result
+end
+
+function resolvers.find_file(filename,filetype,mustexist)
+    return (resolvers.find_files(filename,filetype,mustexist)[1] or "")
+end
+
+function resolvers.find_given_files(filename)
+    local bname, result = file.basename(filename), { }
+    local hashes = instance.hashes
+    for k=1,#hashes do
+        local hash = hashes[k]
+        local files = instance.files[hash.tag] or { }
+        local blist = files[bname]
+        if not blist then
+            local rname = "remap:"..bname
+            blist = files[rname]
+            if blist then
+                bname = files[rname]
+                blist = files[bname]
+            end
+        end
+        if blist then
+            if type(blist) == 'string' then
+                result[#result+1] = resolvers.concatinators[hash.type](hash.tag,blist,bname) or ""
+                if not instance.allresults then break end
+            else
+                for kk=1,#blist do
+                    local vv = blist[kk]
+                    result[#result+1] = resolvers.concatinators[hash.type](hash.tag,vv,bname) or ""
+                    if not instance.allresults then break end
+                end
+            end
+        end
+    end
+    return result
+end
+
+function resolvers.find_given_file(filename)
+    return (resolvers.find_given_files(filename)[1] or "")
+end
+
+local function doit(path,blist,bname,tag,kind,result,allresults)
+    local done = false
+    if blist and kind then
+        if type(blist) == 'string' then
+            -- make function and share code
+            if find(lower(blist),path) then
+                result[#result+1] = resolvers.concatinators[kind](tag,blist,bname) or ""
+                done = true
+            end
+        else
+            for kk=1,#blist do
+                local vv = blist[kk]
+                if find(lower(vv),path) then
+                    result[#result+1] = resolvers.concatinators[kind](tag,vv,bname) or ""
+                    done = true
+                    if not allresults then break end
+                end
+            end
+        end
+    end
+    return done
+end
+
+function resolvers.find_wildcard_files(filename) -- todo: remap:
+    local result = { }
+    local bname, dname = file.basename(filename), file.dirname(filename)
+    local path = gsub(dname,"^*/","")
+    path = gsub(path,"*",".*")
+    path = gsub(path,"-","%%-")
+    if dname == "" then
+        path = ".*"
+    end
+    local name = bname
+    name = gsub(name,"*",".*")
+    name = gsub(name,"-","%%-")
+    path = lower(path)
+    name = lower(name)
+    local files, allresults, done = instance.files, instance.allresults, false
+    if find(name,"%*") then
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            local tag, kind = hash.tag, hash.type
+            for kk, hh in next, files[hash.tag] do
+                if not find(kk,"^remap:") then
+                    if find(lower(kk),name) then
+                        if doit(path,hh,kk,tag,kind,result,allresults) then done = true end
+                        if done and not allresults then break end
+                    end
+                end
+            end
+        end
+    else
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            local tag, kind = hash.tag, hash.type
+            if doit(path,files[tag][bname],bname,tag,kind,result,allresults) then done = true end
+            if done and not allresults then break end
+        end
+    end
+    -- we can consider also searching the paths not in the database, but then
+    -- we end up with a messy search (all // in all path specs)
+    return result
+end
+
+function resolvers.find_wildcard_file(filename)
+    return (resolvers.find_wildcard_files(filename)[1] or "")
+end
+
+-- main user functions
+
+function resolvers.automount()
+    -- implemented later
+end
+
+function resolvers.load(option)
+    statistics.starttiming(instance)
+    resolvers.resetconfig()
+    resolvers.identify_cnf()
+    resolvers.load_lua() -- will become the new method
+    resolvers.expand_variables()
+    resolvers.load_cnf() -- will be skipped when we have a lua file
+    resolvers.expand_variables()
+    if option ~= "nofiles" then
+        resolvers.load_hash()
+        resolvers.automount()
+    end
+    statistics.stoptiming(instance)
+end
+
+function resolvers.for_files(command, files, filetype, mustexist)
+    if files and #files > 0 then
+        local function report(str)
+            if trace_locating then
+                logs.report("fileio",str) -- has already verbose
+            else
+                print(str)
+            end
+        end
+        if trace_locating then
+            report('') -- ?
+        end
+        for f=1,#files do
+            local file = files[f]
+            local result = command(file,filetype,mustexist)
+            if type(result) == 'string' then
+                report(result)
+            else
+                for i=1,#result do
+                    report(result[i]) -- could be unpack
+                end
+            end
+        end
+    end
+end
+
+-- strtab
+
+resolvers.var_value  = resolvers.variable   -- output the value of variable $STRING.
+resolvers.expand_var = resolvers.expansion  -- output variable expansion of STRING.
+
+function resolvers.show_path(str)     -- output search path for file type NAME
+    return file.join_path(resolvers.expanded_path_list(resolvers.format_of_var(str)))
+end
+
+-- resolvers.find_file(filename)
+-- resolvers.find_file(filename, filetype, mustexist)
+-- resolvers.find_file(filename, mustexist)
+-- resolvers.find_file(filename, filetype)
+
+function resolvers.register_file(files, name, path)
+    if files[name] then
+        if type(files[name]) == 'string' then
+            files[name] = { files[name], path }
+        else
+            files[name] = path
+        end
+    else
+        files[name] = path
+    end
+end
+
+function resolvers.splitmethod(filename)
+    if not filename then
+        return { } -- safeguard
+    elseif type(filename) == "table" then
+        return filename -- already split
+    elseif not find(filename,"://") then
+        return { scheme="file", path = filename, original=filename } -- quick hack
+    else
+        return url.hashed(filename)
+    end
+end
+
+function table.sequenced(t,sep) -- temp here
+    local s = { }
+    for k, v in next, t do -- indexed?
+        s[#s+1] = k .. "=" .. tostring(v)
+    end
+    return concat(s, sep or " | ")
+end
+
+function resolvers.methodhandler(what, filename, filetype) -- ...
+    filename = file.collapse_path(filename)
+    local specification = (type(filename) == "string" and resolvers.splitmethod(filename)) or filename -- no or { }, let it bomb
+    local scheme = specification.scheme
+    if resolvers[what][scheme] then
+        if trace_locating then
+            logs.report("fileio","handler '%s' -> '%s' -> '%s'",specification.original,what,table.sequenced(specification))
+        end
+        return resolvers[what][scheme](filename,filetype) -- todo: specification
+    else
+        return resolvers[what].tex(filename,filetype) -- todo: specification
+    end
+end
+
+function resolvers.clean_path(str)
+    if str then
+        str = gsub(str,"\\","/")
+        str = gsub(str,"^!+","")
+        str = gsub(str,"^~",resolvers.homedir)
+        return str
+    else
+        return nil
+    end
+end
+
+function resolvers.do_with_path(name,func)
+    local pathlist = resolvers.expanded_path_list(name)
+    for i=1,#pathlist do
+        func("^"..resolvers.clean_path(pathlist[i]))
+    end
+end
+
+function resolvers.do_with_var(name,func)
+    func(expanded_var(name))
+end
+
+function resolvers.with_files(pattern,handle)
+    local hashes = instance.hashes
+    for i=1,#hashes do
+        local hash = hashes[i]
+        local blobpath = hash.tag
+        local blobtype = hash.type
+        if blobpath then
+            local files = instance.files[blobpath]
+            if files then
+                for k,v in next, files do
+                    if find(k,"^remap:") then
+                        k = files[k]
+                        v = files[k] -- chained
+                    end
+                    if find(k,pattern) then
+                        if type(v) == "string" then
+                            handle(blobtype,blobpath,v,k)
+                        else
+                            for _,vv in next, v do -- indexed
+                                handle(blobtype,blobpath,vv,k)
+                            end
+                        end
+                    end
+                end
+            end
+        end
+    end
+end
+
+function resolvers.locate_format(name)
+    local barename, fmtname = gsub(name,"%.%a+$",""), ""
+    if resolvers.usecache then
+        local path = file.join(caches.setpath("formats")) -- maybe platform
+        fmtname = file.join(path,barename..".fmt") or ""
+    end
+    if fmtname == "" then
+        fmtname = resolvers.find_files(barename..".fmt")[1] or ""
+    end
+    fmtname = resolvers.clean_path(fmtname)
+    if fmtname ~= "" then
+        local barename = file.removesuffix(fmtname)
+        local luaname, lucname, luiname = barename .. ".lua", barename .. ".luc", barename .. ".lui"
+        if lfs.isfile(luiname) then
+            return barename, luiname
+        elseif lfs.isfile(lucname) then
+            return barename, lucname
+        elseif lfs.isfile(luaname) then
+            return barename, luaname
+        end
+    end
+    return nil, nil
+end
+
+function resolvers.boolean_variable(str,default)
+    local b = resolvers.expansion(str)
+    if b == "" then
+        return default
+    else
+        b = toboolean(b)
+        return (b == nil and default) or b
+    end
+end
+
+texconfig.kpse_init = false
+
+kpse = { original = kpse } setmetatable(kpse, { __index = function(k,v) return resolvers[v] end } )
+
+-- for a while
+
+input = resolvers
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-tmp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+--[[ldx--
+<p>This module deals with caching data. It sets up the paths and
+implements loaders and savers for tables. Best is to set the
+following variable. When not set, the usual paths will be
+checked. Personally I prefer the (users) temporary path.</p>
+
+</code>
+TEXMFCACHE=$TMP;$TEMP;$TMPDIR;$TEMPDIR;$HOME;$TEXMFVAR;$VARTEXMF;.
+</code>
+
+<p>Currently we do no locking when we write files. This is no real
+problem because most caching involves fonts and the chance of them
+being written at the same time is small. We also need to extend
+luatools with a recache feature.</p>
+--ldx]]--
+
+local format, lower, gsub = string.format, string.lower, string.gsub
+
+local trace_cache = false  trackers.register("resolvers.cache", function(v) trace_cache = v end) -- not used yet
+
+caches = caches or { }
+
+caches.path     = caches.path or nil
+caches.base     = caches.base or "luatex-cache"
+caches.more     = caches.more or "context"
+caches.direct   = false -- true is faster but may need huge amounts of memory
+caches.tree     = false
+caches.paths    = caches.paths or nil
+caches.force    = false
+caches.defaults = { "TEXMFCACHE", "TMPDIR", "TEMPDIR", "TMP", "TEMP", "HOME", "HOMEPATH" }
+
+function caches.temp()
+    local cachepath = nil
+    local function check(list,isenv)
+        if not cachepath then
+            for k=1,#list do
+                local v = list[k]
+                cachepath = (isenv and (os.env[v] or "")) or v or ""
+                if cachepath == "" then
+                    -- next
+                else
+                    cachepath = resolvers.clean_path(cachepath)
+                    if lfs.isdir(cachepath) and file.iswritable(cachepath) then -- lfs.attributes(cachepath,"mode") == "directory"
+                        break
+                    elseif caches.force or io.ask(format("\nShould I create the cache path %s?",cachepath), "no", { "yes", "no" }) == "yes" then
+                        dir.mkdirs(cachepath)
+                        if lfs.isdir(cachepath) and file.iswritable(cachepath) then
+                            break
+                        end
+                    end
+                end
+                cachepath = nil
+            end
+        end
+    end
+    check(resolvers.clean_path_list("TEXMFCACHE") or { })
+    check(caches.defaults,true)
+    if not cachepath then
+        print("\nfatal error: there is no valid (writable) cache path defined\n")
+        os.exit()
+    elseif not lfs.isdir(cachepath) then -- lfs.attributes(cachepath,"mode") ~= "directory"
+        print(format("\nfatal error: cache path %s is not a directory\n",cachepath))
+        os.exit()
+    end
+    cachepath = file.collapse_path(cachepath)
+    function caches.temp()
+        return cachepath
+    end
+    return cachepath
+end
+
+function caches.configpath()
+    return table.concat(resolvers.instance.cnffiles,";")
+end
+
+function caches.hashed(tree)
+    return md5.hex(gsub(lower(tree),"[\\\/]+","/"))
+end
+
+function caches.treehash()
+    local tree = caches.configpath()
+    if not tree or tree == "" then
+        return false
+    else
+        return caches.hashed(tree)
+    end
+end
+
+function caches.setpath(...)
+    if not caches.path then
+        if not caches.path then
+            caches.path = caches.temp()
+        end
+        caches.path = resolvers.clean_path(caches.path) -- to be sure
+        caches.tree = caches.tree or caches.treehash()
+        if caches.tree then
+            caches.path = dir.mkdirs(caches.path,caches.base,caches.more,caches.tree)
+        else
+            caches.path = dir.mkdirs(caches.path,caches.base,caches.more)
+        end
+    end
+    if not caches.path then
+        caches.path = '.'
+    end
+    caches.path = resolvers.clean_path(caches.path)
+    local dirs = { ... }
+    if #dirs > 0 then
+        local pth = dir.mkdirs(caches.path,...)
+        return pth
+    end
+    caches.path = dir.expand_name(caches.path)
+    return caches.path
+end
+
+function caches.definepath(category,subcategory)
+    return function()
+        return caches.setpath(category,subcategory)
+    end
+end
+
+function caches.setluanames(path,name)
+    return path .. "/" .. name .. ".tma", path .. "/" .. name .. ".tmc"
+end
+
+function caches.loaddata(path,name)
+    local tmaname, tmcname = caches.setluanames(path,name)
+    local loader = loadfile(tmcname) or loadfile(tmaname)
+    if loader then
+        loader = loader()
+        collectgarbage("step")
+        return loader
+    else
+        return false
+    end
+end
+
+--~ function caches.loaddata(path,name)
+--~     local tmaname, tmcname = caches.setluanames(path,name)
+--~     return dofile(tmcname) or dofile(tmaname)
+--~ end
+
+function caches.iswritable(filepath,filename)
+    local tmaname, tmcname = caches.setluanames(filepath,filename)
+    return file.iswritable(tmaname)
+end
+
+function caches.savedata(filepath,filename,data,raw)
+    local tmaname, tmcname = caches.setluanames(filepath,filename)
+    local reduce, simplify = true, true
+    if raw then
+        reduce, simplify = false, false
+    end
+    data.cache_uuid = os.uuid()
+    if caches.direct then
+        file.savedata(tmaname, table.serialize(data,'return',false,true,false)) -- no hex
+    else
+        table.tofile(tmaname, data,'return',false,true,false) -- maybe not the last true
+    end
+    local cleanup = resolvers.boolean_variable("PURGECACHE", false)
+    local strip = resolvers.boolean_variable("LUACSTRIP", true)
+    utils.lua.compile(tmaname, tmcname, cleanup, strip)
+end
+
+-- here we use the cache for format loading (texconfig.[formatname|jobname])
+
+--~ if tex and texconfig and texconfig.formatname and texconfig.formatname == "" then
+if tex and texconfig and (not texconfig.formatname or texconfig.formatname == "") and input and resolvers.instance then
+    if not texconfig.luaname then texconfig.luaname = "cont-en.lua" end -- or luc
+    texconfig.formatname = caches.setpath("formats") .. "/" .. gsub(texconfig.luaname,"%.lu.$",".fmt")
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-res'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+--~ print(resolvers.resolve("abc env:tmp file:cont-en.tex path:cont-en.tex full:cont-en.tex rel:zapf/one/p-chars.tex"))
+
+local upper, lower, gsub = string.upper, string.lower, string.gsub
+
+local prefixes = { }
+
+prefixes.environment = function(str)
+    return resolvers.clean_path(os.getenv(str) or os.getenv(upper(str)) or os.getenv(lower(str)) or "")
+end
+
+prefixes.relative = function(str,n)
+    if io.exists(str) then
+        -- nothing
+    elseif io.exists("./" .. str) then
+        str = "./" .. str
+    else
+        local p = "../"
+        for i=1,n or 2 do
+            if io.exists(p .. str) then
+                str = p .. str
+                break
+            else
+                p = p .. "../"
+            end
+        end
+    end
+    return resolvers.clean_path(str)
+end
+
+prefixes.auto = function(str)
+    local fullname = prefixes.relative(str)
+    if not lfs.isfile(fullname) then
+        fullname = prefixes.locate(str)
+    end
+    return fullname
+end
+
+prefixes.locate = function(str)
+    local fullname = resolvers.find_given_file(str) or ""
+    return resolvers.clean_path((fullname ~= "" and fullname) or str)
+end
+
+prefixes.filename = function(str)
+    local fullname = resolvers.find_given_file(str) or ""
+    return resolvers.clean_path(file.basename((fullname ~= "" and fullname) or str))
+end
+
+prefixes.pathname = function(str)
+    local fullname = resolvers.find_given_file(str) or ""
+    return resolvers.clean_path(file.dirname((fullname ~= "" and fullname) or str))
+end
+
+prefixes.env  = prefixes.environment
+prefixes.rel  = prefixes.relative
+prefixes.loc  = prefixes.locate
+prefixes.kpse = prefixes.locate
+prefixes.full = prefixes.locate
+prefixes.file = prefixes.filename
+prefixes.path = prefixes.pathname
+
+function resolvers.allprefixes(separator)
+    local all = table.sortedkeys(prefixes)
+    if separator then
+        for i=1,#all do
+            all[i] = all[i] .. ":"
+        end
+    end
+    return all
+end
+
+local function _resolve_(method,target)
+    if prefixes[method] then
+        return prefixes[method](target)
+    else
+        return method .. ":" .. target
+    end
+end
+
+local function resolve(str)
+    if type(str) == "table" then
+        for k=1,#str do
+            local v = str[k]
+            str[k] = resolve(v) or v
+        end
+    elseif str and str ~= "" then
+        str = gsub(str,"([a-z]+):([^ \"\']*)",_resolve_)
+    end
+    return str
+end
+
+resolvers.resolve = resolve
+
+if os.uname then
+
+    for k, v in next, os.uname() do
+        if not prefixes[k] then
+            prefixes[k] = function() return v end
+        end
+    end
+
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-inp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+resolvers.finders = resolvers.finders or { }
+resolvers.openers = resolvers.openers or { }
+resolvers.loaders = resolvers.loaders or { }
+
+resolvers.finders.notfound  = { nil }
+resolvers.openers.notfound  = { nil }
+resolvers.loaders.notfound  = { false, nil, 0 }
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-out'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+outputs = outputs or { }
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-con'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, lower, gsub = string.format, string.lower, string.gsub
+
+local trace_cache      = false  trackers.register("resolvers.cache",      function(v) trace_cache      = v end)
+local trace_containers = false  trackers.register("resolvers.containers", function(v) trace_containers = v end)
+local trace_storage    = false  trackers.register("resolvers.storage",    function(v) trace_storage    = v end)
+
+--[[ldx--
+<p>Once we found ourselves defining similar cache constructs
+several times, containers were introduced. Containers are used
+to collect tables in memory and reuse them when possible based
+on (unique) hashes (to be provided by the calling function).</p>
+
+<p>Caching to disk is disabled by default. Version numbers are
+stored in the saved table which makes it possible to change the
+table structures without bothering about the disk cache.</p>
+
+<p>Examples of usage can be found in the font related code.</p>
+--ldx]]--
+
+containers = containers or { }
+
+containers.usecache = true
+
+local function report(container,tag,name)
+    if trace_cache or trace_containers then
+        logs.report(format("%s cache",container.subcategory),"%s: %s",tag,name or 'invalid')
+    end
+end
+
+local allocated = { }
+
+-- tracing
+
+function containers.define(category, subcategory, version, enabled)
+    return function()
+        if category and subcategory then
+            local c = allocated[category]
+            if not c then
+                c  = { }
+                allocated[category] = c
+            end
+            local s = c[subcategory]
+            if not s then
+                s = {
+                    category = category,
+                    subcategory = subcategory,
+                    storage = { },
+                    enabled = enabled,
+                    version = version or 1.000,
+                    trace = false,
+                    path = caches and caches.setpath and caches.setpath(category,subcategory),
+                }
+                c[subcategory] = s
+            end
+            return s
+        else
+            return nil
+        end
+    end
+end
+
+function containers.is_usable(container, name)
+    return container.enabled and caches and caches.iswritable(container.path, name)
+end
+
+function containers.is_valid(container, name)
+    if name and name ~= "" then
+        local storage = container.storage[name]
+        return storage and storage.cache_version == container.version
+    else
+        return false
+    end
+end
+
+function containers.read(container,name)
+    if container.enabled and caches and not container.storage[name] and containers.usecache then
+        container.storage[name] = caches.loaddata(container.path,name)
+        if containers.is_valid(container,name) then
+            report(container,"loaded",name)
+        else
+            container.storage[name] = nil
+        end
+    end
+    if container.storage[name] then
+        report(container,"reusing",name)
+    end
+    return container.storage[name]
+end
+
+function containers.write(container, name, data)
+    if data then
+        data.cache_version = container.version
+        if container.enabled and caches then
+            local unique, shared = data.unique, data.shared
+            data.unique, data.shared = nil, nil
+            caches.savedata(container.path, name, data)
+            report(container,"saved",name)
+            data.unique, data.shared = unique, shared
+        end
+        report(container,"stored",name)
+        container.storage[name] = data
+    end
+    return data
+end
+
+function containers.content(container,name)
+    return container.storage[name]
+end
+
+function containers.cleanname(name)
+    return (gsub(lower(name),"[^%w%d]+","-"))
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-use'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, lower, gsub, find = string.format, string.lower, string.gsub, string.find
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+-- since we want to use the cache instead of the tree, we will now
+-- reimplement the saver.
+
+local save_data = resolvers.save_data
+local load_data = resolvers.load_data
+
+resolvers.cachepath = nil  -- public, for tracing
+resolvers.usecache  = true -- public, for tracing
+
+function resolvers.save_data(dataname)
+    save_data(dataname, function(cachename,dataname)
+        resolvers.usecache = not toboolean(resolvers.expansion("CACHEINTDS") or "false",true)
+        if resolvers.usecache then
+            resolvers.cachepath = resolvers.cachepath or caches.definepath("trees")
+            return file.join(resolvers.cachepath(),caches.hashed(cachename))
+        else
+            return file.join(cachename,dataname)
+        end
+    end)
+end
+
+function resolvers.load_data(pathname,dataname,filename)
+    load_data(pathname,dataname,filename,function(dataname,filename)
+        resolvers.usecache = not toboolean(resolvers.expansion("CACHEINTDS") or "false",true)
+        if resolvers.usecache then
+            resolvers.cachepath = resolvers.cachepath or caches.definepath("trees")
+            return file.join(resolvers.cachepath(),caches.hashed(pathname))
+        else
+            if not filename or (filename == "") then
+                filename = dataname
+            end
+            return file.join(pathname,filename)
+        end
+    end)
+end
+
+-- we will make a better format, maybe something xml or just text or lua
+
+resolvers.automounted = resolvers.automounted or { }
+
+function resolvers.automount(usecache)
+    local mountpaths = resolvers.clean_path_list(resolvers.expansion('TEXMFMOUNT'))
+    if (not mountpaths or #mountpaths == 0) and usecache then
+        mountpaths = { caches.setpath("mount") }
+    end
+    if mountpaths and #mountpaths > 0 then
+        statistics.starttiming(resolvers.instance)
+        for k=1,#mountpaths do
+            local root = mountpaths[k]
+            local f = io.open(root.."/url.tmi")
+            if f then
+                for line in f:lines() do
+                    if line then
+                        if find(line,"^[%%#%-]") then -- or %W
+                            -- skip
+                        elseif find(line,"^zip://") then
+                            if trace_locating then
+                                logs.report("fileio","mounting %s",line)
+                            end
+                            table.insert(resolvers.automounted,line)
+                            resolvers.usezipfile(line)
+                        end
+                    end
+                end
+                f:close()
+            end
+        end
+        statistics.stoptiming(resolvers.instance)
+    end
+end
+
+-- status info
+
+statistics.register("used config path", function() return caches.configpath()  end)
+statistics.register("used cache path",  function() return caches.temp() or "?" end)
+
+-- experiment (code will move)
+
+function statistics.save_fmt_status(texname,formatbanner,sourcefile) -- texname == formatname
+    local enginebanner = status.list().banner
+    if formatbanner and enginebanner and sourcefile then
+        local luvname = file.replacesuffix(texname,"luv")
+        local luvdata = {
+            enginebanner = enginebanner,
+            formatbanner = formatbanner,
+            sourcehash   = md5.hex(io.loaddata(resolvers.find_file(sourcefile)) or "unknown"),
+            sourcefile   = sourcefile,
+        }
+        io.savedata(luvname,table.serialize(luvdata,true))
+    end
+end
+
+function statistics.check_fmt_status(texname)
+    local enginebanner = status.list().banner
+    if enginebanner and texname then
+        local luvname = file.replacesuffix(texname,"luv")
+        if lfs.isfile(luvname) then
+            local luv = dofile(luvname)
+            if luv and luv.sourcefile then
+                local sourcehash = md5.hex(io.loaddata(resolvers.find_file(luv.sourcefile)) or "unknown")
+                local luvbanner = luv.enginebanner or "?"
+                if luvbanner ~= enginebanner then
+                    return string.format("engine mismatch (luv:%s <> bin:%s)",luvbanner,enginebanner)
+                end
+                local luvhash = luv.sourcehash or "?"
+                if luvhash ~= sourcehash then
+                    return string.format("source mismatch (luv:%s <> bin:%s)",luvhash,sourcehash)
+                end
+            else
+                return "invalid status file"
+            end
+        else
+            return "missing status file"
+        end
+    end
+    return true
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-zip'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, find, match = string.format, string.find, string.match
+local unpack = unpack or table.unpack
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+-- zip:///oeps.zip?name=bla/bla.tex
+-- zip:///oeps.zip?tree=tex/texmf-local
+-- zip:///texmf.zip?tree=/tex/texmf
+-- zip:///texmf.zip?tree=/tex/texmf-local
+-- zip:///texmf-mine.zip?tree=/tex/texmf-projects
+
+zip                 = zip or { }
+zip.archives        = zip.archives or { }
+zip.registeredfiles = zip.registeredfiles or { }
+
+local finders, openers, loaders = resolvers.finders, resolvers.openers, resolvers.loaders
+local locators, hashers, concatinators = resolvers.locators, resolvers.hashers, resolvers.concatinators
+
+local archives = zip.archives
+
+local function validzip(str) -- todo: use url splitter
+    if not find(str,"^zip://") then
+        return "zip:///" .. str
+    else
+        return str
+    end
+end
+
+function zip.openarchive(name)
+    if not name or name == "" then
+        return nil
+    else
+        local arch = archives[name]
+        if not arch then
+           local full = resolvers.find_file(name) or ""
+           arch = (full ~= "" and zip.open(full)) or false
+           archives[name] = arch
+        end
+       return arch
+    end
+end
+
+function zip.closearchive(name)
+    if not name or (name == "" and archives[name]) then
+        zip.close(archives[name])
+        archives[name] = nil
+    end
+end
+
+function locators.zip(specification) -- where is this used? startup zips (untested)
+    specification = resolvers.splitmethod(specification)
+    local zipfile = specification.path
+    local zfile = zip.openarchive(name) -- tricky, could be in to be initialized tree
+    if trace_locating then
+        if zfile then
+            logs.report("fileio","zip locator, archive '%s' found",specification.original)
+        else
+            logs.report("fileio","zip locator, archive '%s' not found",specification.original)
+        end
+    end
+end
+
+function hashers.zip(tag,name)
+    if trace_locating then
+        logs.report("fileio","loading zip file '%s' as '%s'",name,tag)
+    end
+    resolvers.usezipfile(format("%s?tree=%s",tag,name))
+end
+
+function concatinators.zip(tag,path,name)
+    if not path or path == "" then
+        return format('%s?name=%s',tag,name)
+    else
+        return format('%s?name=%s/%s',tag,path,name)
+    end
+end
+
+function resolvers.isreadable.zip(name)
+    return true
+end
+
+function finders.zip(specification,filetype)
+    specification = resolvers.splitmethod(specification)
+    if specification.path then
+        local q = url.query(specification.query)
+        if q.name then
+            local zfile = zip.openarchive(specification.path)
+            if zfile then
+                if trace_locating then
+                    logs.report("fileio","zip finder, archive '%s' found",specification.path)
+                end
+                local dfile = zfile:open(q.name)
+                if dfile then
+                    dfile = zfile:close()
+                    if trace_locating then
+                        logs.report("fileio","zip finder, file '%s' found",q.name)
+                    end
+                    return specification.original
+                elseif trace_locating then
+                    logs.report("fileio","zip finder, file '%s' not found",q.name)
+                end
+            elseif trace_locating then
+                logs.report("fileio","zip finder, unknown archive '%s'",specification.path)
+            end
+        end
+    end
+    if trace_locating then
+        logs.report("fileio","zip finder, '%s' not found",filename)
+    end
+    return unpack(finders.notfound)
+end
+
+function openers.zip(specification)
+    local zipspecification = resolvers.splitmethod(specification)
+    if zipspecification.path then
+        local q = url.query(zipspecification.query)
+        if q.name then
+            local zfile = zip.openarchive(zipspecification.path)
+            if zfile then
+                if trace_locating then
+                    logs.report("fileio","zip opener, archive '%s' opened",zipspecification.path)
+                end
+                local dfile = zfile:open(q.name)
+                if dfile then
+                    logs.show_open(specification)
+                    if trace_locating then
+                        logs.report("fileio","zip opener, file '%s' found",q.name)
+                    end
+                    return openers.text_opener(specification,dfile,'zip')
+                elseif trace_locating then
+                    logs.report("fileio","zip opener, file '%s' not found",q.name)
+                end
+            elseif trace_locating then
+                logs.report("fileio","zip opener, unknown archive '%s'",zipspecification.path)
+            end
+        end
+    end
+    if trace_locating then
+        logs.report("fileio","zip opener, '%s' not found",filename)
+    end
+    return unpack(openers.notfound)
+end
+
+function loaders.zip(specification)
+    specification = resolvers.splitmethod(specification)
+    if specification.path then
+        local q = url.query(specification.query)
+        if q.name then
+            local zfile = zip.openarchive(specification.path)
+            if zfile then
+                if trace_locating then
+                    logs.report("fileio","zip loader, archive '%s' opened",specification.path)
+                end
+                local dfile = zfile:open(q.name)
+                if dfile then
+                    logs.show_load(filename)
+                    if trace_locating then
+                        logs.report("fileio","zip loader, file '%s' loaded",filename)
+                    end
+                    local s = dfile:read("*all")
+                    dfile:close()
+                    return true, s, #s
+                elseif trace_locating then
+                    logs.report("fileio","zip loader, file '%s' not found",q.name)
+                end
+            elseif trace_locating then
+                logs.report("fileio","zip loader, unknown archive '%s'",specification.path)
+            end
+        end
+    end
+    if trace_locating then
+        logs.report("fileio","zip loader, '%s' not found",filename)
+    end
+    return unpack(openers.notfound)
+end
+
+-- zip:///somefile.zip
+-- zip:///somefile.zip?tree=texmf-local -> mount
+
+function resolvers.usezipfile(zipname)
+    zipname = validzip(zipname)
+    local specification = resolvers.splitmethod(zipname)
+    local zipfile = specification.path
+    if zipfile and not zip.registeredfiles[zipname] then
+        local tree = url.query(specification.query).tree or ""
+        local z = zip.openarchive(zipfile)
+        if z then
+            local instance = resolvers.instance
+            if trace_locating then
+                logs.report("fileio","zip registering, registering archive '%s'",zipname)
+            end
+            statistics.starttiming(instance)
+            resolvers.prepend_hash('zip',zipname,zipfile)
+            resolvers.extend_texmf_var(zipname) -- resets hashes too
+            zip.registeredfiles[zipname] = z
+            instance.files[zipname] = resolvers.register_zip_file(z,tree or "")
+            statistics.stoptiming(instance)
+        elseif trace_locating then
+            logs.report("fileio","zip registering, unknown archive '%s'",zipname)
+        end
+    elseif trace_locating then
+        logs.report("fileio","zip registering, '%s' not found",zipname)
+    end
+end
+
+function resolvers.register_zip_file(z,tree)
+    local files, filter = { }, ""
+    if tree == "" then
+        filter = "^(.+)/(.-)$"
+    else
+        filter = format("^%s/(.+)/(.-)$",tree)
+    end
+    if trace_locating then
+        logs.report("fileio","zip registering, using filter '%s'",filter)
+    end
+    local register, n = resolvers.register_file, 0
+    for i in z:files() do
+        local path, name = match(i.filename,filter)
+        if path then
+            if name and name ~= '' then
+                register(files, name, path)
+                n = n + 1
+            else
+                -- directory
+            end
+        else
+            register(files, i.filename, '')
+            n = n + 1
+        end
+    end
+    logs.report("fileio","zip registering, %s files registered",n)
+    return files
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-crl'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local gsub = string.gsub
+
+curl = curl or { }
+
+curl.cached    = { }
+curl.cachepath = caches.definepath("curl")
+
+local finders, openers, loaders = resolvers.finders, resolvers.openers, resolvers.loaders
+
+function curl.fetch(protocol, name)
+    local cachename = curl.cachepath() .. "/" .. gsub(name,"[^%a%d%.]+","-")
+--  cachename = gsub(cachename,"[\\/]", io.fileseparator)
+    cachename = gsub(cachename,"[\\]", "/") -- cleanup
+    if not curl.cached[name] then
+        if not io.exists(cachename) then
+            curl.cached[name] = cachename
+            local command = "curl --silent --create-dirs --output " .. cachename .. " " .. name -- no protocol .. "://"
+            os.spawn(command)
+        end
+        if io.exists(cachename) then
+            curl.cached[name] = cachename
+        else
+            curl.cached[name] = ""
+        end
+    end
+    return curl.cached[name]
+end
+
+function finders.curl(protocol,filename)
+    local foundname = curl.fetch(protocol, filename)
+    return finders.generic(protocol,foundname,filetype)
+end
+
+function openers.curl(protocol,filename)
+    return openers.generic(protocol,filename)
+end
+
+function loaders.curl(protocol,filename)
+    return loaders.generic(protocol,filename)
+end
+
+-- todo: metamethod
+
+function curl.install(protocol)
+    finders[protocol] = function (filename,filetype) return finders.curl(protocol,filename) end
+    openers[protocol] = function (filename)          return openers.curl(protocol,filename) end
+    loaders[protocol] = function (filename)          return loaders.curl(protocol,filename) end
+end
+
+curl.install('http')
+curl.install('https')
+curl.install('ftp')
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-lua'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- some loading stuff ... we might move this one to slot 2 depending
+-- on the developments (the loaders must not trigger kpse); we could
+-- of course use a more extensive lib path spec
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+local gsub, insert = string.gsub, table.insert
+local unpack = unpack or table.unpack
+
+local  libformats = { 'luatexlibs', 'tex', 'texmfscripts', 'othertextfiles' } -- 'luainputs'
+local clibformats = { 'lib' }
+
+local _path_, libpaths, _cpath_, clibpaths
+
+function package.libpaths()
+    if not _path_ or package.path ~= _path_ then
+        _path_ = package.path
+        libpaths = file.split_path(_path_,";")
+    end
+    return libpaths
+end
+
+function package.clibpaths()
+    if not _cpath_ or package.cpath ~= _cpath_ then
+        _cpath_ = package.cpath
+        clibpaths = file.split_path(_cpath_,";")
+    end
+    return clibpaths
+end
+
+local function thepath(...)
+    local t = { ... } t[#t+1] = "?.lua"
+    local path = file.join(unpack(t))
+    if trace_locating then
+        logs.report("fileio","! appending '%s' to 'package.path'",path)
+    end
+    return path
+end
+
+local p_libpaths, a_libpaths = { }, { }
+
+function package.append_libpath(...)
+    insert(a_libpath,thepath(...))
+end
+
+function package.prepend_libpath(...)
+    insert(p_libpaths,1,thepath(...))
+end
+
+-- beware, we need to return a loadfile result !
+
+local function loaded(libpaths,name,simple)
+    for i=1,#libpaths do -- package.path, might become option
+        local libpath = libpaths[i]
+        local resolved = gsub(libpath,"%?",simple)
+        if trace_locating then -- more detail
+            logs.report("fileio","! checking for '%s' on 'package.path': '%s' => '%s'",simple,libpath,resolved)
+        end
+        if resolvers.isreadable.file(resolved) then
+            if trace_locating then
+                logs.report("fileio","! lib '%s' located via 'package.path': '%s'",name,resolved)
+            end
+            return loadfile(resolved)
+        end
+    end
+end
+
+
+package.loaders[2] = function(name) -- was [#package.loaders+1]
+    if trace_locating then -- mode detail
+        logs.report("fileio","! locating '%s'",name)
+    end
+    for i=1,#libformats do
+        local format = libformats[i]
+        local resolved = resolvers.find_file(name,format) or ""
+        if trace_locating then -- mode detail
+            logs.report("fileio","! checking for '%s' using 'libformat path': '%s'",name,format)
+        end
+        if resolved ~= "" then
+            if trace_locating then
+                logs.report("fileio","! lib '%s' located via environment: '%s'",name,resolved)
+            end
+            return loadfile(resolved)
+        end
+    end
+    -- libpaths
+    local libpaths, clibpaths = package.libpaths(), package.clibpaths()
+    local simple = gsub(name,"%.lua$","")
+    local simple = gsub(simple,"%.","/")
+    local resolved = loaded(p_libpaths,name,simple) or loaded(libpaths,name,simple) or loaded(a_libpaths,name,simple)
+    if resolved then
+        return resolved
+    end
+    --
+    local libname = file.addsuffix(simple,os.libsuffix)
+    for i=1,#clibformats do
+        -- better have a dedicated loop
+        local format = clibformats[i]
+        local paths = resolvers.expanded_path_list_from_var(format)
+        for p=1,#paths do
+            local path = paths[p]
+            local resolved = file.join(path,libname)
+            if trace_locating then -- mode detail
+                logs.report("fileio","! checking for '%s' using 'clibformat path': '%s'",libname,path)
+            end
+            if resolvers.isreadable.file(resolved) then
+                if trace_locating then
+                    logs.report("fileio","! lib '%s' located via 'clibformat': '%s'",libname,resolved)
+                end
+                return package.loadlib(resolved,name)
+            end
+        end
+    end
+    for i=1,#clibpaths do -- package.path, might become option
+        local libpath = clibpaths[i]
+        local resolved = gsub(libpath,"?",simple)
+        if trace_locating then -- more detail
+            logs.report("fileio","! checking for '%s' on 'package.cpath': '%s'",simple,libpath)
+        end
+        if resolvers.isreadable.file(resolved) then
+            if trace_locating then
+                logs.report("fileio","! lib '%s' located via 'package.cpath': '%s'",name,resolved)
+            end
+            return package.loadlib(resolved,name)
+        end
+    end
+    -- just in case the distribution is messed up
+    if trace_loading then -- more detail
+        logs.report("fileio","! checking for '%s' using 'luatexlibs': '%s'",name)
+    end
+    local resolved = resolvers.find_file(file.basename(name),'luatexlibs') or ""
+    if resolved ~= "" then
+        if trace_locating then
+            logs.report("fileio","! lib '%s' located by basename via environment: '%s'",name,resolved)
+        end
+        return loadfile(resolved)
+    end
+    if trace_locating then
+        logs.report("fileio",'? unable to locate lib: %s',name)
+    end
+--  return "unable to locate " .. name
+end
+
+resolvers.loadlualib = require
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['luat-kps'] = {
+    version   = 1.001,
+    comment   = "companion to luatools.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+--[[ldx--
+<p>This file is used when we want the input handlers to behave like
+<type>kpsewhich</type>. What to do with the following:</p>
+
+<typing>
+{$SELFAUTOLOC,$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,}/web2c}
+$SELFAUTOLOC    : /usr/tex/bin/platform
+$SELFAUTODIR    : /usr/tex/bin
+$SELFAUTOPARENT : /usr/tex
+</typing>
+
+<p>How about just forgetting about them?</p>
+--ldx]]--
+
+local suffixes = resolvers.suffixes
+local formats  = resolvers.formats
+
+suffixes['gf']                       = { '<resolution>gf' }
+suffixes['pk']                       = { '<resolution>pk' }
+suffixes['base']                     = { 'base' }
+suffixes['bib']                      = { 'bib' }
+suffixes['bst']                      = { 'bst' }
+suffixes['cnf']                      = { 'cnf' }
+suffixes['mem']                      = { 'mem' }
+suffixes['mf']                       = { 'mf' }
+suffixes['mfpool']                   = { 'pool' }
+suffixes['mft']                      = { 'mft' }
+suffixes['mppool']                   = { 'pool' }
+suffixes['graphic/figure']           = { 'eps', 'epsi' }
+suffixes['texpool']                  = { 'pool' }
+suffixes['PostScript header']        = { 'pro' }
+suffixes['ist']                      = { 'ist' }
+suffixes['web']                      = { 'web', 'ch' }
+suffixes['cweb']                     = { 'w', 'web', 'ch' }
+suffixes['cmap files']               = { 'cmap' }
+suffixes['lig files']                = { 'lig' }
+suffixes['bitmap font']              = { }
+suffixes['MetaPost support']         = { }
+suffixes['TeX system documentation'] = { }
+suffixes['TeX system sources']       = { }
+suffixes['dvips config']             = { }
+suffixes['type42 fonts']             = { }
+suffixes['web2c files']              = { }
+suffixes['other text files']         = { }
+suffixes['other binary files']       = { }
+suffixes['opentype fonts']           = { 'otf' }
+
+suffixes['fmt']                      = { 'fmt' }
+suffixes['texmfscripts']             = { 'rb','lua','py','pl' }
+
+suffixes['pdftex config']            = { }
+suffixes['Troff fonts']              = { }
+
+suffixes['ls-R']                     = { }
+
+--[[ldx--
+<p>If you wondered abou tsome of the previous mappings, how about
+the next bunch:</p>
+--ldx]]--
+
+formats['bib']                      = ''
+formats['bst']                      = ''
+formats['mft']                      = ''
+formats['ist']                      = ''
+formats['web']                      = ''
+formats['cweb']                     = ''
+formats['MetaPost support']         = ''
+formats['TeX system documentation'] = ''
+formats['TeX system sources']       = ''
+formats['Troff fonts']              = ''
+formats['dvips config']             = ''
+formats['graphic/figure']           = ''
+formats['ls-R']                     = ''
+formats['other text files']         = ''
+formats['other binary files']       = ''
+
+formats['gf']                       = ''
+formats['pk']                       = ''
+formats['base']                     = 'MFBASES'
+formats['cnf']                      = ''
+formats['mem']                      = 'MPMEMS'
+formats['mf']                       = 'MFINPUTS'
+formats['mfpool']                   = 'MFPOOL'
+formats['mppool']                   = 'MPPOOL'
+formats['texpool']                  = 'TEXPOOL'
+formats['PostScript header']        = 'TEXPSHEADERS'
+formats['cmap files']               = 'CMAPFONTS'
+formats['type42 fonts']             = 'T42FONTS'
+formats['web2c files']              = 'WEB2C'
+formats['pdftex config']            = 'PDFTEXCONFIG'
+formats['texmfscripts']             = 'TEXMFSCRIPTS'
+formats['bitmap font']              = ''
+formats['lig files']                = 'LIGFONTS'
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-aux'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local find = string.find
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+function resolvers.update_script(oldname,newname) -- oldname -> own.name, not per se a suffix
+    local scriptpath = "scripts/context/lua"
+    newname = file.addsuffix(newname,"lua")
+    local oldscript = resolvers.clean_path(oldname)
+    if trace_locating then
+        logs.report("fileio","to be replaced old script %s", oldscript)
+    end
+    local newscripts = resolvers.find_files(newname) or { }
+    if #newscripts == 0 then
+        if trace_locating then
+            logs.report("fileio","unable to locate new script")
+        end
+    else
+        for i=1,#newscripts do
+            local newscript = resolvers.clean_path(newscripts[i])
+            if trace_locating then
+                logs.report("fileio","checking new script %s", newscript)
+            end
+            if oldscript == newscript then
+                if trace_locating then
+                    logs.report("fileio","old and new script are the same")
+                end
+            elseif not find(newscript,scriptpath) then
+                if trace_locating then
+                    logs.report("fileio","new script should come from %s",scriptpath)
+                end
+            elseif not (find(oldscript,file.removesuffix(newname).."$") or find(oldscript,newname.."$")) then
+                if trace_locating then
+                    logs.report("fileio","invalid new script name")
+                end
+            else
+                local newdata = io.loaddata(newscript)
+                if newdata then
+                    if trace_locating then
+                        logs.report("fileio","old script content replaced by new content")
+                    end
+                    io.savedata(oldscript,newdata)
+                    break
+                elseif trace_locating then
+                    logs.report("fileio","unable to load new script")
+                end
+            end
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-tmf'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local find, gsub, match = string.find, string.gsub, string.match
+local getenv, setenv = os.getenv, os.setenv
+
+-- loads *.tmf files in minimal tree roots (to be optimized and documented)
+
+function resolvers.check_environment(tree)
+    logs.simpleline()
+    setenv('TMP', getenv('TMP') or getenv('TEMP') or getenv('TMPDIR') or getenv('HOME'))
+    setenv('TEXOS', getenv('TEXOS') or ("texmf-" .. os.platform))
+    setenv('TEXPATH', gsub(tree or "tex","\/+$",''))
+    setenv('TEXMFOS', getenv('TEXPATH') .. "/" .. getenv('TEXOS'))
+    logs.simpleline()
+    logs.simple("preset : TEXPATH => %s", getenv('TEXPATH'))
+    logs.simple("preset : TEXOS   => %s", getenv('TEXOS'))
+    logs.simple("preset : TEXMFOS => %s", getenv('TEXMFOS'))
+    logs.simple("preset : TMP     => %s", getenv('TMP'))
+    logs.simple('')
+end
+
+function resolvers.load_environment(name) -- todo: key=value as well as lua
+    local f = io.open(name)
+    if f then
+        for line in f:lines() do
+            if find(line,"^[%%%#]") then
+                -- skip comment
+            else
+                local key, how, value = match(line,"^(.-)%s*([<=>%?]+)%s*(.*)%s*$")
+                if how then
+                    value = gsub(value,"%%(.-)%%", function(v) return getenv(v) or "" end)
+                        if how == "=" or how == "<<" then
+                            setenv(key,value)
+                    elseif how == "?" or how == "??" then
+                            setenv(key,getenv(key) or value)
+                    elseif how == "<" or how == "+=" then
+                        if getenv(key) then
+                            setenv(key,getenv(key) .. io.fileseparator .. value)
+                        else
+                            setenv(key,value)
+                        end
+                    elseif how == ">" or how == "=+" then
+                        if getenv(key) then
+                            setenv(key,value .. io.pathseparator .. getenv(key))
+                        else
+                            setenv(key,value)
+                        end
+                    end
+                end
+            end
+        end
+        f:close()
+    end
+end
+
+function resolvers.load_tree(tree)
+    if tree and tree ~= "" then
+        local setuptex = 'setuptex.tmf'
+        if lfs.attributes(tree, "mode") == "directory" then -- check if not nil
+            setuptex = tree .. "/" .. setuptex
+        else
+            setuptex = tree
+        end
+        if io.exists(setuptex) then
+            resolvers.check_environment(tree)
+            resolvers.load_environment(setuptex)
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['luat-sta'] = {
+    version   = 1.001,
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- this code is used in the updater
+
+local gmatch, match = string.gmatch, string.match
+local type = type
+
+states          = states          or { }
+states.data     = states.data     or { }
+states.hash     = states.hash     or { }
+states.tag      = states.tag      or ""
+states.filename = states.filename or ""
+
+function states.save(filename,tag)
+    tag = tag or states.tag
+    filename = file.addsuffix(filename or states.filename,'lus')
+    io.savedata(filename,
+        "-- generator : luat-sta.lua\n" ..
+        "-- state tag : " .. tag .. "\n\n" ..
+        table.serialize(states.data[tag or states.tag] or {},true)
+    )
+end
+
+function states.load(filename,tag)
+    states.filename = filename
+    states.tag = tag or "whatever"
+    states.filename = file.addsuffix(states.filename,'lus')
+    states.data[states.tag], states.hash[states.tag] = (io.exists(filename) and dofile(filename)) or { }, { }
+end
+
+function states.set_by_tag(tag,key,value,default,persistent)
+    local d, h = states.data[tag], states.hash[tag]
+    if d then
+        if type(d) == "table" then
+            local dkey, hkey = key, key
+            local pre, post = match(key,"(.+)%.([^%.]+)$")
+            if pre and post then
+                for k in gmatch(pre,"[^%.]+") do
+                    local dk = d[k]
+                    if not dk then
+                        dk = { }
+                        d[k] = dk
+                    elseif type(dk) == "string" then
+                        -- invalid table, unable to upgrade structure
+                        -- hope for the best or delete the state file
+                        break
+                    end
+                    d = dk
+                end
+                dkey, hkey = post, key
+            end
+            if type(value) == nil then
+                value = value or default
+            elseif persistent then
+                value = value or d[dkey] or default
+            else
+                value = value or default
+            end
+            d[dkey], h[hkey] = value, value
+        elseif type(d) == "string" then
+            -- weird
+            states.data[tag], states.hash[tag] = value, value
+        end
+    end
+end
+
+function states.get_by_tag(tag,key,default)
+    local h = states.hash[tag]
+    if h and h[key] then
+        return h[key]
+    else
+        local d = states.data[tag]
+        if d then
+            for k in gmatch(key,"[^%.]+") do
+                local dk = d[k]
+                if dk then
+                    d = dk
+                else
+                    return default
+                end
+            end
+            return d or default
+        end
+    end
+end
+
+function states.set(key,value,default,persistent)
+    states.set_by_tag(states.tag,key,value,default,persistent)
+end
+
+function states.get(key,default)
+    return states.get_by_tag(states.tag,key,default)
+end
+
+--~ states.data.update = {
+--~ 	["version"] = {
+--~ 		["major"] = 0,
+--~ 		["minor"] = 1,
+--~ 	},
+--~ 	["rsync"] = {
+--~ 		["server"]     = "contextgarden.net",
+--~ 		["module"]     = "minimals",
+--~ 		["repository"] = "current",
+--~ 		["flags"]      = "-rpztlv --stats",
+--~ 	},
+--~ 	["tasks"] = {
+--~ 		["update"] = true,
+--~ 		["make"]   = true,
+--~         ["delete"] = false,
+--~ 	},
+--~ 	["platform"] = {
+--~ 		["host"]  = true,
+--~ 		["other"] = {
+--~ 			["mswin"]     = false,
+--~ 			["linux"]     = false,
+--~ 			["linux-64"]  = false,
+--~ 			["osx-intel"] = false,
+--~ 			["osx-ppc"]   = false,
+--~ 			["sun"]       = false,
+--~ 		},
+--~ 	},
+--~ 	["context"] = {
+--~ 		["available"] = {"current", "beta", "alpha", "experimental"},
+--~ 		["selected"]  = "current",
+--~ 	},
+--~ 	["formats"] = {
+--~ 		["cont-en"] = true,
+--~ 		["cont-nl"] = true,
+--~ 		["cont-de"] = false,
+--~ 		["cont-cz"] = false,
+--~ 		["cont-fr"] = false,
+--~ 		["cont-ro"] = false,
+--~ 	},
+--~ 	["engine"] = {
+--~ 		["pdftex"] = {
+--~ 			["install"] = true,
+--~ 			["formats"] = {
+--~ 				["pdftex"] = true,
+--~ 			},
+--~ 		},
+--~ 		["luatex"] = {
+--~ 			["install"] = true,
+--~ 			["formats"] = {
+--~ 			},
+--~ 		},
+--~ 		["xetex"] = {
+--~ 			["install"] = true,
+--~ 			["formats"] = {
+--~ 				["xetex"] = false,
+--~ 			},
+--~ 		},
+--~ 		["metapost"] = {
+--~ 			["install"] = true,
+--~ 			["formats"] = {
+--~ 				["mpost"] = true,
+--~ 				["metafun"] = true,
+--~ 			},
+--~ 		},
+--~ 	},
+--~ 	["fonts"] = {
+--~ 	},
+--~ 	["doc"] = {
+--~ 	},
+--~ 	["modules"] = {
+--~ 		["f-urwgaramond"] = false,
+--~ 		["f-urwgothic"] = false,
+--~ 		["t-bnf"] = false,
+--~ 		["t-chromato"] = false,
+--~ 		["t-cmscbf"] = false,
+--~ 		["t-cmttbf"] = false,
+--~ 		["t-construction-plan"] = false,
+--~ 		["t-degrade"] = false,
+--~ 		["t-french"] = false,
+--~ 		["t-lettrine"] = false,
+--~ 		["t-lilypond"] = false,
+--~ 		["t-mathsets"] = false,
+--~ 		["t-tikz"] = false,
+--~ 		["t-typearea"] = false,
+--~ 		["t-vim"] = false,
+--~ 	},
+--~ }
+
+--~ states.save("teststate", "update")
+--~ states.load("teststate", "update")
+
+--~ print(states.get_by_tag("update","rsync.server","unknown"))
+--~ states.set_by_tag("update","rsync.server","oeps")
+--~ print(states.get_by_tag("update","rsync.server","unknown"))
+--~ states.save("teststate", "update")
+--~ states.load("teststate", "update")
+--~ print(states.get_by_tag("update","rsync.server","unknown"))
+
+
+end -- of closure
+-- end library merge
+
+own = { } -- not local
+
+own.libs = { -- todo: check which ones are really needed
+    'l-string.lua',
+    'l-lpeg.lua',
+    'l-table.lua',
+    'l-io.lua',
+    'l-number.lua',
+    'l-set.lua',
+    'l-os.lua',
+    'l-file.lua',
+    'l-md5.lua',
+    'l-url.lua',
+    'l-dir.lua',
+    'l-boolean.lua',
+    'l-math.lua',
+--  'l-unicode.lua',
+--  'l-tex.lua',
+    'l-utils.lua',
+    'l-aux.lua',
+--  'l-xml.lua',
+    'trac-tra.lua',
+    'lxml-tab.lua',
+    'lxml-lpt.lua',
+--  'lxml-ent.lua',
+    'lxml-mis.lua',
+    'lxml-aux.lua',
+    'lxml-xml.lua',
+    'luat-env.lua',
+    'trac-inf.lua',
+    'trac-log.lua',
+    'data-res.lua',
+    'data-tmp.lua',
+    'data-pre.lua',
+    'data-inp.lua',
+    'data-out.lua',
+    'data-con.lua',
+    'data-use.lua',
+--  'data-tex.lua',
+--  'data-bin.lua',
+    'data-zip.lua',
+    'data-crl.lua',
+    'data-lua.lua',
+    'data-kps.lua', -- so that we can replace kpsewhich
+    'data-aux.lua', -- updater
+    'data-tmf.lua', -- tree files
+    -- needed ?
+    'luat-sta.lua', -- states
+}
+
+-- We need this hack till luatex is fixed.
+--
+-- for k,v in pairs(arg) do print(k,v) end
+
+if arg and (arg[0] == 'luatex' or arg[0] == 'luatex.exe') and arg[1] == "--luaonly" then
+    arg[-1]=arg[0] arg[0]=arg[2] for k=3,#arg do arg[k-2]=arg[k] end arg[#arg]=nil arg[#arg]=nil
+end
+
+-- End of hack.
+
+own.name = (environment and environment.ownname) or arg[0]  or 'luatools.lua'
+
+
+own.path = string.match(own.name,"^(.+)[\\/].-$") or "."
+own.list = { '.' }
+if own.path ~= '.' then
+    table.insert(own.list,own.path)
+end
+table.insert(own.list,own.path.."/../../../tex/context/base")
+table.insert(own.list,own.path.."/mtx")
+table.insert(own.list,own.path.."/../sources")
+
+local function locate_libs()
+    for _, lib in pairs(own.libs) do
+        for _, pth in pairs(own.list) do
+            local filename = string.gsub(pth .. "/" .. lib,"\\","/")
+            local codeblob = loadfile(filename)
+            if codeblob then
+                codeblob()
+                own.list = { pth } -- speed up te search
+                break
+            end
+        end
+    end
+end
+
+if not resolvers then
+    locate_libs()
+end
+
+if not resolvers then
+    print("")
+    print("Mtxrun is unable to start up due to lack of libraries. You may")
+    print("try to run 'lua mtxrun.lua --selfmerge' in the path where this")
+    print("script is located (normally under ..../scripts/context/lua) which")
+    print("will make this script library independent.")
+    os.exit()
+end
+
+logs.setprogram('MTXrun',"TDS Runner Tool 1.24",environment.arguments["verbose"] or false)
+
+local instance = resolvers.reset()
+
+local trackspec = environment.argument("trackers") or environment.argument("track")
+
+if trackspec then
+    trackers.enable(trackspec)
+end
+
+runners  = runners  or { } -- global
+messages = messages or { }
+
+messages.help = [[
+--script              run an mtx script (lua prefered method) (--noquotes), no script gives list
+--execute             run a script or program (texmfstart method) (--noquotes)
+--resolve             resolve prefixed arguments
+--ctxlua              run internally (using preloaded libs)
+--internal            run script using built in libraries (same as --ctxlua)
+--locate              locate given filename
+
+--autotree            use texmf tree cf. env 'texmfstart_tree' or 'texmfstarttree'
+--tree=pathtotree     use given texmf tree (default file: 'setuptex.tmf')
+--environment=name    use given (tmf) environment file
+--path=runpath        go to given path before execution
+--ifchanged=filename  only execute when given file has changed (md checksum)
+--iftouched=old,new   only execute when given file has changed (time stamp)
+
+--make                create stubs for (context related) scripts
+--remove              remove stubs (context related) scripts
+--stubpath=binpath    paths where stubs wil be written
+--windows             create windows (mswin) stubs
+--unix                create unix (linux) stubs
+
+--verbose             give a bit more info
+--trackers=list       enable given trackers
+--engine=str          target engine
+--progname=str        format or backend
+
+--edit                launch editor with found file
+--launch (--all)      launch files like manuals, assumes os support
+
+--timedrun            run a script an time its run
+--autogenerate        regenerate databases if needed (handy when used to run context in an editor)
+
+--usekpse             use kpse as fallback (when no mkiv and cache installed, often slower)
+--forcekpse           force using kpse (handy when no mkiv and cache installed but less functionality)
+
+--prefixes            show supported prefixes
+]]
+
+runners.applications = {
+    ["lua"] = "luatex --luaonly",
+    ["luc"] = "luatex --luaonly",
+    ["pl"] = "perl",
+    ["py"] = "python",
+    ["rb"] = "ruby",
+}
+
+runners.suffixes = {
+    'rb', 'lua', 'py', 'pl'
+}
+
+runners.registered = {
+    texexec      = { 'texexec.rb',      false },  -- context mkii runner (only tool not to be luafied)
+    texutil      = { 'texutil.rb',      true  },  -- old perl based index sorter for mkii (old versions need it)
+    texfont      = { 'texfont.pl',      true  },  -- perl script that makes mkii font metric files
+    texfind      = { 'texfind.pl',      false },  -- perltk based tex searching tool, mostly used at pragma
+    texshow      = { 'texshow.pl',      false },  -- perltk based context help system, will be luafied
+ -- texwork      = { 'texwork.pl',      false },  -- perltk based editing environment, only used at pragma
+    makempy      = { 'makempy.pl',      true  },
+    mptopdf      = { 'mptopdf.pl',      true  },
+    pstopdf      = { 'pstopdf.rb',      true  },  -- converts ps (and some more) images, does some cleaning (replaced)
+--  examplex     = { 'examplex.rb',     false },
+    concheck     = { 'concheck.rb',     false },
+    runtools     = { 'runtools.rb',     true  },
+    textools     = { 'textools.rb',     true  },
+    tmftools     = { 'tmftools.rb',     true  },
+    ctxtools     = { 'ctxtools.rb',     true  },
+    rlxtools     = { 'rlxtools.rb',     true  },
+    pdftools     = { 'pdftools.rb',     true  },
+    mpstools     = { 'mpstools.rb',     true  },
+--  exatools     = { 'exatools.rb',     true  },
+    xmltools     = { 'xmltools.rb',     true  },
+--  luatools     = { 'luatools.lua',    true  },
+    mtxtools     = { 'mtxtools.rb',     true  },
+    pdftrimwhite = { 'pdftrimwhite.pl', false }
+}
+
+runners.launchers = {
+    windows = { },
+    unix = { }
+}
+
+-- like runners.libpath("framework"): looks on script's subpath
+
+function runners.libpath(...)
+    package.prepend_libpath(file.dirname(environment.ownscript),...)
+    package.prepend_libpath(file.dirname(environment.ownname)  ,...)
+end
+
+function runners.prepare()
+    local checkname = environment.argument("ifchanged")
+    if checkname and checkname ~= "" then
+        local oldchecksum = file.loadchecksum(checkname)
+        local newchecksum = file.checksum(checkname)
+        if oldchecksum == newchecksum then
+            logs.simple("file '%s' is unchanged",checkname)
+            return "skip"
+        else
+            logs.simple("file '%s' is changed, processing started",checkname)
+        end
+        file.savechecksum(checkname)
+    end
+    local oldname, newname = string.split(environment.argument("iftouched") or "", ",")
+    if oldname and newname and oldname ~= "" and newname ~= "" then
+        if not file.needs_updating(oldname,newname) then
+            logs.simple("file '%s' and '%s' have same age",oldname,newname)
+            return "skip"
+        else
+            logs.simple("file '%s' is older than '%s'",oldname,newname)
+        end
+    end
+    local tree = environment.argument('tree') or ""
+    if environment.argument('autotree') then
+        tree = os.getenv('TEXMFSTART_TREE') or os.getenv('TEXMFSTARTTREE') or tree
+    end
+    if tree and tree ~= "" then
+        resolvers.load_tree(tree)
+    end
+    local env = environment.argument('environment') or ""
+    if env and env ~= "" then
+        for _,e in pairs(string.split(env)) do
+            -- maybe force suffix when not given
+            resolvers.load_tree(e)
+        end
+    end
+    local runpath = environment.argument("path")
+    if runpath and not lfs.chdir(runpath) then
+        logs.simple("unable to change to path '%s'",runpath)
+        return "error"
+    end
+    return "run"
+end
+
+function runners.execute_script(fullname,internal,nosplit)
+    local noquote = environment.argument("noquotes")
+    if fullname and fullname ~= "" then
+        local state = runners.prepare()
+        if state == 'error' then
+            return false
+        elseif state == 'skip' then
+            return true
+        elseif state == "run" then
+            instance.progname = environment.argument("progname") or instance.progname
+            instance.format   = environment.argument("format")   or instance.format
+            local path, name, suffix, result = file.dirname(fullname), file.basename(fullname), file.extname(fullname), ""
+            if path ~= "" then
+                result = fullname
+            elseif name then
+                name = name:gsub("^int[%a]*:",function()
+                    internal = true
+                    return ""
+                end )
+                name = name:gsub("^script:","")
+                if suffix == "" and runners.registered[name] and runners.registered[name][1] then
+                    name = runners.registered[name][1]
+                    suffix = file.extname(name)
+                end
+                if suffix == "" then
+                    -- loop over known suffixes
+                    for _,s in pairs(runners.suffixes) do
+                        result = resolvers.find_file(name .. "." .. s, 'texmfscripts')
+                        if result ~= "" then
+                            break
+                        end
+                    end
+                elseif runners.applications[suffix] then
+                    result = resolvers.find_file(name, 'texmfscripts')
+                else
+                    -- maybe look on path
+                    result = resolvers.find_file(name, 'other text files')
+                end
+            end
+            if result and result ~= "" then
+                if not no_split then
+                    local before, after = environment.split_arguments(fullname) -- already done
+                    environment.arguments_before, environment.arguments_after = before, after
+                end
+                if internal then
+                    arg = { } for _,v in pairs(environment.arguments_after) do arg[#arg+1] = v end
+                    environment.ownscript = result
+                    dofile(result)
+                else
+                    local binary = runners.applications[file.extname(result)]
+                    if binary and binary ~= "" then
+                        result = binary .. " " .. result
+                    end
+                    local command = result .. " " .. environment.reconstruct_commandline(environment.arguments_after,noquote)
+                    if logs.verbose then
+                        logs.simpleline()
+                        logs.simple("executing: %s",command)
+                        logs.simpleline()
+                        logs.simpleline()
+                        io.flush()
+                    end
+                    -- no os.exec because otherwise we get the wrong return value
+                    local code = os.execute(command) -- maybe spawn
+                    if code == 0 then
+                        return true
+                    else
+                        if binary then
+                            binary = file.addsuffix(binary,os.binsuffix)
+                            for p in string.gmatch(os.getenv("PATH"),"[^"..io.pathseparator.."]+") do
+                                if lfs.isfile(file.join(p,binary)) then
+                                    return false
+                                end
+                            end
+                            logs.simpleline()
+                            logs.simple("This script needs '%s' which seems not to be installed.",binary)
+                            logs.simpleline()
+                        end
+                        return false
+                    end
+                end
+            end
+        end
+    end
+    return false
+end
+
+function runners.execute_program(fullname)
+    local noquote = environment.argument("noquotes")
+    if fullname and fullname ~= "" then
+        local state = runners.prepare()
+        if state == 'error' then
+            return false
+        elseif state == 'skip' then
+            return true
+        elseif state == "run" then
+            local before, after = environment.split_arguments(fullname)
+            environment.initialize_arguments(after)
+            fullname = fullname:gsub("^bin:","")
+            local command = fullname .. " " .. (environment.reconstruct_commandline(after or "",noquote) or "")
+            logs.simpleline()
+            logs.simple("executing: %s",command)
+            logs.simpleline()
+            logs.simpleline()
+            io.flush()
+            local code = os.exec(command) -- (fullname,unpack(after)) does not work / maybe spawn
+            return code == 0
+        end
+    end
+    return false
+end
+
+-- the --usekpse flag will fallback on kpse (hm, we can better update mtx-stubs)
+
+local windows_stub = '@echo off\013\010setlocal\013\010set ownpath=%%~dp0%%\013\010texlua "%%ownpath%%mtxrun.lua" --usekpse --execute %s %%*\013\010endlocal\013\010'
+local unix_stub    = '#!/bin/sh\010mtxrun --usekpse --execute %s \"$@\"\010'
+
+function runners.handle_stubs(create)
+    local stubpath = environment.argument('stubpath') or '.' -- 'auto' no longer subpathssupported
+    local windows  = environment.argument('windows') or environment.argument('mswin') or false
+    local unix     = environment.argument('unix') or environment.argument('linux') or false
+    if not windows and not unix then
+        if os.platform == "unix" then
+            unix = true
+        else
+            windows = true
+        end
+    end
+    for _,v in pairs(runners.registered) do
+        local name, doit = v[1], v[2]
+        if doit then
+            local base = string.gsub(file.basename(name), "%.(.-)$", "")
+            if create then
+                if windows then
+                    io.savedata(file.join(stubpath,base..".bat"),string.format(windows_stub,name))
+                    logs.simple("windows stub for '%s' created",base)
+                end
+                if unix then
+                    io.savedata(file.join(stubpath,base),string.format(unix_stub,name))
+                    logs.simple("unix stub for '%s' created",base)
+                end
+            else
+                if windows and (os.remove(file.join(stubpath,base..'.bat')) or os.remove(file.join(stubpath,base..'.cmd'))) then
+                    logs.simple("windows stub for '%s' removed", base)
+                end
+                if unix and (os.remove(file.join(stubpath,base)) or os.remove(file.join(stubpath,base..'.sh'))) then
+                    logs.simple("unix stub for '%s' removed",base)
+                end
+            end
+        end
+    end
+end
+
+function runners.resolve_string(filename)
+    if filename and filename ~= "" then
+        runners.report_location(resolvers.resolve(filename))
+    end
+end
+
+function runners.locate_file(filename)
+    -- differs from texmfstart where locate appends .com .exe .bat ... todo
+    if filename and filename ~= "" then
+        runners.report_location(resolvers.find_given_file(filename))
+    end
+end
+
+function runners.locate_platform()
+    runners.report_location(os.platform)
+end
+
+function runners.report_location(result)
+    if logs.verbose then
+        logs.simpleline()
+        if result and result ~= "" then
+            logs.simple(result)
+        else
+            logs.simple("not found")
+        end
+    else
+        io.write(result)
+    end
+end
+
+function runners.edit_script(filename) -- we assume that vim is present on most systems
+    local editor = os.getenv("MTXRUN_EDITOR") or os.getenv("TEXMFSTART_EDITOR") or os.getenv("EDITOR") or 'vim'
+    local rest = resolvers.resolve(filename)
+    if rest ~= "" then
+        local command = editor .. " " .. rest
+        if logs.verbose then
+            logs.simpleline()
+            logs.simple("starting editor: %s",command)
+            logs.simple_line()
+            logs.simple_line()
+        end
+        os.launch(command)
+    end
+end
+
+function runners.save_script_session(filename, list)
+    local t = { }
+    for i=1,#list do
+        local key = list[i]
+        t[key] = environment.arguments[key]
+    end
+    io.savedata(filename,table.serialize(t,true))
+end
+
+function runners.load_script_session(filename)
+    if lfs.isfile(filename) then
+        local t = io.loaddata(filename)
+        if t then
+            t = loadstring(t)
+            if t then t = t() end
+            for key, value in pairs(t) do
+                environment.arguments[key] = value
+            end
+        end
+    end
+end
+
+function resolvers.launch(str)
+    -- maybe we also need to test on mtxrun.launcher.suffix environment
+    -- variable or on windows consult the assoc and ftype vars and such
+    local launchers = runners.launchers[os.platform] if launchers then
+        local suffix = file.extname(str) if suffix then
+            local runner = launchers[suffix] if runner then
+                str = runner .. " " .. str
+            end
+        end
+    end
+    os.launch(str)
+end
+
+function runners.launch_file(filename)
+    instance.allresults = true
+    logs.setverbose(true)
+    local pattern = environment.arguments["pattern"]
+    if not pattern or pattern == "" then
+        pattern = filename
+    end
+    if not pattern or pattern == "" then
+        logs.simple("provide name or --pattern=")
+    else
+        local t = resolvers.find_files(pattern)
+        if not t or #t == 0 then
+            t = resolvers.find_files("*/" .. pattern)
+        end
+        if not t or #t == 0 then
+            t = resolvers.find_files("*/" .. pattern .. "*")
+        end
+        if t and #t > 0 then
+            if environment.arguments["all"] then
+                for _, v in pairs(t) do
+                    logs.simple("launching %s", v)
+                    resolvers.launch(v)
+                end
+            else
+                logs.simple("launching %s", t[1])
+                resolvers.launch(t[1])
+            end
+        else
+            logs.simple("no match for %s", pattern)
+        end
+    end
+end
+
+function runners.find_mtx_script(filename)
+    local function found(name)
+        local path = file.dirname(name)
+        if path and path ~= "" then
+            return false
+        else
+            local fullname = own and own.path and file.join(own.path,name)
+            return io.exists(fullname) and fullname
+        end
+    end
+    filename = file.addsuffix(filename,"lua")
+    local basename = file.removesuffix(file.basename(filename))
+    local suffix = file.extname(filename)
+    -- qualified path, raw name
+    local fullname = file.is_qualified_path(filename) and io.exists(filename) and filename
+    if fullname and fullname ~= "" then
+        return fullname
+    end
+    -- current path, raw name
+    fullname = "./" .. filename
+    fullname = io.exists(fullname) and fullname
+    if fullname and fullname ~= "" then
+        return fullname
+    end
+    -- mtx- prefix checking
+    local mtxprefix = (filename:find("^mtx%-") and "") or "mtx-"
+    -- context namespace, mtx-<filename>
+    fullname = mtxprefix .. filename
+    fullname = found(fullname) or resolvers.find_file(fullname)
+    if fullname and fullname ~= "" then
+        return fullname
+    end
+    -- context namespace, mtx-<filename>s
+    fullname = mtxprefix .. basename .. "s" .. "." .. suffix
+    fullname = found(fullname) or resolvers.find_file(fullname)
+    if fullname and fullname ~= "" then
+        return fullname
+    end
+    -- context namespace, mtx-<filename minus trailing s>
+    fullname = mtxprefix .. basename:gsub("s$","") .. "." .. suffix
+    fullname = found(fullname) or resolvers.find_file(fullname)
+    if fullname and fullname ~= "" then
+        return fullname
+    end
+    -- context namespace, just <filename>
+    fullname = resolvers.find_file(filename)
+    return fullname
+end
+
+function runners.execute_ctx_script(filename)
+    local arguments = environment.arguments_after
+    local fullname = runners.find_mtx_script(filename) or ""
+    if file.extname(fullname) == "cld" then
+        -- handy in editors where we force --autopdf
+        logs.simple("running cld script: %s",filename)
+        table.insert(arguments,1,fullname)
+        table.insert(arguments,"--autopdf")
+        fullname = runners.find_mtx_script("context") or ""
+    end
+    -- retry after generate but only if --autogenerate
+    if fullname == "" and environment.argument("autogenerate") then -- might become the default
+        instance.renewcache = true
+        logs.setverbose(true)
+        resolvers.load()
+        --
+        fullname = runners.find_mtx_script(filename) or ""
+    end
+    -- that should do it
+    if fullname ~= "" then
+        local state = runners.prepare()
+        if state == 'error' then
+            return false
+        elseif state == 'skip' then
+            return true
+        elseif state == "run" then
+            -- load and save ... kind of undocumented
+            arg = { } for _,v in pairs(arguments) do arg[#arg+1] = resolvers.resolve(v) end
+            environment.initialize_arguments(arg)
+            local loadname = environment.arguments['load']
+            if loadname then
+                if type(loadname) ~= "string" then loadname = file.basename(fullname) end
+                loadname = file.replacesuffix(loadname,"cfg")
+                runners.load_script_session(loadname)
+            end
+            filename = environment.files[1]
+            if logs.verbose then
+                logs.simple("using script: %s\n",fullname)
+            end
+            environment.ownscript = fullname
+            dofile(fullname)
+            local savename = environment.arguments['save']
+            if savename then
+                local save_list = runners.save_list
+                if save_list and next(save_list) then
+                    if type(savename) ~= "string" then savename = file.basename(fullname) end
+                    savename = file.replacesuffix(savename,"cfg")
+                    runners.save_script_session(savename,save_list)
+                end
+            end
+            return true
+        end
+    else
+    --  logs.setverbose(true)
+        if filename == "" or filename == "help" then
+            local context = resolvers.find_file("mtx-context.lua")
+            logs.setverbose(true)
+            if context ~= "" then
+                local result = dir.glob((string.gsub(context,"mtx%-context","mtx-*"))) -- () needed
+                local valid = { }
+                table.sort(result)
+                for i=1,#result do
+                    local scriptname = result[i]
+                    local scriptbase = string.match(scriptname,".*mtx%-([^%-]-)%.lua")
+                    if scriptbase then
+                        local data = io.loaddata(scriptname)
+                        local banner, version = string.match(data,"[\n\r]logs%.extendbanner%s*%(%s*[\"\']([^\n\r]+)%s*(%d+%.%d+)")
+                        if banner then
+                            valid[#valid+1] = { scriptbase, version, banner }
+                        end
+                    end
+                end
+                if #valid > 0 then
+                    logs.reportbanner()
+                    logs.reportline()
+                    logs.simple("no script name given, known scripts:")
+                    logs.simple()
+                    for k=1,#valid do
+                        local v = valid[k]
+                        logs.simple("%-12s  %4s  %s",v[1],v[2],v[3])
+                    end
+                end
+            else
+                logs.simple("no script name given")
+            end
+        else
+            filename = file.addsuffix(filename,"lua")
+            if file.is_qualified_path(filename) then
+                logs.simple("unknown script '%s'",filename)
+            else
+                logs.simple("unknown script '%s' or 'mtx-%s'",filename,filename)
+            end
+        end
+        return false
+    end
+end
+
+function runners.prefixes()
+    logs.reportbanner()
+    logs.reportline()
+    logs.simple(table.concat(resolvers.allprefixes(true)," "))
+end
+
+function runners.timedrun(filename) -- just for me
+    if filename and filename ~= "" then
+        runners.timed(function() os.execute(filename) end)
+    end
+end
+
+function runners.timed(action)
+    statistics.timed(action)
+end
+
+-- this is a bit dirty ... first we store the first filename and next we
+-- split the arguments so that we only see the ones meant for this script
+-- ... later we will use the second half
+
+local filename = environment.files[1] or ""
+local ok      = true
+
+local before, after = environment.split_arguments(filename)
+environment.arguments_before, environment.arguments_after = before, after
+environment.initialize_arguments(before)
+
+instance.engine   = environment.argument("engine")   or 'luatex'
+instance.progname = environment.argument("progname") or 'context'
+instance.lsrmode  = environment.argument("lsr")      or false
+
+-- maybe the unset has to go to this level
+
+local is_mkii_stub = runners.registered[file.removesuffix(file.basename(filename))]
+
+if environment.argument("usekpse") or environment.argument("forcekpse") or is_mkii_stub then
+
+    os.setenv("engine","")
+    os.setenv("progname","")
+
+    local remapper = {
+        otf = "opentype fonts",
+        ttf = "truetype fonts",
+        ttc = "truetype fonts",
+        pfb = "type1 fonts",
+        other = "other text files",
+    }
+
+    local function kpse_initialized()
+        texconfig.kpse_init = true
+        local t = os.clock()
+        local k = kpse.original.new("luatex",instance.progname)
+        local dummy = k:find_file("mtxrun.lua") -- so that we're initialized
+        logs.simple("kpse fallback with progname '%s' initialized in %s seconds",instance.progname,os.clock()-t)
+        kpse_initialized = function() return k end
+        return k
+    end
+
+    local find_file = resolvers.find_file
+    local show_path = resolvers.show_path
+
+    if environment.argument("forcekpse") then
+
+        function resolvers.find_file(name,kind)
+            return (kpse_initialized():find_file(resolvers.clean_path(name),(kind ~= "" and (remapper[kind] or kind)) or "tex") or "") or ""
+        end
+        function resolvers.show_path(name)
+            return (kpse_initialized():show_path(name)) or ""
+        end
+
+    elseif environment.argument("usekpse") or is_mkii_stub then
+
+        resolvers.load()
+
+        function resolvers.find_file(name,kind)
+            local found = find_file(name,kind) or ""
+            if found ~= "" then
+                return found
+            else
+                return (kpse_initialized():find_file(resolvers.clean_path(name),(kind ~= "" and (remapper[kind] or kind)) or "tex") or "") or ""
+            end
+        end
+        function resolvers.show_path(name)
+            local found = show_path(name) or ""
+            if found ~= "" then
+                return found
+            else
+                return (kpse_initialized():show_path(name)) or ""
+            end
+        end
+
+    end
+
+else
+
+    resolvers.load()
+
+end
+
+if environment.argument("selfmerge") then
+    -- embed used libraries
+    utils.merger.selfmerge(own.name,own.libs,own.list)
+elseif environment.argument("selfclean") then
+    -- remove embedded libraries
+    utils.merger.selfclean(own.name)
+elseif environment.argument("selfupdate") then
+    logs.setverbose(true)
+    resolvers.update_script(own.name,"mtxrun")
+elseif environment.argument("ctxlua") or environment.argument("internal") then
+    -- run a script by loading it (using libs)
+    ok = runners.execute_script(filename,true)
+elseif environment.argument("script") or environment.argument("scripts") then
+    -- run a script by loading it (using libs), pass args
+    if is_mkii_stub then
+    -- execute mkii script
+        ok = runners.execute_script(filename,false,true)
+    else
+        ok = runners.execute_ctx_script(filename)
+    end
+elseif environment.argument("execute") then
+    -- execute script
+    ok = runners.execute_script(filename)
+elseif environment.argument("direct") then
+    -- equals bin:
+    ok = runners.execute_program(filename)
+elseif environment.argument("edit") then
+    -- edit file
+    runners.edit_script(filename)
+elseif environment.argument("launch") then
+    runners.launch_file(filename)
+elseif environment.argument("make") then
+    -- make stubs
+    runners.handle_stubs(true)
+elseif environment.argument("remove") then
+    -- remove stub
+    runners.handle_stubs(false)
+elseif environment.argument("resolve") then
+    -- resolve string
+    runners.resolve_string(filename)
+elseif environment.argument("locate") then
+    -- locate file
+    runners.locate_file(filename)
+elseif environment.argument("platform")then
+    -- locate platform
+    runners.locate_platform()
+elseif environment.argument("prefixes") then
+    runners.prefixes()
+elseif environment.argument("timedrun") then
+    -- locate platform
+    runners.timedrun(filename)
+elseif environment.argument("help") or filename=='help' or filename == "" then
+    logs.help(messages.help)
+    -- execute script
+elseif filename:find("^bin:") then
+    ok = runners.execute_program(filename)
+elseif is_mkii_stub then
+    -- execute mkii script
+    ok = runners.execute_script(filename,false,true)
+else
+    ok = runners.execute_ctx_script(filename)
+    if not ok then
+        ok = runners.execute_script(filename)
+    end
+end
+
+if os.platform == "unix" then
+    io.write("\n")
+end
+
+if ok == false then ok = 1 elseif ok == true then ok = 0 end
+
+os.exit(ok)
diff --git a/scripts/context/stubs/mswin/texexec.exe b/scripts/context/stubs/mswin/texexec.exe
new file mode 100644
index 000000000..2d45f2749
Binary files /dev/null and b/scripts/context/stubs/mswin/texexec.exe differ
diff --git a/scripts/context/stubs/mswin/texmfstart.exe b/scripts/context/stubs/mswin/texmfstart.exe
new file mode 100644
index 000000000..2d45f2749
Binary files /dev/null and b/scripts/context/stubs/mswin/texmfstart.exe differ
diff --git a/scripts/context/stubs/source/mtxrun_dll.c b/scripts/context/stubs/source/mtxrun_dll.c
new file mode 100644
index 000000000..5b7cd31a0
--- /dev/null
+++ b/scripts/context/stubs/source/mtxrun_dll.c
@@ -0,0 +1,221 @@
+/************************************************************************
+
+  Copyright:
+
+  Public Domain
+  Originally written in 2010 by Tomasz M. Trzeciak and Hans Hagen
+
+  This program is derived from the 'runscript' program originally 
+  written in 2009 by T.M. Trzeciak. It has been adapted for use in  
+  ConTeXt MkIV.
+
+  Comment:
+
+  In ConTeXt MkIV we have two core scripts: luatools.lua and
+  mtxrun.lua where the second one is used to launch other scripts.
+  Normally a user will use a call like:
+
+  mtxrun --script font --reload
+
+  Here mtxrun is a lua script. In order to avoid the usage of a cmd
+  file on windows this runner will start texlua directly. If the 
+  shared library luatex.dll is available, texlua will be started in 
+  the same process avoiding thus any additional overhead. Otherwise 
+  it will be spawned in a new proces.
+
+  We also don't want to use other runners, like those that use kpse
+  to locate the script as this is exactly what mtxrun itself is doing
+  already. Therefore the runscript program is adapted to a more direct
+  approach suitable for mtxrun.
+  
+  Compilation:
+
+  with gcc (size optimized):
+
+  gcc -Os -s -shared -o mtxrun.dll mtxrun_dll.c 
+  gcc -Os -s -o mtxrun.exe mtxrun_exe.c -L./ -lmtxrun
+
+  with tcc (extra small size):
+  
+  tcc -shared -o mtxrun.dll mtxrun_dll.c 
+  tcc -o mtxrun.exe mtxrun_exe.c mtxrun.def
+
+************************************************************************/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <windows.h>
+
+//#define STATIC
+#define IS_WHITESPACE(c) ((c == ' ') || (c == '\t'))
+#define MAX_CMD 32768
+#define DIE(...) { \
+  fprintf( stderr, "mtxrun: " ); \
+  fprintf( stderr, __VA_ARGS__ ); \
+  return 1; \
+}
+
+char texlua_name[] = "texlua"; // just a bare name, luatex strips the rest anyway
+static char cmdline[MAX_CMD];
+static char dirpath[MAX_PATH];
+static char progname[MAX_PATH];
+static char scriptpath[MAX_PATH];
+static char luatexpath[MAX_PATH];
+HMODULE dllluatex = NULL;
+typedef int ( *mainlikeproc )( int, char ** );
+
+#ifdef STATIC
+int main( int argc, char *argv[] ) 
+#else
+__declspec(dllexport) int dllrunscript( int argc, char *argv[] ) 
+#endif
+{
+  char *s, *luatexfname, *argstr, **lua_argv;
+  int k, quoted, lua_argc;
+  int passprogname = 0;
+
+  // directory of this module/executable
+  
+  HMODULE module_handle = GetModuleHandle( "mtxrun.dll" ); 
+  // if ( module_handle == NULL ) exe path will be used, which is OK too
+  k = (int) GetModuleFileName( module_handle, dirpath, MAX_PATH );
+  if ( !k || ( k == MAX_PATH ) ) 
+    DIE( "unable to determine a valid module name\n" );
+  s = strrchr(dirpath, '\\');
+  if ( s == NULL ) DIE( "no directory part in module path: %s\n", dirpath );
+  *(++s) = '\0'; //remove file name, leave trailing backslash
+  
+  // program name
+  
+  k = strlen(argv[0]);
+  while ( k && (argv[0][k-1] != '/') && (argv[0][k-1] != '\\') ) k--;
+  strcpy(progname, &argv[0][k]);
+  s = progname;
+  if ( s = strrchr(s, '.') ) *s = '\0'; // remove file extension part
+  
+  // script path
+  
+  strcpy( scriptpath, dirpath );
+  k = strlen(progname);
+  if ( k < 6 ) k = 6; // in case the program name is shorter than "mtxrun"
+  if ( strlen(dirpath) + k + 4 >=  MAX_PATH ) 
+    DIE( "path too long: %s%s\n", dirpath, progname );
+  if ( ( strcmpi(progname,"mtxrun") == 0 ) || ( strcmpi(progname,"luatools") == 0 ) ) {
+    strcat( scriptpath, progname );
+    strcat( scriptpath, ".lua" );
+  } else {
+    strcat( scriptpath, "mtxrun.lua" );
+    if ( strcmpi(progname,"texmfstart") != 0 ) passprogname = 1;
+  }
+  if ( GetFileAttributes(scriptpath) == INVALID_FILE_ATTRIBUTES ) 
+    DIE( "file not found: %s\n", scriptpath );
+  
+  // find texlua.exe
+  
+  if ( !SearchPath( 
+    getenv( "PATH" ), // path to search (optional)
+    "texlua.exe",     // file name to search
+    NULL,             // file extension to add (optional)
+    MAX_PATH,         // output buffer size
+    luatexpath,       // output buffer pointer
+    &luatexfname )    // pointer to a file part in the output buffer (optional)
+  ) DIE( "unable to locate texlua.exe on the search path" );
+
+  // link directly with luatex.dll if available in texlua's dir
+
+  strcpy( luatexfname, "luatex.dll" );
+  if ( dllluatex = LoadLibrary(luatexpath) )
+  {
+    mainlikeproc dllluatexmain = (mainlikeproc) GetProcAddress( dllluatex, "dllluatexmain" );
+    if ( dllluatexmain == NULL ) 
+      DIE( "unable to locate dllluatexmain procedure in luatex.dll" );
+    
+    // set up argument list for texlua script
+    
+    lua_argv = (char **)malloc( (argc + 4) * sizeof(char *) );
+    if ( lua_argv == NULL ) DIE( "out of memory\n" );
+    lua_argv[lua_argc=0] = texlua_name;
+    lua_argv[++lua_argc] = scriptpath; // script to execute
+    if (passprogname) {
+      lua_argv[++lua_argc] = "--script";
+      lua_argv[++lua_argc] = progname;
+    } 
+    for ( k = 1; k < argc; k++ ) lua_argv[++lua_argc] = argv[k];
+    lua_argv[++lua_argc] = NULL;
+
+    // call texlua interpreter
+    // dllluatexmain  never returns, but we pretend that it does
+    
+    k = dllluatexmain( lua_argc, lua_argv );
+    if (lua_argv) free( lua_argv );
+    return k;
+  }
+  
+  // we are still here, so no luatex.dll; spawn texlua.exe instead
+
+  strcpy( luatexfname, "texlua.exe" );
+  strcpy( cmdline, "\"" );
+  strcat( cmdline, luatexpath );
+  strcat( cmdline, "\" \"" );
+  strcat( cmdline, scriptpath );
+  strcat( cmdline, "\"" );
+  if (passprogname) {
+    strcat( cmdline, " --script " );
+    strcat( cmdline, progname );
+  }
+ 
+  argstr = GetCommandLine(); // get the command line of this process
+  if ( argstr == NULL ) DIE( "unable to retrieve the command line string\n" );
+
+  // skip over argv[0] in the argument string
+  // (it can contain embedded double quotes if launched from cmd.exe!)
+  
+  for ( quoted = 0; (*argstr) && ( !IS_WHITESPACE(*argstr) || quoted ); argstr++ ) 
+    if (*argstr == '"') quoted = !quoted;
+    
+  // pass through all the arguments
+  
+  if ( strlen(cmdline) + strlen(argstr) >= MAX_CMD ) 
+    DIE( "command line string too long:\n%s%s\n", cmdline, argstr );
+  strcat( cmdline, argstr ); 
+  
+  // create child process
+  
+  STARTUPINFO si;
+  PROCESS_INFORMATION pi;
+  ZeroMemory( &si, sizeof(si) );
+  si.cb = sizeof(si);
+	si.dwFlags = STARTF_USESTDHANDLES;// | STARTF_USESHOWWINDOW;
+	//si.dwFlags = STARTF_USESHOWWINDOW;
+	//si.wShowWindow = SW_HIDE ; // can be used to hide console window (requires STARTF_USESHOWWINDOW flag)
+	si.hStdInput  = GetStdHandle( STD_INPUT_HANDLE );
+	si.hStdOutput = GetStdHandle( STD_OUTPUT_HANDLE );
+	si.hStdError  = GetStdHandle( STD_ERROR_HANDLE );
+  ZeroMemory( &pi, sizeof(pi) );
+  
+  if( !CreateProcess(
+    NULL,     // module name (uses command line if NULL)
+    cmdline,  // command line
+    NULL,     // process security atrributes
+    NULL,     // thread security atrributes
+    TRUE,     // handle inheritance
+    0,        // creation flags, e.g. CREATE_NEW_CONSOLE, CREATE_NO_WINDOW, DETACHED_PROCESS
+    NULL,     // pointer to environment block (uses parent if NULL)
+    NULL,     // starting directory (uses parent if NULL)
+    &si,      // STARTUPINFO structure
+    &pi )     // PROCESS_INFORMATION structure
+  ) DIE( "command execution failed: %s\n", cmdline );
+  
+  DWORD ret = 0;
+  CloseHandle( pi.hThread ); // thread handle is not needed
+  if ( WaitForSingleObject( pi.hProcess, INFINITE ) == WAIT_OBJECT_0 ) {
+    if ( !GetExitCodeProcess( pi.hProcess, &ret) ) 
+        DIE( "unable to retrieve process exit code: %s\n", cmdline );
+  } else DIE( "failed to wait for process termination: %s\n", cmdline );
+  CloseHandle( pi.hProcess );
+  
+  // propagate exit code from the child process
+  
+  return ret; 
+ 
+}
diff --git a/scripts/context/stubs/source/mtxrun_exe.c b/scripts/context/stubs/source/mtxrun_exe.c
new file mode 100644
index 000000000..0c27c272e
--- /dev/null
+++ b/scripts/context/stubs/source/mtxrun_exe.c
@@ -0,0 +1,8 @@
+// This is the .exe part of the mtxrun program, see mtxrun_dll.c
+// for more details.
+
+#include <windows.h>
+
+__declspec(dllimport) int dllrunscript( int argc, char *argv[] );
+
+int main( int argc, char *argv[] ) { return dllrunscript( argc, argv ); }
diff --git a/scripts/context/stubs/source/readme.txt b/scripts/context/stubs/source/readme.txt
new file mode 100644
index 000000000..354d85b09
--- /dev/null
+++ b/scripts/context/stubs/source/readme.txt
@@ -0,0 +1,36 @@
+Copyright:
+
+The originally 'runscript' program was written by in 2009 by
+T.M.Trzeciak and is public domain. This derived mtxrun program
+is an adapted version by Hans Hagen.
+
+Comment:
+
+In ConTeXt MkIV we have two core scripts: luatools.lua and
+mtxrun.lua where the second one is used to launch other scripts.
+Normally a user will use a call like:
+
+mtxrun --script font --reload
+
+Here mtxrun is a lua script. In order to avoid the usage of a cmd
+file on windows this runner will start texlua directly. In TeXlive
+a runner is added for each cmd file but we don't want that overhead
+(and extra files). By using an exe we can call these scripts in
+batch files without the need for using call.
+
+We also don't want to use other runners, like those that use kpse
+to locate the script as this is exactly what mtxrun itself is doing
+already. Therefore the runscript program is adapted to a more direct
+approach suitable for mtxrun.
+
+Compilation:
+
+with gcc (size optimized):
+
+gcc -Os -s -shared -o mtxrun.dll mtxrun_dll.c
+gcc -Os -s -o mtxrun.exe mtxrun_exe.c -L./ -lmtxrun
+
+with tcc (ver. 0.9.24), extra small size
+
+tcc -shared -o runscript.dll runscript_dll.c
+tcc -o runscript.exe runscript_exe.c runscript.def
diff --git a/scripts/context/stubs/unix/context b/scripts/context/stubs/unix/context
new file mode 100644
index 000000000..fa62ba8d1
--- /dev/null
+++ b/scripts/context/stubs/unix/context
@@ -0,0 +1,2 @@
+#!/bin/sh
+mtxrun --script context "$@"
diff --git a/scripts/context/stubs/unix/luatools b/scripts/context/stubs/unix/luatools
new file mode 100644
index 000000000..1d87322c1
--- /dev/null
+++ b/scripts/context/stubs/unix/luatools
@@ -0,0 +1,8185 @@
+#!/usr/bin/env texlua
+
+if not modules then modules = { } end modules ['luatools'] = {
+    version   = 1.001,
+    comment   = "companion to context.tex",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format = string.format
+
+-- one can make a stub:
+--
+-- #!/bin/sh
+-- env LUATEXDIR=/....../texmf/scripts/context/lua texlua luatools.lua "$@"
+
+-- Although this script is part of the ConTeXt distribution it is
+-- relatively indepent of ConTeXt.  The same is true for some of
+-- the luat files. We may may make them even less dependent in
+-- the future. As long as Luatex is under development the
+-- interfaces and names of functions may change.
+
+-- For the sake of independence we optionally can merge the library
+-- code here. It's too much code, but that does not harm. Much of the
+-- library code is used elsewhere. We don't want dependencies on
+-- Lua library paths simply because these scripts are located in the
+-- texmf tree and not in some Lua path. Normally this merge is not
+-- needed when texmfstart is used, or when the proper stub is used or
+-- when (windows) suffix binding is active.
+
+texlua = true
+
+-- begin library merge
+
+
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-string'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local sub, gsub, find, match, gmatch, format, char, byte, rep, lower = string.sub, string.gsub, string.find, string.match, string.gmatch, string.format, string.char, string.byte, string.rep, string.lower
+local lpegmatch = lpeg.match
+
+-- some functions may disappear as they are not used anywhere
+
+if not string.split then
+
+    -- this will be overloaded by a faster lpeg variant
+
+    function string:split(pattern)
+        if #self > 0 then
+            local t = { }
+            for s in gmatch(self..pattern,"(.-)"..pattern) do
+                t[#t+1] = s
+            end
+            return t
+        else
+            return { }
+        end
+    end
+
+end
+
+local chr_to_esc = {
+    ["%"] = "%%",
+    ["."] = "%.",
+    ["+"] = "%+", ["-"] = "%-", ["*"] = "%*",
+    ["^"] = "%^", ["$"] = "%$",
+    ["["] = "%[", ["]"] = "%]",
+    ["("] = "%(", [")"] = "%)",
+    ["{"] = "%{", ["}"] = "%}"
+}
+
+string.chr_to_esc = chr_to_esc
+
+function string:esc() -- variant 2
+    return (gsub(self,"(.)",chr_to_esc))
+end
+
+function string:unquote()
+    return (gsub(self,"^([\"\'])(.*)%1$","%2"))
+end
+
+--~ function string:unquote()
+--~     if find(self,"^[\'\"]") then
+--~         return sub(self,2,-2)
+--~     else
+--~         return self
+--~     end
+--~ end
+
+function string:quote() -- we could use format("%q")
+    return format("%q",self)
+end
+
+function string:count(pattern) -- variant 3
+    local n = 0
+    for _ in gmatch(self,pattern) do
+        n = n + 1
+    end
+    return n
+end
+
+function string:limit(n,sentinel)
+    if #self > n then
+        sentinel = sentinel or " ..."
+        return sub(self,1,(n-#sentinel)) .. sentinel
+    else
+        return self
+    end
+end
+
+--~ function string:strip() -- the .- is quite efficient
+--~  -- return match(self,"^%s*(.-)%s*$") or ""
+--~  -- return match(self,'^%s*(.*%S)') or '' -- posted on lua list
+--~     return find(s,'^%s*$') and '' or match(s,'^%s*(.*%S)')
+--~ end
+
+do -- roberto's variant:
+    local space    = lpeg.S(" \t\v\n")
+    local nospace  = 1 - space
+    local stripper = space^0 * lpeg.C((space^0 * nospace^1)^0)
+    function string.strip(str)
+        return lpegmatch(stripper,str) or ""
+    end
+end
+
+function string:is_empty()
+    return not find(self,"%S")
+end
+
+function string:enhance(pattern,action)
+    local ok, n = true, 0
+    while ok do
+        ok = false
+        self = gsub(self,pattern, function(...)
+            ok, n = true, n + 1
+            return action(...)
+        end)
+    end
+    return self, n
+end
+
+local chr_to_hex, hex_to_chr = { }, { }
+
+for i=0,255 do
+    local c, h = char(i), format("%02X",i)
+    chr_to_hex[c], hex_to_chr[h] = h, c
+end
+
+function string:to_hex()
+    return (gsub(self or "","(.)",chr_to_hex))
+end
+
+function string:from_hex()
+    return (gsub(self or "","(..)",hex_to_chr))
+end
+
+if not string.characters then
+
+    local function nextchar(str, index)
+        index = index + 1
+        return (index <= #str) and index or nil, sub(str,index,index)
+    end
+    function string:characters()
+        return nextchar, self, 0
+    end
+    local function nextbyte(str, index)
+        index = index + 1
+        return (index <= #str) and index or nil, byte(sub(str,index,index))
+    end
+    function string:bytes()
+        return nextbyte, self, 0
+    end
+
+end
+
+-- we can use format for this (neg n)
+
+function string:rpadd(n,chr)
+    local m = n-#self
+    if m > 0 then
+        return self .. rep(chr or " ",m)
+    else
+        return self
+    end
+end
+
+function string:lpadd(n,chr)
+    local m = n-#self
+    if m > 0 then
+        return rep(chr or " ",m) .. self
+    else
+        return self
+    end
+end
+
+string.padd = string.rpadd
+
+function is_number(str) -- tonumber
+    return find(str,"^[%-%+]?[%d]-%.?[%d+]$") == 1
+end
+
+--~ print(is_number("1"))
+--~ print(is_number("1.1"))
+--~ print(is_number(".1"))
+--~ print(is_number("-0.1"))
+--~ print(is_number("+0.1"))
+--~ print(is_number("-.1"))
+--~ print(is_number("+.1"))
+
+function string:split_settings() -- no {} handling, see l-aux for lpeg variant
+    if find(self,"=") then
+        local t = { }
+        for k,v in gmatch(self,"(%a+)=([^%,]*)") do
+            t[k] = v
+        end
+        return t
+    else
+        return nil
+    end
+end
+
+local patterns_escapes = {
+    ["-"] = "%-",
+    ["."] = "%.",
+    ["+"] = "%+",
+    ["*"] = "%*",
+    ["%"] = "%%",
+    ["("] = "%)",
+    [")"] = "%)",
+    ["["] = "%[",
+    ["]"] = "%]",
+}
+
+function string:pattesc()
+    return (gsub(self,".",patterns_escapes))
+end
+
+local simple_escapes = {
+    ["-"] = "%-",
+    ["."] = "%.",
+    ["?"] = ".",
+    ["*"] = ".*",
+}
+
+function string:simpleesc()
+    return (gsub(self,".",simple_escapes))
+end
+
+function string:tohash()
+    local t = { }
+    for s in gmatch(self,"([^, ]+)") do -- lpeg
+        t[s] = true
+    end
+    return t
+end
+
+local pattern = lpeg.Ct(lpeg.C(1)^0)
+
+function string:totable()
+    return lpegmatch(pattern,self)
+end
+
+--~ local t = {
+--~     "1234567123456712345671234567",
+--~     "a\tb\tc",
+--~     "aa\tbb\tcc",
+--~     "aaa\tbbb\tccc",
+--~     "aaaa\tbbbb\tcccc",
+--~     "aaaaa\tbbbbb\tccccc",
+--~     "aaaaaa\tbbbbbb\tcccccc",
+--~ }
+--~ for k,v do
+--~     print(string.tabtospace(t[k]))
+--~ end
+
+function string.tabtospace(str,tab)
+    -- we don't handle embedded newlines
+    while true do
+        local s = find(str,"\t")
+        if s then
+            if not tab then tab = 7 end -- only when found
+            local d = tab-(s-1) % tab
+            if d > 0 then
+                str = gsub(str,"\t",rep(" ",d),1)
+            else
+                str = gsub(str,"\t","",1)
+            end
+        else
+            break
+        end
+    end
+    return str
+end
+
+function string:compactlong() -- strips newlines and leading spaces
+    self = gsub(self,"[\n\r]+ *","")
+    self = gsub(self,"^ *","")
+    return self
+end
+
+function string:striplong() -- strips newlines and leading spaces
+    self = gsub(self,"^%s*","")
+    self = gsub(self,"[\n\r]+ *","\n")
+    return self
+end
+
+function string:topattern(lowercase,strict)
+    if lowercase then
+        self = lower(self)
+    end
+    self = gsub(self,".",simple_escapes)
+    if self == "" then
+        self = ".*"
+    elseif strict then
+        self = "^" .. self .. "$"
+    end
+    return self
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-lpeg'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local lpeg = require("lpeg")
+
+lpeg.patterns  = lpeg.patterns or { } -- so that we can share
+local patterns = lpeg.patterns
+
+local P, R, S, Ct, C, Cs, Cc, V = lpeg.P, lpeg.R, lpeg.S, lpeg.Ct, lpeg.C, lpeg.Cs, lpeg.Cc, lpeg.V
+local match = lpeg.match
+
+local digit, sign      = R('09'), S('+-')
+local cr, lf, crlf     = P("\r"), P("\n"), P("\r\n")
+local utf8byte         = R("\128\191")
+
+patterns.utf8byte      = utf8byte
+patterns.utf8one       = R("\000\127")
+patterns.utf8two       = R("\194\223") * utf8byte
+patterns.utf8three     = R("\224\239") * utf8byte * utf8byte
+patterns.utf8four      = R("\240\244") * utf8byte * utf8byte * utf8byte
+
+patterns.digit         = digit
+patterns.sign          = sign
+patterns.cardinal      = sign^0 * digit^1
+patterns.integer       = sign^0 * digit^1
+patterns.float         = sign^0 * digit^0 * P('.') * digit^1
+patterns.number        = patterns.float + patterns.integer
+patterns.oct           = P("0") * R("07")^1
+patterns.octal         = patterns.oct
+patterns.HEX           = P("0x") * R("09","AF")^1
+patterns.hex           = P("0x") * R("09","af")^1
+patterns.hexadecimal   = P("0x") * R("09","AF","af")^1
+patterns.lowercase     = R("az")
+patterns.uppercase     = R("AZ")
+patterns.letter        = patterns.lowercase + patterns.uppercase
+patterns.space         = S(" ")
+patterns.eol           = S("\n\r")
+patterns.spacer        = S(" \t\f\v")  -- + string.char(0xc2, 0xa0) if we want utf (cf mail roberto)
+patterns.newline       = crlf + cr + lf
+patterns.nonspace      = 1 - patterns.space
+patterns.nonspacer     = 1 - patterns.spacer
+patterns.whitespace    = patterns.eol + patterns.spacer
+patterns.nonwhitespace = 1 - patterns.whitespace
+patterns.utf8          = patterns.utf8one + patterns.utf8two + patterns.utf8three + patterns.utf8four
+patterns.utfbom        = P('\000\000\254\255') + P('\255\254\000\000') + P('\255\254') + P('\254\255') + P('\239\187\191')
+
+function lpeg.anywhere(pattern) --slightly adapted from website
+    return P { P(pattern) + 1 * V(1) } -- why so complex?
+end
+
+function lpeg.splitter(pattern, action)
+    return (((1-P(pattern))^1)/action+1)^0
+end
+
+local spacing  = patterns.spacer^0 * patterns.newline -- sort of strip
+local empty    = spacing * Cc("")
+local nonempty = Cs((1-spacing)^1) * spacing^-1
+local content  = (empty + nonempty)^1
+
+local capture = Ct(content^0)
+
+function string:splitlines()
+    return match(capture,self)
+end
+
+patterns.textline = content
+
+--~ local p = lpeg.splitat("->",false)  print(match(p,"oeps->what->more"))  -- oeps what more
+--~ local p = lpeg.splitat("->",true)   print(match(p,"oeps->what->more"))  -- oeps what->more
+--~ local p = lpeg.splitat("->",false)  print(match(p,"oeps"))              -- oeps
+--~ local p = lpeg.splitat("->",true)   print(match(p,"oeps"))              -- oeps
+
+local splitters_s, splitters_m = { }, { }
+
+local function splitat(separator,single)
+    local splitter = (single and splitters_s[separator]) or splitters_m[separator]
+    if not splitter then
+        separator = P(separator)
+        if single then
+            local other, any = C((1 - separator)^0), P(1)
+            splitter = other * (separator * C(any^0) + "") -- ?
+            splitters_s[separator] = splitter
+        else
+            local other = C((1 - separator)^0)
+            splitter = other * (separator * other)^0
+            splitters_m[separator] = splitter
+        end
+    end
+    return splitter
+end
+
+lpeg.splitat = splitat
+
+local cache = { }
+
+function lpeg.split(separator,str)
+    local c = cache[separator]
+    if not c then
+        c = Ct(splitat(separator))
+        cache[separator] = c
+    end
+    return match(c,str)
+end
+
+function string:split(separator)
+    local c = cache[separator]
+    if not c then
+        c = Ct(splitat(separator))
+        cache[separator] = c
+    end
+    return match(c,self)
+end
+
+lpeg.splitters = cache
+
+local cache = { }
+
+function lpeg.checkedsplit(separator,str)
+    local c = cache[separator]
+    if not c then
+        separator = P(separator)
+        local other = C((1 - separator)^0)
+        c = Ct(separator^0 * other * (separator^1 * other)^0)
+        cache[separator] = c
+    end
+    return match(c,str)
+end
+
+function string:checkedsplit(separator)
+    local c = cache[separator]
+    if not c then
+        separator = P(separator)
+        local other = C((1 - separator)^0)
+        c = Ct(separator^0 * other * (separator^1 * other)^0)
+        cache[separator] = c
+    end
+    return match(c,self)
+end
+
+--~ function lpeg.append(list,pp)
+--~     local p = pp
+--~     for l=1,#list do
+--~         if p then
+--~             p = p + P(list[l])
+--~         else
+--~             p = P(list[l])
+--~         end
+--~     end
+--~     return p
+--~ end
+
+--~ from roberto's site:
+
+local f1 = string.byte
+
+local function f2(s) local c1, c2         = f1(s,1,2) return   c1 * 64 + c2                       -    12416 end
+local function f3(s) local c1, c2, c3     = f1(s,1,3) return  (c1 * 64 + c2) * 64 + c3            -   925824 end
+local function f4(s) local c1, c2, c3, c4 = f1(s,1,4) return ((c1 * 64 + c2) * 64 + c3) * 64 + c4 - 63447168 end
+
+patterns.utf8byte = patterns.utf8one/f1 + patterns.utf8two/f2 + patterns.utf8three/f3 + patterns.utf8four/f4
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-table'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+table.join = table.concat
+
+local concat, sort, insert, remove = table.concat, table.sort, table.insert, table.remove
+local format, find, gsub, lower, dump, match = string.format, string.find, string.gsub, string.lower, string.dump, string.match
+local getmetatable, setmetatable = getmetatable, setmetatable
+local type, next, tostring, tonumber, ipairs = type, next, tostring, tonumber, ipairs
+local unpack = unpack or table.unpack
+
+function table.strip(tab)
+    local lst = { }
+    for i=1,#tab do
+        local s = gsub(tab[i],"^%s*(.-)%s*$","%1")
+        if s == "" then
+            -- skip this one
+        else
+            lst[#lst+1] = s
+        end
+    end
+    return lst
+end
+
+function table.keys(t)
+    local k = { }
+    for key, _ in next, t do
+        k[#k+1] = key
+    end
+    return k
+end
+
+local function compare(a,b)
+    return (tostring(a) < tostring(b))
+end
+
+local function sortedkeys(tab)
+    local srt, kind = { }, 0 -- 0=unknown 1=string, 2=number 3=mixed
+    for key,_ in next, tab do
+        srt[#srt+1] = key
+        if kind == 3 then
+            -- no further check
+        else
+            local tkey = type(key)
+            if tkey == "string" then
+            --  if kind == 2 then kind = 3 else kind = 1 end
+                kind = (kind == 2 and 3) or 1
+            elseif tkey == "number" then
+            --  if kind == 1 then kind = 3 else kind = 2 end
+                kind = (kind == 1 and 3) or 2
+            else
+                kind = 3
+            end
+        end
+    end
+    if kind == 0 or kind == 3 then
+        sort(srt,compare)
+    else
+        sort(srt)
+    end
+    return srt
+end
+
+local function sortedhashkeys(tab) -- fast one
+    local srt = { }
+    for key,_ in next, tab do
+        srt[#srt+1] = key
+    end
+    sort(srt)
+    return srt
+end
+
+table.sortedkeys     = sortedkeys
+table.sortedhashkeys = sortedhashkeys
+
+function table.sortedhash(t)
+    local s = sortedhashkeys(t) -- maybe just sortedkeys
+    local n = 0
+    local function kv(s)
+        n = n + 1
+        local k = s[n]
+        return k, t[k]
+    end
+    return kv, s
+end
+
+table.sortedpairs = table.sortedhash
+
+function table.append(t, list)
+    for _,v in next, list do
+        insert(t,v)
+    end
+end
+
+function table.prepend(t, list)
+    for k,v in next, list do
+        insert(t,k,v)
+    end
+end
+
+function table.merge(t, ...) -- first one is target
+    t = t or {}
+    local lst = {...}
+    for i=1,#lst do
+        for k, v in next, lst[i] do
+            t[k] = v
+        end
+    end
+    return t
+end
+
+function table.merged(...)
+    local tmp, lst = { }, {...}
+    for i=1,#lst do
+        for k, v in next, lst[i] do
+            tmp[k] = v
+        end
+    end
+    return tmp
+end
+
+function table.imerge(t, ...)
+    local lst = {...}
+    for i=1,#lst do
+        local nst = lst[i]
+        for j=1,#nst do
+            t[#t+1] = nst[j]
+        end
+    end
+    return t
+end
+
+function table.imerged(...)
+    local tmp, lst = { }, {...}
+    for i=1,#lst do
+        local nst = lst[i]
+        for j=1,#nst do
+            tmp[#tmp+1] = nst[j]
+        end
+    end
+    return tmp
+end
+
+local function fastcopy(old) -- fast one
+    if old then
+        local new = { }
+        for k,v in next, old do
+            if type(v) == "table" then
+                new[k] = fastcopy(v) -- was just table.copy
+            else
+                new[k] = v
+            end
+        end
+        -- optional second arg
+        local mt = getmetatable(old)
+        if mt then
+            setmetatable(new,mt)
+        end
+        return new
+    else
+        return { }
+    end
+end
+
+local function copy(t, tables) -- taken from lua wiki, slightly adapted
+    tables = tables or { }
+    local tcopy = {}
+    if not tables[t] then
+        tables[t] = tcopy
+    end
+    for i,v in next, t do -- brrr, what happens with sparse indexed
+        if type(i) == "table" then
+            if tables[i] then
+                i = tables[i]
+            else
+                i = copy(i, tables)
+            end
+        end
+        if type(v) ~= "table" then
+            tcopy[i] = v
+        elseif tables[v] then
+            tcopy[i] = tables[v]
+        else
+            tcopy[i] = copy(v, tables)
+        end
+    end
+    local mt = getmetatable(t)
+    if mt then
+        setmetatable(tcopy,mt)
+    end
+    return tcopy
+end
+
+table.fastcopy = fastcopy
+table.copy     = copy
+
+-- rougly: copy-loop : unpack : sub == 0.9 : 0.4 : 0.45 (so in critical apps, use unpack)
+
+function table.sub(t,i,j)
+    return { unpack(t,i,j) }
+end
+
+function table.replace(a,b)
+    for k,v in next, b do
+        a[k] = v
+    end
+end
+
+-- slower than #t on indexed tables (#t only returns the size of the numerically indexed slice)
+
+function table.is_empty(t) -- obolete, use inline code instead
+    return not t or not next(t)
+end
+
+function table.one_entry(t) -- obolete, use inline code instead
+    local n = next(t)
+    return n and not next(t,n)
+end
+
+--~ function table.starts_at(t) -- obsolete, not nice
+--~     return ipairs(t,1)(t,0)
+--~ end
+
+function table.tohash(t,value)
+    local h = { }
+    if t then
+        if value == nil then value = true end
+        for _, v in next, t do -- no ipairs here
+            h[v] = value
+        end
+    end
+    return h
+end
+
+function table.fromhash(t)
+    local h = { }
+    for k, v in next, t do -- no ipairs here
+        if v then h[#h+1] = k end
+    end
+    return h
+end
+
+--~ print(table.serialize(t), "\n")
+--~ print(table.serialize(t,"name"), "\n")
+--~ print(table.serialize(t,false), "\n")
+--~ print(table.serialize(t,true), "\n")
+--~ print(table.serialize(t,"name",true), "\n")
+--~ print(table.serialize(t,"name",true,true), "\n")
+
+table.serialize_functions = true
+table.serialize_compact   = true
+table.serialize_inline    = true
+
+local noquotes, hexify, handle, reduce, compact, inline, functions
+
+local reserved = table.tohash { -- intercept a language flaw, no reserved words as key
+    'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'if',
+    'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while',
+}
+
+local function simple_table(t)
+    if #t > 0 then
+        local n = 0
+        for _,v in next, t do
+            n = n + 1
+        end
+        if n == #t then
+            local tt = { }
+            for i=1,#t do
+                local v = t[i]
+                local tv = type(v)
+                if tv == "number" then
+                    if hexify then
+                        tt[#tt+1] = format("0x%04X",v)
+                    else
+                        tt[#tt+1] = tostring(v) -- tostring not needed
+                    end
+                elseif tv == "boolean" then
+                    tt[#tt+1] = tostring(v)
+                elseif tv == "string" then
+                    tt[#tt+1] = format("%q",v)
+                else
+                    tt = nil
+                    break
+                end
+            end
+            return tt
+        end
+    end
+    return nil
+end
+
+-- Because this is a core function of mkiv I moved some function calls
+-- inline.
+--
+-- twice as fast in a test:
+--
+-- local propername = lpeg.P(lpeg.R("AZ","az","__") * lpeg.R("09","AZ","az", "__")^0 * lpeg.P(-1) )
+
+-- problem: there no good number_to_string converter with the best resolution
+
+local function do_serialize(root,name,depth,level,indexed)
+    if level > 0 then
+        depth = depth .. " "
+        if indexed then
+            handle(format("%s{",depth))
+        elseif name then
+        --~ handle(format("%s%s={",depth,key(name)))
+            if type(name) == "number" then -- or find(k,"^%d+$") then
+                if hexify then
+                    handle(format("%s[0x%04X]={",depth,name))
+                else
+                    handle(format("%s[%s]={",depth,name))
+                end
+            elseif noquotes and not reserved[name] and find(name,"^%a[%w%_]*$") then
+                handle(format("%s%s={",depth,name))
+            else
+                handle(format("%s[%q]={",depth,name))
+            end
+        else
+            handle(format("%s{",depth))
+        end
+    end
+    -- we could check for k (index) being number (cardinal)
+    if root and next(root) then
+        local first, last = nil, 0 -- #root cannot be trusted here (will be ok in 5.2 when ipairs is gone)
+        if compact then
+            -- NOT: for k=1,#root do (we need to quit at nil)
+            for k,v in ipairs(root) do -- can we use next?
+                if not first then first = k end
+                last = last + 1
+            end
+        end
+        local sk = sortedkeys(root)
+        for i=1,#sk do
+            local k = sk[i]
+            local v = root[k]
+            --~ if v == root then
+                -- circular
+            --~ else
+            local t = type(v)
+            if compact and first and type(k) == "number" and k >= first and k <= last then
+                if t == "number" then
+                    if hexify then
+                        handle(format("%s 0x%04X,",depth,v))
+                    else
+                        handle(format("%s %s,",depth,v)) -- %.99g
+                    end
+                elseif t == "string" then
+                    if reduce and tonumber(v) then
+                        handle(format("%s %s,",depth,v))
+                    else
+                        handle(format("%s %q,",depth,v))
+                    end
+                elseif t == "table" then
+                    if not next(v) then
+                        handle(format("%s {},",depth))
+                    elseif inline then -- and #t > 0
+                        local st = simple_table(v)
+                        if st then
+                            handle(format("%s { %s },",depth,concat(st,", ")))
+                        else
+                            do_serialize(v,k,depth,level+1,true)
+                        end
+                    else
+                        do_serialize(v,k,depth,level+1,true)
+                    end
+                elseif t == "boolean" then
+                    handle(format("%s %s,",depth,tostring(v)))
+                elseif t == "function" then
+                    if functions then
+                        handle(format('%s loadstring(%q),',depth,dump(v)))
+                    else
+                        handle(format('%s "function",',depth))
+                    end
+                else
+                    handle(format("%s %q,",depth,tostring(v)))
+                end
+            elseif k == "__p__" then -- parent
+                if false then
+                    handle(format("%s __p__=nil,",depth))
+                end
+            elseif t == "number" then
+            --~ if hexify then
+            --~     handle(format("%s %s=0x%04X,",depth,key(k),v))
+            --~ else
+            --~     handle(format("%s %s=%s,",depth,key(k),v)) -- %.99g
+            --~ end
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s [%s]=%s,",depth,k,v)) -- %.99g
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    if hexify then
+                        handle(format("%s %s=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s %s=%s,",depth,k,v)) -- %.99g
+                    end
+                else
+                    if hexify then
+                        handle(format("%s [%q]=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%s,",depth,k,v)) -- %.99g
+                    end
+                end
+            elseif t == "string" then
+                if reduce and tonumber(v) then
+                --~ handle(format("%s %s=%s,",depth,key(k),v))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=%s,",depth,k,v))
+                        else
+                            handle(format("%s [%s]=%s,",depth,k,v))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=%s,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%s,",depth,k,v))
+                    end
+                else
+                --~ handle(format("%s %s=%q,",depth,key(k),v))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=%q,",depth,k,v))
+                        else
+                            handle(format("%s [%s]=%q,",depth,k,v))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=%q,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%q,",depth,k,v))
+                    end
+                end
+            elseif t == "table" then
+                if not next(v) then
+                    --~ handle(format("%s %s={},",depth,key(k)))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]={},",depth,k))
+                        else
+                            handle(format("%s [%s]={},",depth,k))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s={},",depth,k))
+                    else
+                        handle(format("%s [%q]={},",depth,k))
+                    end
+                elseif inline then
+                    local st = simple_table(v)
+                    if st then
+                    --~ handle(format("%s %s={ %s },",depth,key(k),concat(st,", ")))
+                        if type(k) == "number" then -- or find(k,"^%d+$") then
+                            if hexify then
+                                handle(format("%s [0x%04X]={ %s },",depth,k,concat(st,", ")))
+                            else
+                                handle(format("%s [%s]={ %s },",depth,k,concat(st,", ")))
+                            end
+                        elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                            handle(format("%s %s={ %s },",depth,k,concat(st,", ")))
+                        else
+                            handle(format("%s [%q]={ %s },",depth,k,concat(st,", ")))
+                        end
+                    else
+                        do_serialize(v,k,depth,level+1)
+                    end
+                else
+                    do_serialize(v,k,depth,level+1)
+                end
+            elseif t == "boolean" then
+            --~ handle(format("%s %s=%s,",depth,key(k),tostring(v)))
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=%s,",depth,k,tostring(v)))
+                    else
+                        handle(format("%s [%s]=%s,",depth,k,tostring(v)))
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    handle(format("%s %s=%s,",depth,k,tostring(v)))
+                else
+                    handle(format("%s [%q]=%s,",depth,k,tostring(v)))
+                end
+            elseif t == "function" then
+                if functions then
+                    --~ handle(format('%s %s=loadstring(%q),',depth,key(k),dump(v)))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=loadstring(%q),",depth,k,dump(v)))
+                        else
+                            handle(format("%s [%s]=loadstring(%q),",depth,k,dump(v)))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=loadstring(%q),",depth,k,dump(v)))
+                    else
+                        handle(format("%s [%q]=loadstring(%q),",depth,k,dump(v)))
+                    end
+                end
+            else
+                --~ handle(format("%s %s=%q,",depth,key(k),tostring(v)))
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=%q,",depth,k,tostring(v)))
+                    else
+                        handle(format("%s [%s]=%q,",depth,k,tostring(v)))
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    handle(format("%s %s=%q,",depth,k,tostring(v)))
+                else
+                    handle(format("%s [%q]=%q,",depth,k,tostring(v)))
+                end
+            end
+            --~ end
+        end
+    end
+   if level > 0 then
+        handle(format("%s},",depth))
+    end
+end
+
+-- replacing handle by a direct t[#t+1] = ... (plus test) is not much
+-- faster (0.03 on 1.00 for zapfino.tma)
+
+local function serialize(root,name,_handle,_reduce,_noquotes,_hexify)
+    noquotes = _noquotes
+    hexify = _hexify
+    handle = _handle or print
+    reduce = _reduce or false
+    compact = table.serialize_compact
+    inline  = compact and table.serialize_inline
+    functions = table.serialize_functions
+    local tname = type(name)
+    if tname == "string" then
+        if name == "return" then
+            handle("return {")
+        else
+            handle(name .. "={")
+        end
+    elseif tname == "number" then
+        if hexify then
+            handle(format("[0x%04X]={",name))
+        else
+            handle("[" .. name .. "]={")
+        end
+    elseif tname == "boolean" then
+        if name then
+            handle("return {")
+        else
+            handle("{")
+        end
+    else
+        handle("t={")
+    end
+    if root and next(root) then
+        do_serialize(root,name,"",0,indexed)
+    end
+    handle("}")
+end
+
+--~ name:
+--~
+--~ true     : return     { }
+--~ false    :            { }
+--~ nil      : t        = { }
+--~ string   : string   = { }
+--~ 'return' : return     { }
+--~ number   : [number] = { }
+
+function table.serialize(root,name,reduce,noquotes,hexify)
+    local t = { }
+    local function flush(s)
+        t[#t+1] = s
+    end
+    serialize(root,name,flush,reduce,noquotes,hexify)
+    return concat(t,"\n")
+end
+
+function table.tohandle(handle,root,name,reduce,noquotes,hexify)
+    serialize(root,name,handle,reduce,noquotes,hexify)
+end
+
+-- sometimes tables are real use (zapfino extra pro is some 85M) in which
+-- case a stepwise serialization is nice; actually, we could consider:
+--
+-- for line in table.serializer(root,name,reduce,noquotes) do
+--    ...(line)
+-- end
+--
+-- so this is on the todo list
+
+table.tofile_maxtab = 2*1024
+
+function table.tofile(filename,root,name,reduce,noquotes,hexify)
+    local f = io.open(filename,'w')
+    if f then
+        local maxtab = table.tofile_maxtab
+        if maxtab > 1 then
+            local t = { }
+            local function flush(s)
+                t[#t+1] = s
+                if #t > maxtab then
+                    f:write(concat(t,"\n"),"\n") -- hm, write(sometable) should be nice
+                    t = { }
+                end
+            end
+            serialize(root,name,flush,reduce,noquotes,hexify)
+            f:write(concat(t,"\n"),"\n")
+        else
+            local function flush(s)
+                f:write(s,"\n")
+            end
+            serialize(root,name,flush,reduce,noquotes,hexify)
+        end
+        f:close()
+    end
+end
+
+local function flatten(t,f,complete) -- is this used? meybe a variant with next, ...
+    for i=1,#t do
+        local v = t[i]
+        if type(v) == "table" then
+            if complete or type(v[1]) == "table" then
+                flatten(v,f,complete)
+            else
+                f[#f+1] = v
+            end
+        else
+            f[#f+1] = v
+        end
+    end
+end
+
+function table.flatten(t)
+    local f = { }
+    flatten(t,f,true)
+    return f
+end
+
+function table.unnest(t) -- bad name
+    local f = { }
+    flatten(t,f,false)
+    return f
+end
+
+table.flatten_one_level = table.unnest
+
+-- a better one:
+
+local function flattened(t,f)
+    if not f then
+        f = { }
+    end
+    for k, v in next, t do
+        if type(v) == "table" then
+            flattened(v,f)
+        else
+            f[k] = v
+        end
+    end
+    return f
+end
+
+table.flattened = flattened
+
+-- the next three may disappear
+
+function table.remove_value(t,value) -- todo: n
+    if value then
+        for i=1,#t do
+            if t[i] == value then
+                remove(t,i)
+                -- remove all, so no: return
+            end
+        end
+    end
+end
+
+function table.insert_before_value(t,value,str)
+    if str then
+        if value then
+            for i=1,#t do
+                if t[i] == value then
+                    insert(t,i,str)
+                    return
+                end
+            end
+        end
+        insert(t,1,str)
+    elseif value then
+        insert(t,1,value)
+    end
+end
+
+function table.insert_after_value(t,value,str)
+    if str then
+        if value then
+            for i=1,#t do
+                if t[i] == value then
+                    insert(t,i+1,str)
+                    return
+                end
+            end
+        end
+        t[#t+1] = str
+    elseif value then
+        t[#t+1] = value
+    end
+end
+
+local function are_equal(a,b,n,m) -- indexed
+    if a and b and #a == #b then
+        n = n or 1
+        m = m or #a
+        for i=n,m do
+            local ai, bi = a[i], b[i]
+            if ai==bi then
+                -- same
+            elseif type(ai)=="table" and type(bi)=="table" then
+                if not are_equal(ai,bi) then
+                    return false
+                end
+            else
+                return false
+            end
+        end
+        return true
+    else
+        return false
+    end
+end
+
+local function identical(a,b) -- assumes same structure
+    for ka, va in next, a do
+        local vb = b[k]
+        if va == vb then
+            -- same
+        elseif type(va) == "table" and  type(vb) == "table" then
+            if not identical(va,vb) then
+                return false
+            end
+        else
+            return false
+        end
+    end
+    return true
+end
+
+table.are_equal = are_equal
+table.identical = identical
+
+-- maybe also make a combined one
+
+function table.compact(t)
+    if t then
+        for k,v in next, t do
+            if not next(v) then
+                t[k] = nil
+            end
+        end
+    end
+end
+
+function table.contains(t, v)
+    if t then
+        for i=1, #t do
+            if t[i] == v then
+                return i
+            end
+        end
+    end
+    return false
+end
+
+function table.count(t)
+    local n, e = 0, next(t)
+    while e do
+        n, e = n + 1, next(t,e)
+    end
+    return n
+end
+
+function table.swapped(t)
+    local s = { }
+    for k, v in next, t do
+        s[v] = k
+    end
+    return s
+end
+
+--~ function table.are_equal(a,b)
+--~     return table.serialize(a) == table.serialize(b)
+--~ end
+
+function table.clone(t,p) -- t is optional or nil or table
+    if not p then
+        t, p = { }, t or { }
+    elseif not t then
+        t = { }
+    end
+    setmetatable(t, { __index = function(_,key) return p[key] end }) -- why not __index = p ?
+    return t
+end
+
+function table.hexed(t,seperator)
+    local tt = { }
+    for i=1,#t do tt[i] = format("0x%04X",t[i]) end
+    return concat(tt,seperator or " ")
+end
+
+function table.reverse_hash(h)
+    local r = { }
+    for k,v in next, h do
+        r[v] = lower(gsub(k," ",""))
+    end
+    return r
+end
+
+function table.reverse(t)
+    local tt = { }
+    if #t > 0 then
+        for i=#t,1,-1 do
+            tt[#tt+1] = t[i]
+        end
+    end
+    return tt
+end
+
+function table.insert_before_value(t,value,extra)
+    for i=1,#t do
+        if t[i] == extra then
+            remove(t,i)
+        end
+    end
+    for i=1,#t do
+        if t[i] == value then
+            insert(t,i,extra)
+            return
+        end
+    end
+    insert(t,1,extra)
+end
+
+function table.insert_after_value(t,value,extra)
+    for i=1,#t do
+        if t[i] == extra then
+            remove(t,i)
+        end
+    end
+    for i=1,#t do
+        if t[i] == value then
+            insert(t,i+1,extra)
+            return
+        end
+    end
+    insert(t,#t+1,extra)
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-io'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local byte, find, gsub = string.byte, string.find, string.gsub
+
+if string.find(os.getenv("PATH"),";") then
+    io.fileseparator, io.pathseparator = "\\", ";"
+else
+    io.fileseparator, io.pathseparator = "/" , ":"
+end
+
+function io.loaddata(filename,textmode)
+    local f = io.open(filename,(textmode and 'r') or 'rb')
+    if f then
+    --  collectgarbage("step") -- sometimes makes a big difference in mem consumption
+        local data = f:read('*all')
+    --  garbagecollector.check(data)
+        f:close()
+        return data
+    else
+        return nil
+    end
+end
+
+function io.savedata(filename,data,joiner)
+    local f = io.open(filename,"wb")
+    if f then
+        if type(data) == "table" then
+            f:write(table.join(data,joiner or ""))
+        elseif type(data) == "function" then
+            data(f)
+        else
+            f:write(data or "")
+        end
+        f:close()
+        return true
+    else
+        return false
+    end
+end
+
+function io.exists(filename)
+    local f = io.open(filename)
+    if f == nil then
+        return false
+    else
+        assert(f:close())
+        return true
+    end
+end
+
+function io.size(filename)
+    local f = io.open(filename)
+    if f == nil then
+        return 0
+    else
+        local s = f:seek("end")
+        assert(f:close())
+        return s
+    end
+end
+
+function io.noflines(f)
+    local n = 0
+    for _ in f:lines() do
+        n = n + 1
+    end
+    f:seek('set',0)
+    return n
+end
+
+local nextchar = {
+    [ 4] = function(f)
+        return f:read(1,1,1,1)
+    end,
+    [ 2] = function(f)
+        return f:read(1,1)
+    end,
+    [ 1] = function(f)
+        return f:read(1)
+    end,
+    [-2] = function(f)
+        local a, b = f:read(1,1)
+        return b, a
+    end,
+    [-4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        return d, c, b, a
+    end
+}
+
+function io.characters(f,n)
+    if f then
+        return nextchar[n or 1], f
+    else
+        return nil, nil
+    end
+end
+
+local nextbyte = {
+    [4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        if d then
+            return byte(a), byte(b), byte(c), byte(d)
+        else
+            return nil, nil, nil, nil
+        end
+    end,
+    [2] = function(f)
+        local a, b = f:read(1,1)
+        if b then
+            return byte(a), byte(b)
+        else
+            return nil, nil
+        end
+    end,
+    [1] = function (f)
+        local a = f:read(1)
+        if a then
+            return byte(a)
+        else
+            return nil
+        end
+    end,
+    [-2] = function (f)
+        local a, b = f:read(1,1)
+        if b then
+            return byte(b), byte(a)
+        else
+            return nil, nil
+        end
+    end,
+    [-4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        if d then
+            return byte(d), byte(c), byte(b), byte(a)
+        else
+            return nil, nil, nil, nil
+        end
+    end
+}
+
+function io.bytes(f,n)
+    if f then
+        return nextbyte[n or 1], f
+    else
+        return nil, nil
+    end
+end
+
+function io.ask(question,default,options)
+    while true do
+        io.write(question)
+        if options then
+            io.write(string.format(" [%s]",table.concat(options,"|")))
+        end
+        if default then
+            io.write(string.format(" [%s]",default))
+        end
+        io.write(string.format(" "))
+        local answer = io.read()
+        answer = gsub(answer,"^%s*(.*)%s*$","%1")
+        if answer == "" and default then
+            return default
+        elseif not options then
+            return answer
+        else
+            for k=1,#options do
+                if options[k] == answer then
+                    return answer
+                end
+            end
+            local pattern = "^" .. answer
+            for k=1,#options do
+                local v = options[k]
+                if find(v,pattern) then
+                    return v
+                end
+            end
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-number'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local tostring = tostring
+local format, floor, insert, match = string.format, math.floor, table.insert, string.match
+local lpegmatch = lpeg.match
+
+number = number or { }
+
+-- a,b,c,d,e,f = number.toset(100101)
+
+function number.toset(n)
+    return match(tostring(n),"(.?)(.?)(.?)(.?)(.?)(.?)(.?)(.?)")
+end
+
+function number.toevenhex(n)
+    local s = format("%X",n)
+    if #s % 2 == 0 then
+        return s
+    else
+        return "0" .. s
+    end
+end
+
+-- the lpeg way is slower on 8 digits, but faster on 4 digits, some 7.5%
+-- on
+--
+-- for i=1,1000000 do
+--     local a,b,c,d,e,f,g,h = number.toset(12345678)
+--     local a,b,c,d         = number.toset(1234)
+--     local a,b,c           = number.toset(123)
+-- end
+--
+-- of course dedicated "(.)(.)(.)(.)" matches are even faster
+
+local one = lpeg.C(1-lpeg.S(''))^1
+
+function number.toset(n)
+    return lpegmatch(one,tostring(n))
+end
+
+function number.bits(n,zero)
+    local t, i = { }, (zero and 0) or 1
+    while n > 0 do
+        local m = n % 2
+        if m > 0 then
+            insert(t,1,i)
+        end
+        n = floor(n/2)
+        i = i + 1
+    end
+    return t
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-set'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+set = set or { }
+
+local nums   = { }
+local tabs   = { }
+local concat = table.concat
+local next, type = next, type
+
+set.create = table.tohash
+
+function set.tonumber(t)
+    if next(t) then
+        local s = ""
+    --  we could save mem by sorting, but it slows down
+        for k, v in next, t do
+            if v then
+            --  why bother about the leading space
+                s = s .. " " .. k
+            end
+        end
+        local n = nums[s]
+        if not n then
+            n = #tabs + 1
+            tabs[n] = t
+            nums[s] = n
+        end
+        return n
+    else
+        return 0
+    end
+end
+
+function set.totable(n)
+    if n == 0 then
+        return { }
+    else
+        return tabs[n] or { }
+    end
+end
+
+function set.tolist(n)
+    if n == 0 or not tabs[n] then
+        return ""
+    else
+        local t = { }
+        for k, v in next, tabs[n] do
+            if v then
+                t[#t+1] = k
+            end
+        end
+        return concat(t," ")
+    end
+end
+
+function set.contains(n,s)
+    if type(n) == "table" then
+        return n[s]
+    elseif n == 0 then
+        return false
+    else
+        local t = tabs[n]
+        return t and t[s]
+    end
+end
+
+--~ local c = set.create{'aap','noot','mies'}
+--~ local s = set.tonumber(c)
+--~ local t = set.totable(s)
+--~ print(t['aap'])
+--~ local c = set.create{'zus','wim','jet'}
+--~ local s = set.tonumber(c)
+--~ local t = set.totable(s)
+--~ print(t['aap'])
+--~ print(t['jet'])
+--~ print(set.contains(t,'jet'))
+--~ print(set.contains(t,'aap'))
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-os'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- maybe build io.flush in os.execute
+
+local find, format, gsub = string.find, string.format, string.gsub
+local random, ceil = math.random, math.ceil
+
+local execute, spawn, exec, ioflush = os.execute, os.spawn or os.execute, os.exec or os.execute, io.flush
+
+function os.execute(...) ioflush() return execute(...) end
+function os.spawn  (...) ioflush() return spawn  (...) end
+function os.exec   (...) ioflush() return exec   (...) end
+
+function os.resultof(command)
+    ioflush() -- else messed up logging
+    local handle = io.popen(command,"r")
+    if not handle then
+    --  print("unknown command '".. command .. "' in os.resultof")
+        return ""
+    else
+        return handle:read("*all") or ""
+    end
+end
+
+--~ os.type     : windows | unix (new, we already guessed os.platform)
+--~ os.name     : windows | msdos | linux | macosx | solaris | .. | generic (new)
+--~ os.platform : extended os.name with architecture
+
+if not io.fileseparator then
+    if find(os.getenv("PATH"),";") then
+        io.fileseparator, io.pathseparator, os.type = "\\", ";", os.type or "mswin"
+    else
+        io.fileseparator, io.pathseparator, os.type = "/" , ":", os.type or "unix"
+    end
+end
+
+os.type = os.type or (io.pathseparator == ";"       and "windows") or "unix"
+os.name = os.name or (os.type          == "windows" and "mswin"  ) or "linux"
+
+if os.type == "windows" then
+    os.libsuffix, os.binsuffix = 'dll', 'exe'
+else
+    os.libsuffix, os.binsuffix = 'so', ''
+end
+
+function os.launch(str)
+    if os.type == "windows" then
+        os.execute("start " .. str) -- os.spawn ?
+    else
+        os.execute(str .. " &")     -- os.spawn ?
+    end
+end
+
+if not os.times then
+    -- utime  = user time
+    -- stime  = system time
+    -- cutime = children user time
+    -- cstime = children system time
+    function os.times()
+        return {
+            utime  = os.gettimeofday(), -- user
+            stime  = 0,                 -- system
+            cutime = 0,                 -- children user
+            cstime = 0,                 -- children system
+        }
+    end
+end
+
+os.gettimeofday = os.gettimeofday or os.clock
+
+local startuptime = os.gettimeofday()
+
+function os.runtime()
+    return os.gettimeofday() - startuptime
+end
+
+--~ print(os.gettimeofday()-os.time())
+--~ os.sleep(1.234)
+--~ print (">>",os.runtime())
+--~ print(os.date("%H:%M:%S",os.gettimeofday()))
+--~ print(os.date("%H:%M:%S",os.time()))
+
+-- no need for function anymore as we have more clever code and helpers now
+-- this metatable trickery might as well disappear
+
+os.resolvers = os.resolvers or { }
+
+local resolvers = os.resolvers
+
+local osmt = getmetatable(os) or { __index = function(t,k) t[k] = "unset" return "unset" end } -- maybe nil
+local osix = osmt.__index
+
+osmt.__index = function(t,k)
+    return (resolvers[k] or osix)(t,k)
+end
+
+setmetatable(os,osmt)
+
+if not os.setenv then
+
+    -- we still store them but they won't be seen in
+    -- child processes although we might pass them some day
+    -- using command concatination
+
+    local env, getenv = { }, os.getenv
+
+    function os.setenv(k,v)
+        env[k] = v
+    end
+
+    function os.getenv(k)
+        return env[k] or getenv(k)
+    end
+
+end
+
+-- we can use HOSTTYPE on some platforms
+
+local name, platform = os.name or "linux", os.getenv("MTX_PLATFORM") or ""
+
+local function guess()
+    local architecture = os.resultof("uname -m") or ""
+    if architecture ~= "" then
+        return architecture
+    end
+    architecture = os.getenv("HOSTTYPE") or ""
+    if architecture ~= "" then
+        return architecture
+    end
+    return os.resultof("echo $HOSTTYPE") or ""
+end
+
+if platform ~= "" then
+
+    os.platform = platform
+
+elseif os.type == "windows" then
+
+    -- we could set the variable directly, no function needed here
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.getenv("PROCESSOR_ARCHITECTURE") or ""
+        if find(architecture,"AMD64") then
+            platform = "mswin-64"
+        else
+            platform = "mswin"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "linux" then
+
+    function os.resolvers.platform(t,k)
+        -- we sometims have HOSTTYPE set so let's check that first
+        local platform, architecture = "", os.getenv("HOSTTYPE") or os.resultof("uname -m") or ""
+        if find(architecture,"x86_64") then
+            platform = "linux-64"
+        elseif find(architecture,"ppc") then
+            platform = "linux-ppc"
+        else
+            platform = "linux"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "macosx" then
+
+    --[[
+        Identifying the architecture of OSX is quite a mess and this
+        is the best we can come up with. For some reason $HOSTTYPE is
+        a kind of pseudo environment variable, not known to the current
+        environment. And yes, uname cannot be trusted either, so there
+        is a change that you end up with a 32 bit run on a 64 bit system.
+        Also, some proper 64 bit intel macs are too cheap (low-end) and
+        therefore not permitted to run the 64 bit kernel.
+      ]]--
+
+    function os.resolvers.platform(t,k)
+     -- local platform, architecture = "", os.getenv("HOSTTYPE") or ""
+     -- if architecture == "" then
+     --     architecture = os.resultof("echo $HOSTTYPE") or ""
+     -- end
+        local platform, architecture = "", os.resultof("echo $HOSTTYPE") or ""
+        if architecture == "" then
+         -- print("\nI have no clue what kind of OSX you're running so let's assume an 32 bit intel.\n")
+            platform = "osx-intel"
+        elseif find(architecture,"i386") then
+            platform = "osx-intel"
+        elseif find(architecture,"x86_64") then
+            platform = "osx-64"
+        else
+            platform = "osx-ppc"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "sunos" then
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.resultof("uname -m") or ""
+        if find(architecture,"sparc") then
+            platform = "solaris-sparc"
+        else -- if architecture == 'i86pc'
+            platform = "solaris-intel"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "freebsd" then
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.resultof("uname -m") or ""
+        if find(architecture,"amd64") then
+            platform = "freebsd-amd64"
+        else
+            platform = "freebsd"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "kfreebsd" then
+
+    function os.resolvers.platform(t,k)
+        -- we sometims have HOSTTYPE set so let's check that first
+        local platform, architecture = "", os.getenv("HOSTTYPE") or os.resultof("uname -m") or ""
+        if find(architecture,"x86_64") then
+            platform = "kfreebsd-64"
+        else
+            platform = "kfreebsd-i386"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+else
+
+    -- platform = "linux"
+    -- os.setenv("MTX_PLATFORM",platform)
+    -- os.platform = platform
+
+    function os.resolvers.platform(t,k)
+        local platform = "linux"
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+end
+
+-- beware, we set the randomseed
+
+-- from wikipedia: Version 4 UUIDs use a scheme relying only on random numbers. This algorithm sets the
+-- version number as well as two reserved bits. All other bits are set using a random or pseudorandom
+-- data source. Version 4 UUIDs have the form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx with hexadecimal
+-- digits x and hexadecimal digits 8, 9, A, or B for y. e.g. f47ac10b-58cc-4372-a567-0e02b2c3d479.
+--
+-- as we don't call this function too often there is not so much risk on repetition
+
+local t = { 8, 9, "a", "b" }
+
+function os.uuid()
+    return format("%04x%04x-4%03x-%s%03x-%04x-%04x%04x%04x",
+        random(0xFFFF),random(0xFFFF),
+        random(0x0FFF),
+        t[ceil(random(4))] or 8,random(0x0FFF),
+        random(0xFFFF),
+        random(0xFFFF),random(0xFFFF),random(0xFFFF)
+    )
+end
+
+local d
+
+function os.timezone(delta)
+    d = d or tonumber(tonumber(os.date("%H")-os.date("!%H")))
+    if delta then
+        if d > 0 then
+            return format("+%02i:00",d)
+        else
+            return format("-%02i:00",-d)
+        end
+    else
+        return 1
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-file'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- needs a cleanup
+
+file = file or { }
+
+local concat = table.concat
+local find, gmatch, match, gsub, sub, char = string.find, string.gmatch, string.match, string.gsub, string.sub, string.char
+local lpegmatch = lpeg.match
+
+function file.removesuffix(filename)
+    return (gsub(filename,"%.[%a%d]+$",""))
+end
+
+function file.addsuffix(filename, suffix)
+    if not suffix or suffix == "" then
+        return filename
+    elseif not find(filename,"%.[%a%d]+$") then
+        return filename .. "." .. suffix
+    else
+        return filename
+    end
+end
+
+function file.replacesuffix(filename, suffix)
+    return (gsub(filename,"%.[%a%d]+$","")) .. "." .. suffix
+end
+
+function file.dirname(name,default)
+    return match(name,"^(.+)[/\\].-$") or (default or "")
+end
+
+function file.basename(name)
+    return match(name,"^.+[/\\](.-)$") or name
+end
+
+function file.nameonly(name)
+    return (gsub(match(name,"^.+[/\\](.-)$") or name,"%..*$",""))
+end
+
+function file.extname(name,default)
+    return match(name,"^.+%.([^/\\]-)$") or default or ""
+end
+
+file.suffix = file.extname
+
+--~ function file.join(...)
+--~     local pth = concat({...},"/")
+--~     pth = gsub(pth,"\\","/")
+--~     local a, b = match(pth,"^(.*://)(.*)$")
+--~     if a and b then
+--~         return a .. gsub(b,"//+","/")
+--~     end
+--~     a, b = match(pth,"^(//)(.*)$")
+--~     if a and b then
+--~         return a .. gsub(b,"//+","/")
+--~     end
+--~     return (gsub(pth,"//+","/"))
+--~ end
+
+local trick_1 = char(1)
+local trick_2 = "^" .. trick_1 .. "/+"
+
+function file.join(...)
+    local lst = { ... }
+    local a, b = lst[1], lst[2]
+    if a == "" then
+        lst[1] = trick_1
+    elseif b and find(a,"^/+$") and find(b,"^/") then
+        lst[1] = ""
+        lst[2] = gsub(b,"^/+","")
+    end
+    local pth = concat(lst,"/")
+    pth = gsub(pth,"\\","/")
+    local a, b = match(pth,"^(.*://)(.*)$")
+    if a and b then
+        return a .. gsub(b,"//+","/")
+    end
+    a, b = match(pth,"^(//)(.*)$")
+    if a and b then
+        return a .. gsub(b,"//+","/")
+    end
+    pth = gsub(pth,trick_2,"")
+    return (gsub(pth,"//+","/"))
+end
+
+--~ print(file.join("//","/y"))
+--~ print(file.join("/","/y"))
+--~ print(file.join("","/y"))
+--~ print(file.join("/x/","/y"))
+--~ print(file.join("x/","/y"))
+--~ print(file.join("http://","/y"))
+--~ print(file.join("http://a","/y"))
+--~ print(file.join("http:///a","/y"))
+--~ print(file.join("//nas-1","/y"))
+
+function file.iswritable(name)
+    local a = lfs.attributes(name) or lfs.attributes(file.dirname(name,"."))
+    return a and sub(a.permissions,2,2) == "w"
+end
+
+function file.isreadable(name)
+    local a = lfs.attributes(name)
+    return a and sub(a.permissions,1,1) == "r"
+end
+
+file.is_readable = file.isreadable
+file.is_writable = file.iswritable
+
+-- todo: lpeg
+
+--~ function file.split_path(str)
+--~     local t = { }
+--~     str = gsub(str,"\\", "/")
+--~     str = gsub(str,"(%a):([;/])", "%1\001%2")
+--~     for name in gmatch(str,"([^;:]+)") do
+--~         if name ~= "" then
+--~             t[#t+1] = gsub(name,"\001",":")
+--~         end
+--~     end
+--~     return t
+--~ end
+
+local checkedsplit = string.checkedsplit
+
+function file.split_path(str,separator)
+    str = gsub(str,"\\","/")
+    return checkedsplit(str,separator or io.pathseparator)
+end
+
+function file.join_path(tab)
+    return concat(tab,io.pathseparator) -- can have trailing //
+end
+
+-- we can hash them weakly
+
+function file.collapse_path(str)
+    str = gsub(str,"\\","/")
+    if find(str,"/") then
+        str = gsub(str,"^%./",(gsub(lfs.currentdir(),"\\","/")) .. "/") -- ./xx in qualified
+        str = gsub(str,"/%./","/")
+        local n, m = 1, 1
+        while n > 0 or m > 0 do
+            str, n = gsub(str,"[^/%.]+/%.%.$","")
+            str, m = gsub(str,"[^/%.]+/%.%./","")
+        end
+        str = gsub(str,"([^/])/$","%1")
+    --  str = gsub(str,"^%./","") -- ./xx in qualified
+        str = gsub(str,"/%.$","")
+    end
+    if str == "" then str = "." end
+    return str
+end
+
+--~ print(file.collapse_path("/a"))
+--~ print(file.collapse_path("a/./b/.."))
+--~ print(file.collapse_path("a/aa/../b/bb"))
+--~ print(file.collapse_path("a/../.."))
+--~ print(file.collapse_path("a/.././././b/.."))
+--~ print(file.collapse_path("a/./././b/.."))
+--~ print(file.collapse_path("a/b/c/../.."))
+
+function file.robustname(str)
+    return (gsub(str,"[^%a%d%/%-%.\\]+","-"))
+end
+
+file.readdata = io.loaddata
+file.savedata = io.savedata
+
+function file.copy(oldname,newname)
+    file.savedata(newname,io.loaddata(oldname))
+end
+
+-- lpeg variants, slightly faster, not always
+
+--~ local period    = lpeg.P(".")
+--~ local slashes   = lpeg.S("\\/")
+--~ local noperiod  = 1-period
+--~ local noslashes = 1-slashes
+--~ local name      = noperiod^1
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.C(noperiod^1) * -1
+
+--~ function file.extname(name)
+--~     return lpegmatch(pattern,name) or ""
+--~ end
+
+--~ local pattern = lpeg.Cs(((period * noperiod^1 * -1)/"" + 1)^1)
+
+--~ function file.removesuffix(name)
+--~     return lpegmatch(pattern,name)
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^1 * lpeg.C(noslashes^1) * -1
+
+--~ function file.basename(name)
+--~     return lpegmatch(pattern,name) or name
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^1 * lpeg.Cp() * noslashes^1 * -1
+
+--~ function file.dirname(name)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return sub(name,1,p-2)
+--~     else
+--~         return ""
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.Cp() * noperiod^1 * -1
+
+--~ function file.addsuffix(name, suffix)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return name
+--~     else
+--~         return name .. "." .. suffix
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.Cp() * noperiod^1 * -1
+
+--~ function file.replacesuffix(name,suffix)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return sub(name,1,p-2) .. "." .. suffix
+--~     else
+--~         return name .. "." .. suffix
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * lpeg.Cp() * ((noperiod^1 * period)^1 * lpeg.Cp() + lpeg.P(true)) * noperiod^1 * -1
+
+--~ function file.nameonly(name)
+--~     local a, b = lpegmatch(pattern,name)
+--~     if b then
+--~         return sub(name,a,b-2)
+--~     elseif a then
+--~         return sub(name,a)
+--~     else
+--~         return name
+--~     end
+--~ end
+
+--~ local test = file.extname
+--~ local test = file.basename
+--~ local test = file.dirname
+--~ local test = file.addsuffix
+--~ local test = file.replacesuffix
+--~ local test = file.nameonly
+
+--~ print(1,test("./a/b/c/abd.def.xxx","!!!"))
+--~ print(2,test("./../b/c/abd.def.xxx","!!!"))
+--~ print(3,test("a/b/c/abd.def.xxx","!!!"))
+--~ print(4,test("a/b/c/def.xxx","!!!"))
+--~ print(5,test("a/b/c/def","!!!"))
+--~ print(6,test("def","!!!"))
+--~ print(7,test("def.xxx","!!!"))
+
+--~ local tim = os.clock() for i=1,250000 do local ext = test("abd.def.xxx","!!!") end print(os.clock()-tim)
+
+-- also rewrite previous
+
+local letter    = lpeg.R("az","AZ") + lpeg.S("_-+")
+local separator = lpeg.P("://")
+
+local qualified = lpeg.P(".")^0 * lpeg.P("/") + letter*lpeg.P(":") + letter^1*separator + letter^1 * lpeg.P("/")
+local rootbased = lpeg.P("/") + letter*lpeg.P(":")
+
+-- ./name ../name  /name c: :// name/name
+
+function file.is_qualified_path(filename)
+    return lpegmatch(qualified,filename) ~= nil
+end
+
+function file.is_rootbased_path(filename)
+    return lpegmatch(rootbased,filename) ~= nil
+end
+
+local slash  = lpeg.S("\\/")
+local period = lpeg.P(".")
+local drive  = lpeg.C(lpeg.R("az","AZ")) * lpeg.P(":")
+local path   = lpeg.C(((1-slash)^0 * slash)^0)
+local suffix = period * lpeg.C(lpeg.P(1-period)^0 * lpeg.P(-1))
+local base   = lpeg.C((1-suffix)^0)
+
+local pattern = (drive + lpeg.Cc("")) * (path + lpeg.Cc("")) * (base + lpeg.Cc("")) * (suffix + lpeg.Cc(""))
+
+function file.splitname(str) -- returns drive, path, base, suffix
+    return lpegmatch(pattern,str)
+end
+
+-- function test(t) for k, v in next, t do print(v, "=>", file.splitname(v)) end end
+--
+-- test { "c:", "c:/aa", "c:/aa/bb", "c:/aa/bb/cc", "c:/aa/bb/cc.dd", "c:/aa/bb/cc.dd.ee" }
+-- test { "c:", "c:aa", "c:aa/bb", "c:aa/bb/cc", "c:aa/bb/cc.dd", "c:aa/bb/cc.dd.ee" }
+-- test { "/aa", "/aa/bb", "/aa/bb/cc", "/aa/bb/cc.dd", "/aa/bb/cc.dd.ee" }
+-- test { "aa", "aa/bb", "aa/bb/cc", "aa/bb/cc.dd", "aa/bb/cc.dd.ee" }
+
+--~ -- todo:
+--~
+--~ if os.type == "windows" then
+--~     local currentdir = lfs.currentdir
+--~     function lfs.currentdir()
+--~         return (gsub(currentdir(),"\\","/"))
+--~     end
+--~ end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-md5'] = {
+    version   = 1.001,
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- This also provides file checksums and checkers.
+
+local gsub, format, byte = string.gsub, string.format, string.byte
+
+local function convert(str,fmt)
+    return (gsub(md5.sum(str),".",function(chr) return format(fmt,byte(chr)) end))
+end
+
+if not md5.HEX then function md5.HEX(str) return convert(str,"%02X") end end
+if not md5.hex then function md5.hex(str) return convert(str,"%02x") end end
+if not md5.dec then function md5.dec(str) return convert(str,"%03i") end end
+
+--~ if not md5.HEX then
+--~     local function remap(chr) return format("%02X",byte(chr)) end
+--~     function md5.HEX(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+--~ if not md5.hex then
+--~     local function remap(chr) return format("%02x",byte(chr)) end
+--~     function md5.hex(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+--~ if not md5.dec then
+--~     local function remap(chr) return format("%03i",byte(chr)) end
+--~     function md5.dec(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+
+file.needs_updating_threshold = 1
+
+function file.needs_updating(oldname,newname) -- size modification access change
+    local oldtime = lfs.attributes(oldname, modification)
+    local newtime = lfs.attributes(newname, modification)
+    if newtime >= oldtime then
+        return false
+    elseif oldtime - newtime < file.needs_updating_threshold then
+        return false
+    else
+        return true
+    end
+end
+
+function file.checksum(name)
+    if md5 then
+        local data = io.loaddata(name)
+        if data then
+            return md5.HEX(data)
+        end
+    end
+    return nil
+end
+
+function file.loadchecksum(name)
+    if md5 then
+        local data = io.loaddata(name .. ".md5")
+        return data and (gsub(data,"%s",""))
+    end
+    return nil
+end
+
+function file.savechecksum(name, checksum)
+    if not checksum then checksum = file.checksum(name) end
+    if checksum then
+        io.savedata(name .. ".md5",checksum)
+        return checksum
+    end
+    return nil
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-url'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local char, gmatch, gsub = string.char, string.gmatch, string.gsub
+local tonumber, type = tonumber, type
+local lpegmatch = lpeg.match
+
+-- from the spec (on the web):
+--
+--     foo://example.com:8042/over/there?name=ferret#nose
+--     \_/   \______________/\_________/ \_________/ \__/
+--      |           |            |            |        |
+--   scheme     authority       path        query   fragment
+--      |   _____________________|__
+--     / \ /                        \
+--     urn:example:animal:ferret:nose
+
+url = url or { }
+
+local function tochar(s)
+    return char(tonumber(s,16))
+end
+
+local colon, qmark, hash, slash, percent, endofstring = lpeg.P(":"), lpeg.P("?"), lpeg.P("#"), lpeg.P("/"), lpeg.P("%"), lpeg.P(-1)
+
+local hexdigit  = lpeg.R("09","AF","af")
+local plus      = lpeg.P("+")
+local escaped   = (plus / " ") + (percent * lpeg.C(hexdigit * hexdigit) / tochar)
+
+-- we assume schemes with more than 1 character (in order to avoid problems with windows disks)
+
+local scheme    =                 lpeg.Cs((escaped+(1-colon-slash-qmark-hash))^2) * colon + lpeg.Cc("")
+local authority = slash * slash * lpeg.Cs((escaped+(1-      slash-qmark-hash))^0)         + lpeg.Cc("")
+local path      = slash *         lpeg.Cs((escaped+(1-            qmark-hash))^0)         + lpeg.Cc("")
+local query     = qmark         * lpeg.Cs((escaped+(1-                  hash))^0)         + lpeg.Cc("")
+local fragment  = hash          * lpeg.Cs((escaped+(1-           endofstring))^0)         + lpeg.Cc("")
+
+local parser = lpeg.Ct(scheme * authority * path * query * fragment)
+
+-- todo: reconsider Ct as we can as well have five return values (saves a table)
+-- so we can have two parsers, one with and one without
+
+function url.split(str)
+    return (type(str) == "string" and lpegmatch(parser,str)) or str
+end
+
+-- todo: cache them
+
+function url.hashed(str)
+    local s = url.split(str)
+    local somescheme = s[1] ~= ""
+    return {
+        scheme    = (somescheme and s[1]) or "file",
+        authority = s[2],
+        path      = s[3],
+        query     = s[4],
+        fragment  = s[5],
+        original  = str,
+        noscheme  = not somescheme,
+    }
+end
+
+function url.hasscheme(str)
+    return url.split(str)[1] ~= ""
+end
+
+function url.addscheme(str,scheme)
+    return (url.hasscheme(str) and str) or ((scheme or "file:///") .. str)
+end
+
+function url.construct(hash)
+    local fullurl = hash.sheme .. "://".. hash.authority .. hash.path
+    if hash.query then
+        fullurl = fullurl .. "?".. hash.query
+    end
+    if hash.fragment then
+        fullurl = fullurl .. "?".. hash.fragment
+    end
+    return fullurl
+end
+
+function url.filename(filename)
+    local t = url.hashed(filename)
+    return (t.scheme == "file" and (gsub(t.path,"^/([a-zA-Z])([:|])/)","%1:"))) or filename
+end
+
+function url.query(str)
+    if type(str) == "string" then
+        local t = { }
+        for k, v in gmatch(str,"([^&=]*)=([^&=]*)") do
+            t[k] = v
+        end
+        return t
+    else
+        return str
+    end
+end
+
+--~ print(url.filename("file:///c:/oeps.txt"))
+--~ print(url.filename("c:/oeps.txt"))
+--~ print(url.filename("file:///oeps.txt"))
+--~ print(url.filename("file:///etc/test.txt"))
+--~ print(url.filename("/oeps.txt"))
+
+--~ from the spec on the web (sort of):
+--~
+--~ function test(str)
+--~     print(table.serialize(url.hashed(str)))
+--~ end
+--~
+--~ test("%56pass%20words")
+--~ test("file:///c:/oeps.txt")
+--~ test("file:///c|/oeps.txt")
+--~ test("file:///etc/oeps.txt")
+--~ test("file://./etc/oeps.txt")
+--~ test("file:////etc/oeps.txt")
+--~ test("ftp://ftp.is.co.za/rfc/rfc1808.txt")
+--~ test("http://www.ietf.org/rfc/rfc2396.txt")
+--~ test("ldap://[2001:db8::7]/c=GB?objectClass?one#what")
+--~ test("mailto:John.Doe@example.com")
+--~ test("news:comp.infosystems.www.servers.unix")
+--~ test("tel:+1-816-555-1212")
+--~ test("telnet://192.0.2.16:80/")
+--~ test("urn:oasis:names:specification:docbook:dtd:xml:4.1.2")
+--~ test("/etc/passwords")
+--~ test("http://www.pragma-ade.com/spaced%20name")
+
+--~ test("zip:///oeps/oeps.zip#bla/bla.tex")
+--~ test("zip:///oeps/oeps.zip?bla/bla.tex")
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-dir'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- dir.expand_name will be merged with cleanpath and collapsepath
+
+local type = type
+local find, gmatch, match, gsub = string.find, string.gmatch, string.match, string.gsub
+local lpegmatch = lpeg.match
+
+dir = dir or { }
+
+-- handy
+
+function dir.current()
+    return (gsub(lfs.currentdir(),"\\","/"))
+end
+
+-- optimizing for no string.find (*) does not save time
+
+local attributes = lfs.attributes
+local walkdir    = lfs.dir
+
+local function glob_pattern(path,patt,recurse,action)
+    local ok, scanner
+    if path == "/" then
+        ok, scanner = xpcall(function() return walkdir(path..".") end, function() end) -- kepler safe
+    else
+        ok, scanner = xpcall(function() return walkdir(path)      end, function() end) -- kepler safe
+    end
+    if ok and type(scanner) == "function" then
+        if not find(path,"/$") then path = path .. '/' end
+        for name in scanner do
+            local full = path .. name
+            local mode = attributes(full,'mode')
+            if mode == 'file' then
+                if find(full,patt) then
+                    action(full)
+                end
+            elseif recurse and (mode == "directory") and (name ~= '.') and (name ~= "..") then
+                glob_pattern(full,patt,recurse,action)
+            end
+        end
+    end
+end
+
+dir.glob_pattern = glob_pattern
+
+local function collect_pattern(path,patt,recurse,result)
+    local ok, scanner
+    result = result or { }
+    if path == "/" then
+        ok, scanner = xpcall(function() return walkdir(path..".") end, function() end) -- kepler safe
+    else
+        ok, scanner = xpcall(function() return walkdir(path)      end, function() end) -- kepler safe
+    end
+    if ok and type(scanner) == "function" then
+        if not find(path,"/$") then path = path .. '/' end
+        for name in scanner do
+            local full = path .. name
+            local attr = attributes(full)
+            local mode = attr.mode
+            if mode == 'file' then
+                if find(full,patt) then
+                    result[name] = attr
+                end
+            elseif recurse and (mode == "directory") and (name ~= '.') and (name ~= "..") then
+                attr.list = collect_pattern(full,patt,recurse)
+                result[name] = attr
+            end
+        end
+    end
+    return result
+end
+
+dir.collect_pattern = collect_pattern
+
+local P, S, R, C, Cc, Cs, Ct, Cv, V = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.Cc, lpeg.Cs, lpeg.Ct, lpeg.Cv, lpeg.V
+
+local pattern = Ct {
+    [1] = (C(P(".") + P("/")^1) + C(R("az","AZ") * P(":") * P("/")^0) + Cc("./")) * V(2) * V(3),
+    [2] = C(((1-S("*?/"))^0 * P("/"))^0),
+    [3] = C(P(1)^0)
+}
+
+local filter = Cs ( (
+    P("**") / ".*" +
+    P("*")  / "[^/]*" +
+    P("?")  / "[^/]" +
+    P(".")  / "%%." +
+    P("+")  / "%%+" +
+    P("-")  / "%%-" +
+    P(1)
+)^0 )
+
+local function glob(str,t)
+    if type(t) == "function" then
+        if type(str) == "table" then
+            for s=1,#str do
+                glob(str[s],t)
+            end
+        elseif lfs.isfile(str) then
+            t(str)
+        else
+            local split = lpegmatch(pattern,str)
+            if split then
+                local root, path, base = split[1], split[2], split[3]
+                local recurse = find(base,"%*%*")
+                local start = root .. path
+                local result = lpegmatch(filter,start .. base)
+                glob_pattern(start,result,recurse,t)
+            end
+        end
+    else
+        if type(str) == "table" then
+            local t = t or { }
+            for s=1,#str do
+                glob(str[s],t)
+            end
+            return t
+        elseif lfs.isfile(str) then
+            local t = t or { }
+            t[#t+1] = str
+            return t
+        else
+            local split = lpegmatch(pattern,str)
+            if split then
+                local t = t or { }
+                local action = action or function(name) t[#t+1] = name end
+                local root, path, base = split[1], split[2], split[3]
+                local recurse = find(base,"%*%*")
+                local start = root .. path
+                local result = lpegmatch(filter,start .. base)
+                glob_pattern(start,result,recurse,action)
+                return t
+            else
+                return { }
+            end
+        end
+    end
+end
+
+dir.glob = glob
+
+--~ list = dir.glob("**/*.tif")
+--~ list = dir.glob("/**/*.tif")
+--~ list = dir.glob("./**/*.tif")
+--~ list = dir.glob("oeps/**/*.tif")
+--~ list = dir.glob("/oeps/**/*.tif")
+
+local function globfiles(path,recurse,func,files) -- func == pattern or function
+    if type(func) == "string" then
+        local s = func -- alas, we need this indirect way
+        func = function(name) return find(name,s) end
+    end
+    files = files or { }
+    for name in walkdir(path) do
+        if find(name,"^%.") then
+            --- skip
+        else
+            local mode = attributes(name,'mode')
+            if mode == "directory" then
+                if recurse then
+                    globfiles(path .. "/" .. name,recurse,func,files)
+                end
+            elseif mode == "file" then
+                if func then
+                    if func(name) then
+                        files[#files+1] = path .. "/" .. name
+                    end
+                else
+                    files[#files+1] = path .. "/" .. name
+                end
+            end
+        end
+    end
+    return files
+end
+
+dir.globfiles = globfiles
+
+-- t = dir.glob("c:/data/develop/context/sources/**/????-*.tex")
+-- t = dir.glob("c:/data/develop/tex/texmf/**/*.tex")
+-- t = dir.glob("c:/data/develop/context/texmf/**/*.tex")
+-- t = dir.glob("f:/minimal/tex/**/*")
+-- print(dir.ls("f:/minimal/tex/**/*"))
+-- print(dir.ls("*.tex"))
+
+function dir.ls(pattern)
+    return table.concat(glob(pattern),"\n")
+end
+
+--~ mkdirs("temp")
+--~ mkdirs("a/b/c")
+--~ mkdirs(".","/a/b/c")
+--~ mkdirs("a","b","c")
+
+local make_indeed = true -- false
+
+if string.find(os.getenv("PATH"),";") then -- os.type == "windows"
+
+    function dir.mkdirs(...)
+        local str, pth, t = "", "", { ... }
+        for i=1,#t do
+            local s = t[i]
+            if s ~= "" then
+                if str ~= "" then
+                    str = str .. "/" .. s
+                else
+                    str = s
+                end
+            end
+        end
+        local first, middle, last
+        local drive = false
+        first, middle, last = match(str,"^(//)(//*)(.*)$")
+        if first then
+            -- empty network path == local path
+        else
+            first, last = match(str,"^(//)/*(.-)$")
+            if first then
+                middle, last = match(str,"([^/]+)/+(.-)$")
+                if middle then
+                    pth = "//" .. middle
+                else
+                    pth = "//" .. last
+                    last = ""
+                end
+            else
+                first, middle, last = match(str,"^([a-zA-Z]:)(/*)(.-)$")
+                if first then
+                    pth, drive = first .. middle, true
+                else
+                    middle, last = match(str,"^(/*)(.-)$")
+                    if not middle then
+                        last = str
+                    end
+                end
+            end
+        end
+        for s in gmatch(last,"[^/]+") do
+            if pth == "" then
+                pth = s
+            elseif drive then
+                pth, drive = pth .. s, false
+            else
+                pth = pth .. "/" .. s
+            end
+            if make_indeed and not lfs.isdir(pth) then
+                lfs.mkdir(pth)
+            end
+        end
+        return pth, (lfs.isdir(pth) == true)
+    end
+
+--~         print(dir.mkdirs("","","a","c"))
+--~         print(dir.mkdirs("a"))
+--~         print(dir.mkdirs("a:"))
+--~         print(dir.mkdirs("a:/b/c"))
+--~         print(dir.mkdirs("a:b/c"))
+--~         print(dir.mkdirs("a:/bbb/c"))
+--~         print(dir.mkdirs("/a/b/c"))
+--~         print(dir.mkdirs("/aaa/b/c"))
+--~         print(dir.mkdirs("//a/b/c"))
+--~         print(dir.mkdirs("///a/b/c"))
+--~         print(dir.mkdirs("a/bbb//ccc/"))
+
+    function dir.expand_name(str) -- will be merged with cleanpath and collapsepath
+        local first, nothing, last = match(str,"^(//)(//*)(.*)$")
+        if first then
+            first = dir.current() .. "/"
+        end
+        if not first then
+            first, last = match(str,"^(//)/*(.*)$")
+        end
+        if not first then
+            first, last = match(str,"^([a-zA-Z]:)(.*)$")
+            if first and not find(last,"^/") then
+                local d = lfs.currentdir()
+                if lfs.chdir(first) then
+                    first = dir.current()
+                end
+                lfs.chdir(d)
+            end
+        end
+        if not first then
+            first, last = dir.current(), str
+        end
+        last = gsub(last,"//","/")
+        last = gsub(last,"/%./","/")
+        last = gsub(last,"^/*","")
+        first = gsub(first,"/*$","")
+        if last == "" then
+            return first
+        else
+            return first .. "/" .. last
+        end
+    end
+
+else
+
+    function dir.mkdirs(...)
+        local str, pth, t = "", "", { ... }
+        for i=1,#t do
+            local s = t[i]
+            if s ~= "" then
+                if str ~= "" then
+                    str = str .. "/" .. s
+                else
+                    str = s
+                end
+            end
+        end
+        str = gsub(str,"/+","/")
+        if find(str,"^/") then
+            pth = "/"
+            for s in gmatch(str,"[^/]+") do
+                local first = (pth == "/")
+                if first then
+                    pth = pth .. s
+                else
+                    pth = pth .. "/" .. s
+                end
+                if make_indeed and not first and not lfs.isdir(pth) then
+                    lfs.mkdir(pth)
+                end
+            end
+        else
+            pth = "."
+            for s in gmatch(str,"[^/]+") do
+                pth = pth .. "/" .. s
+                if make_indeed and not lfs.isdir(pth) then
+                    lfs.mkdir(pth)
+                end
+            end
+        end
+        return pth, (lfs.isdir(pth) == true)
+    end
+
+--~         print(dir.mkdirs("","","a","c"))
+--~         print(dir.mkdirs("a"))
+--~         print(dir.mkdirs("/a/b/c"))
+--~         print(dir.mkdirs("/aaa/b/c"))
+--~         print(dir.mkdirs("//a/b/c"))
+--~         print(dir.mkdirs("///a/b/c"))
+--~         print(dir.mkdirs("a/bbb//ccc/"))
+
+    function dir.expand_name(str) -- will be merged with cleanpath and collapsepath
+        if not find(str,"^/") then
+            str = lfs.currentdir() .. "/" .. str
+        end
+        str = gsub(str,"//","/")
+        str = gsub(str,"/%./","/")
+        return str
+    end
+
+end
+
+dir.makedirs = dir.mkdirs
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-boolean'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+boolean = boolean or { }
+
+local type, tonumber = type, tonumber
+
+function boolean.tonumber(b)
+    if b then return 1 else return 0 end
+end
+
+function toboolean(str,tolerant)
+    if tolerant then
+        local tstr = type(str)
+        if tstr == "string" then
+            return str == "true" or str == "yes" or str == "on" or str == "1" or str == "t"
+        elseif tstr == "number" then
+            return tonumber(str) ~= 0
+        elseif tstr == "nil" then
+            return false
+        else
+            return str
+        end
+    elseif str == "true" then
+        return true
+    elseif str == "false" then
+        return false
+    else
+        return str
+    end
+end
+
+function string.is_boolean(str)
+    if type(str) == "string" then
+        if str == "true" or str == "yes" or str == "on" or str == "t" then
+            return true
+        elseif str == "false" or str == "no" or str == "off" or str == "f" then
+            return false
+        end
+    end
+    return nil
+end
+
+function boolean.alwaystrue()
+    return true
+end
+
+function boolean.falsetrue()
+    return false
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-unicode'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+if not unicode then
+
+    unicode = { utf8 = { } }
+
+    local floor, char = math.floor, string.char
+
+    function unicode.utf8.utfchar(n)
+        if n < 0x80 then
+            return char(n)
+        elseif n < 0x800 then
+            return char(0xC0 + floor(n/0x40))  .. char(0x80 + (n % 0x40))
+        elseif n < 0x10000 then
+            return char(0xE0 + floor(n/0x1000)) .. char(0x80 + (floor(n/0x40) % 0x40)) .. char(0x80 + (n % 0x40))
+        elseif n < 0x40000 then
+            return char(0xF0 + floor(n/0x40000)) .. char(0x80 + floor(n/0x1000)) .. char(0x80 + (floor(n/0x40) % 0x40)) .. char(0x80 + (n % 0x40))
+        else -- wrong:
+          -- return char(0xF1 + floor(n/0x1000000)) .. char(0x80 + floor(n/0x40000)) .. char(0x80 + floor(n/0x1000)) .. char(0x80 + (floor(n/0x40) % 0x40)) .. char(0x80 + (n % 0x40))
+            return "?"
+        end
+    end
+
+end
+
+utf = utf or unicode.utf8
+
+local concat, utfchar, utfgsub = table.concat, utf.char, utf.gsub
+local char, byte, find, bytepairs = string.char, string.byte, string.find, string.bytepairs
+
+-- 0  EF BB BF      UTF-8
+-- 1  FF FE         UTF-16-little-endian
+-- 2  FE FF         UTF-16-big-endian
+-- 3  FF FE 00 00   UTF-32-little-endian
+-- 4  00 00 FE FF   UTF-32-big-endian
+
+unicode.utfname = {
+    [0] = 'utf-8',
+    [1] = 'utf-16-le',
+    [2] = 'utf-16-be',
+    [3] = 'utf-32-le',
+    [4] = 'utf-32-be'
+}
+
+-- \000 fails in <= 5.0 but is valid in >=5.1 where %z is depricated
+
+function unicode.utftype(f)
+    local str = f:read(4)
+    if not str then
+        f:seek('set')
+        return 0
+ -- elseif find(str,"^%z%z\254\255") then            -- depricated
+ -- elseif find(str,"^\000\000\254\255") then        -- not permitted and bugged
+    elseif find(str,"\000\000\254\255",1,true) then  -- seems to work okay (TH)
+        return 4
+ -- elseif find(str,"^\255\254%z%z") then            -- depricated
+ -- elseif find(str,"^\255\254\000\000") then        -- not permitted and bugged
+    elseif find(str,"\255\254\000\000",1,true) then  -- seems to work okay (TH)
+        return 3
+    elseif find(str,"^\254\255") then
+        f:seek('set',2)
+        return 2
+    elseif find(str,"^\255\254") then
+        f:seek('set',2)
+        return 1
+    elseif find(str,"^\239\187\191") then
+        f:seek('set',3)
+        return 0
+    else
+        f:seek('set')
+        return 0
+    end
+end
+
+function unicode.utf16_to_utf8(str, endian) -- maybe a gsub is faster or an lpeg
+    local result, tmp, n, m, p = { }, { }, 0, 0, 0
+    -- lf | cr | crlf / (cr:13, lf:10)
+    local function doit()
+        if n == 10 then
+            if p ~= 13 then
+                result[#result+1] = concat(tmp)
+                tmp = { }
+                p = 0
+            end
+        elseif n == 13 then
+            result[#result+1] = concat(tmp)
+            tmp = { }
+            p = n
+        else
+            tmp[#tmp+1] = utfchar(n)
+            p = 0
+        end
+    end
+    for l,r in bytepairs(str) do
+        if r then
+            if endian then
+                n = l*256 + r
+            else
+                n = r*256 + l
+            end
+            if m > 0 then
+                n = (m-0xD800)*0x400 + (n-0xDC00) + 0x10000
+                m = 0
+                doit()
+            elseif n >= 0xD800 and n <= 0xDBFF then
+                m = n
+            else
+                doit()
+            end
+        end
+    end
+    if #tmp > 0 then
+        result[#result+1] = concat(tmp)
+    end
+    return result
+end
+
+function unicode.utf32_to_utf8(str, endian)
+    local result = { }
+    local tmp, n, m, p = { }, 0, -1, 0
+    -- lf | cr | crlf / (cr:13, lf:10)
+    local function doit()
+        if n == 10 then
+            if p ~= 13 then
+                result[#result+1] = concat(tmp)
+                tmp = { }
+                p = 0
+            end
+        elseif n == 13 then
+            result[#result+1] = concat(tmp)
+            tmp = { }
+            p = n
+        else
+            tmp[#tmp+1] = utfchar(n)
+            p = 0
+        end
+    end
+    for a,b in bytepairs(str) do
+        if a and b then
+            if m < 0 then
+                if endian then
+                    m = a*256*256*256 + b*256*256
+                else
+                    m = b*256 + a
+                end
+            else
+                if endian then
+                    n = m + a*256 + b
+                else
+                    n = m + b*256*256*256 + a*256*256
+                end
+                m = -1
+                doit()
+            end
+        else
+            break
+        end
+    end
+    if #tmp > 0 then
+        result[#result+1] = concat(tmp)
+    end
+    return result
+end
+
+local function little(c)
+    local b = byte(c) -- b = c:byte()
+    if b < 0x10000 then
+        return char(b%256,b/256)
+    else
+        b = b - 0x10000
+        local b1, b2 = b/1024 + 0xD800, b%1024 + 0xDC00
+        return char(b1%256,b1/256,b2%256,b2/256)
+    end
+end
+
+local function big(c)
+    local b = byte(c)
+    if b < 0x10000 then
+        return char(b/256,b%256)
+    else
+        b = b - 0x10000
+        local b1, b2 = b/1024 + 0xD800, b%1024 + 0xDC00
+        return char(b1/256,b1%256,b2/256,b2%256)
+    end
+end
+
+function unicode.utf8_to_utf16(str,littleendian)
+    if littleendian then
+        return char(255,254) .. utfgsub(str,".",little)
+    else
+        return char(254,255) .. utfgsub(str,".",big)
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-math'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local floor, sin, cos, tan = math.floor, math.sin, math.cos, math.tan
+
+if not math.round then
+    function math.round(x)
+        return floor(x + 0.5)
+    end
+end
+
+if not math.div then
+    function math.div(n,m)
+        return floor(n/m)
+    end
+end
+
+if not math.mod then
+    function math.mod(n,m)
+        return n % m
+    end
+end
+
+local pipi = 2*math.pi/360
+
+function math.sind(d)
+    return sin(d*pipi)
+end
+
+function math.cosd(d)
+    return cos(d*pipi)
+end
+
+function math.tand(d)
+    return tan(d*pipi)
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-utils'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- hm, quite unreadable
+
+local gsub = string.gsub
+local concat = table.concat
+local type, next = type, next
+
+if not utils        then utils        = { } end
+if not utils.merger then utils.merger = { } end
+if not utils.lua    then utils.lua    = { } end
+
+utils.merger.m_begin = "begin library merge"
+utils.merger.m_end   = "end library merge"
+utils.merger.pattern =
+    "%c+" ..
+    "%-%-%s+" .. utils.merger.m_begin ..
+    "%c+(.-)%c+" ..
+    "%-%-%s+" .. utils.merger.m_end ..
+    "%c+"
+
+function utils.merger._self_fake_()
+    return
+        "-- " .. "created merged file" .. "\n\n" ..
+        "-- " .. utils.merger.m_begin  .. "\n\n" ..
+        "-- " .. utils.merger.m_end    .. "\n\n"
+end
+
+function utils.report(...)
+    print(...)
+end
+
+utils.merger.strip_comment = true
+
+function utils.merger._self_load_(name)
+    local f, data = io.open(name), ""
+    if f then
+        utils.report("reading merge from %s",name)
+        data = f:read("*all")
+        f:close()
+    else
+        utils.report("unknown file to merge %s",name)
+    end
+    if data and utils.merger.strip_comment then
+        -- saves some 20K
+        data = gsub(data,"%-%-~[^\n\r]*[\r\n]", "")
+    end
+    return data or ""
+end
+
+function utils.merger._self_save_(name, data)
+    if data ~= "" then
+        local f = io.open(name,'w')
+        if f then
+            utils.report("saving merge from %s",name)
+            f:write(data)
+            f:close()
+        end
+    end
+end
+
+function utils.merger._self_swap_(data,code)
+    if data ~= "" then
+        return (gsub(data,utils.merger.pattern, function(s)
+            return "\n\n" .. "-- "..utils.merger.m_begin .. "\n" .. code .. "\n" .. "-- "..utils.merger.m_end .. "\n\n"
+        end, 1))
+    else
+        return ""
+    end
+end
+
+--~ stripper:
+--~
+--~ data = gsub(data,"%-%-~[^\n]*\n","")
+--~ data = gsub(data,"\n\n+","\n")
+
+function utils.merger._self_libs_(libs,list)
+    local result, f, frozen = { }, nil, false
+    result[#result+1] = "\n"
+    if type(libs) == 'string' then libs = { libs } end
+    if type(list) == 'string' then list = { list } end
+    local foundpath = nil
+    for i=1,#libs do
+        local lib = libs[i]
+        for j=1,#list do
+            local pth = gsub(list[j],"\\","/") -- file.clean_path
+            utils.report("checking library path %s",pth)
+            local name = pth .. "/" .. lib
+            if lfs.isfile(name) then
+                foundpath = pth
+            end
+        end
+        if foundpath then break end
+    end
+    if foundpath then
+        utils.report("using library path %s",foundpath)
+        local right, wrong = { }, { }
+        for i=1,#libs do
+            local lib = libs[i]
+            local fullname = foundpath .. "/" .. lib
+            if lfs.isfile(fullname) then
+            --  right[#right+1] = lib
+                utils.report("merging library %s",fullname)
+                result[#result+1] = "do -- create closure to overcome 200 locals limit"
+                result[#result+1] = io.loaddata(fullname,true)
+                result[#result+1] = "end -- of closure"
+            else
+            --  wrong[#wrong+1] = lib
+                utils.report("no library %s",fullname)
+            end
+        end
+        if #right > 0 then
+            utils.report("merged libraries: %s",concat(right," "))
+        end
+        if #wrong > 0 then
+            utils.report("skipped libraries: %s",concat(wrong," "))
+        end
+    else
+        utils.report("no valid library path found")
+    end
+    return concat(result, "\n\n")
+end
+
+function utils.merger.selfcreate(libs,list,target)
+    if target then
+        utils.merger._self_save_(
+            target,
+            utils.merger._self_swap_(
+                utils.merger._self_fake_(),
+                utils.merger._self_libs_(libs,list)
+            )
+        )
+    end
+end
+
+function utils.merger.selfmerge(name,libs,list,target)
+    utils.merger._self_save_(
+        target or name,
+        utils.merger._self_swap_(
+            utils.merger._self_load_(name),
+            utils.merger._self_libs_(libs,list)
+        )
+    )
+end
+
+function utils.merger.selfclean(name)
+    utils.merger._self_save_(
+        name,
+        utils.merger._self_swap_(
+            utils.merger._self_load_(name),
+            ""
+        )
+    )
+end
+
+function utils.lua.compile(luafile, lucfile, cleanup, strip) -- defaults: cleanup=false strip=true
+ -- utils.report("compiling",luafile,"into",lucfile)
+    os.remove(lucfile)
+    local command = "-o " .. string.quote(lucfile) .. " " .. string.quote(luafile)
+    if strip ~= false then
+        command = "-s " .. command
+    end
+    local done = (os.spawn("texluac " .. command) == 0) or (os.spawn("luac " .. command) == 0)
+    if done and cleanup == true and lfs.isfile(lucfile) and lfs.isfile(luafile) then
+     -- utils.report("removing",luafile)
+        os.remove(luafile)
+    end
+    return done
+end
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-aux'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- for inline, no store split : for s in string.gmatch(str,",* *([^,]+)") do .. end
+
+aux = aux or { }
+
+local concat, format, gmatch = table.concat, string.format, string.gmatch
+local tostring, type = tostring, type
+local lpegmatch = lpeg.match
+
+local P, R, V = lpeg.P, lpeg.R, lpeg.V
+
+local escape, left, right = P("\\"), P('{'), P('}')
+
+lpeg.patterns.balanced = P {
+    [1] = ((escape * (left+right)) + (1 - (left+right)) + V(2))^0,
+    [2] = left * V(1) * right
+}
+
+local space     = lpeg.P(' ')
+local equal     = lpeg.P("=")
+local comma     = lpeg.P(",")
+local lbrace    = lpeg.P("{")
+local rbrace    = lpeg.P("}")
+local nobrace   = 1 - (lbrace+rbrace)
+local nested    = lpeg.P { lbrace * (nobrace + lpeg.V(1))^0 * rbrace }
+local spaces    = space^0
+
+local value     = lpeg.P(lbrace * lpeg.C((nobrace + nested)^0) * rbrace) + lpeg.C((nested + (1-comma))^0)
+
+local key       = lpeg.C((1-equal-comma)^1)
+local pattern_a = (space+comma)^0 * (key * equal * value + key * lpeg.C(""))
+local pattern_c = (space+comma)^0 * (key * equal * value)
+
+local key       = lpeg.C((1-space-equal-comma)^1)
+local pattern_b = spaces * comma^0 * spaces * (key * ((spaces * equal * spaces * value) + lpeg.C("")))
+
+-- "a=1, b=2, c=3, d={a{b,c}d}, e=12345, f=xx{a{b,c}d}xx, g={}" : outer {} removes, leading spaces ignored
+
+local hash = { }
+
+local function set(key,value) -- using Carg is slower here
+    hash[key] = value
+end
+
+local pattern_a_s = (pattern_a/set)^1
+local pattern_b_s = (pattern_b/set)^1
+local pattern_c_s = (pattern_c/set)^1
+
+aux.settings_to_hash_pattern_a = pattern_a_s
+aux.settings_to_hash_pattern_b = pattern_b_s
+aux.settings_to_hash_pattern_c = pattern_c_s
+
+function aux.make_settings_to_hash_pattern(set,how)
+    if how == "strict" then
+        return (pattern_c/set)^1
+    elseif how == "tolerant" then
+        return (pattern_b/set)^1
+    else
+        return (pattern_a/set)^1
+    end
+end
+
+function aux.settings_to_hash(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        if moretolerant then
+            lpegmatch(pattern_b_s,str)
+        else
+            lpegmatch(pattern_a_s,str)
+        end
+        return hash
+    else
+        return { }
+    end
+end
+
+function aux.settings_to_hash_tolerant(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        lpegmatch(pattern_b_s,str)
+        return hash
+    else
+        return { }
+    end
+end
+
+function aux.settings_to_hash_strict(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        lpegmatch(pattern_c_s,str)
+        return next(hash) and hash
+    else
+        return nil
+    end
+end
+
+local separator = comma * space^0
+local value     = lpeg.P(lbrace * lpeg.C((nobrace + nested)^0) * rbrace) + lpeg.C((nested + (1-comma))^0)
+local pattern   = lpeg.Ct(value*(separator*value)^0)
+
+-- "aap, {noot}, mies" : outer {} removes, leading spaces ignored
+
+aux.settings_to_array_pattern = pattern
+
+-- we could use a weak table as cache
+
+function aux.settings_to_array(str)
+    if not str or str == "" then
+        return { }
+    else
+        return lpegmatch(pattern,str)
+    end
+end
+
+local function set(t,v)
+    t[#t+1] = v
+end
+
+local value   = lpeg.P(lpeg.Carg(1)*value) / set
+local pattern = value*(separator*value)^0 * lpeg.Carg(1)
+
+function aux.add_settings_to_array(t,str)
+    return lpegmatch(pattern,str,nil,t)
+end
+
+function aux.hash_to_string(h,separator,yes,no,strict,omit)
+    if h then
+        local t, s = { }, table.sortedkeys(h)
+        omit = omit and table.tohash(omit)
+        for i=1,#s do
+            local key = s[i]
+            if not omit or not omit[key] then
+                local value = h[key]
+                if type(value) == "boolean" then
+                    if yes and no then
+                        if value then
+                            t[#t+1] = key .. '=' .. yes
+                        elseif not strict then
+                            t[#t+1] = key .. '=' .. no
+                        end
+                    elseif value or not strict then
+                        t[#t+1] = key .. '=' .. tostring(value)
+                    end
+                else
+                    t[#t+1] = key .. '=' .. value
+                end
+            end
+        end
+        return concat(t,separator or ",")
+    else
+        return ""
+    end
+end
+
+function aux.array_to_string(a,separator)
+    if a then
+        return concat(a,separator or ",")
+    else
+        return ""
+    end
+end
+
+function aux.settings_to_set(str,t)
+    t = t or { }
+    for s in gmatch(str,"%s*([^,]+)") do
+        t[s] = true
+    end
+    return t
+end
+
+local value     = lbrace * lpeg.C((nobrace + nested)^0) * rbrace
+local pattern   = lpeg.Ct((space + value)^0)
+
+function aux.arguments_to_table(str)
+    return lpegmatch(pattern,str)
+end
+
+-- temporary here
+
+function aux.getparameters(self,class,parentclass,settings)
+    local sc = self[class]
+    if not sc then
+        sc = table.clone(self[parent])
+        self[class] = sc
+    end
+    aux.settings_to_hash(settings,sc)
+end
+
+-- temporary here
+
+local digit         = lpeg.R("09")
+local period        = lpeg.P(".")
+local zero          = lpeg.P("0")
+local trailingzeros = zero^0 * -digit -- suggested by Roberto R
+local case_1        = period * trailingzeros / ""
+local case_2        = period * (digit - trailingzeros)^1 * (trailingzeros / "")
+local number        = digit^1 * (case_1 + case_2)
+local stripper      = lpeg.Cs((number + 1)^0)
+
+--~ local sample = "bla 11.00 bla 11 bla 0.1100 bla 1.00100 bla 0.00 bla 0.001 bla 1.1100 bla 0.100100100 bla 0.00100100100"
+--~ collectgarbage("collect")
+--~ str = string.rep(sample,10000)
+--~ local ts = os.clock()
+--~ lpegmatch(stripper,str)
+--~ print(#str, os.clock()-ts, lpegmatch(stripper,sample))
+
+lpeg.patterns.strip_zeros = stripper
+
+function aux.strip_zeros(str)
+    return lpegmatch(stripper,str)
+end
+
+function aux.definetable(target) -- defines undefined tables
+    local composed, t = nil, { }
+    for name in gmatch(target,"([^%.]+)") do
+        if composed then
+            composed = composed .. "." .. name
+        else
+            composed = name
+        end
+        t[#t+1] = format("%s = %s or { }",composed,composed)
+    end
+    return concat(t,"\n")
+end
+
+function aux.accesstable(target)
+    local t = _G
+    for name in gmatch(target,"([^%.]+)") do
+        t = t[name]
+    end
+    return t
+end
+
+-- as we use this a lot ...
+
+--~ function aux.cachefunction(action,weak)
+--~     local cache = { }
+--~     if weak then
+--~         setmetatable(cache, { __mode = "kv" } )
+--~     end
+--~     local function reminder(str)
+--~         local found = cache[str]
+--~         if not found then
+--~             found = action(str)
+--~             cache[str] = found
+--~         end
+--~         return found
+--~     end
+--~     return reminder, cache
+--~ end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-tra'] = {
+    version   = 1.001,
+    comment   = "companion to trac-tra.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- the <anonymous> tag is kind of generic and used for functions that are not
+-- bound to a variable, like node.new, node.copy etc (contrary to for instance
+-- node.has_attribute which is bound to a has_attribute local variable in mkiv)
+
+local debug = require "debug"
+
+local getinfo = debug.getinfo
+local type, next = type, next
+local concat = table.concat
+local format, find, lower, gmatch, gsub = string.format, string.find, string.lower, string.gmatch, string.gsub
+
+debugger = debugger or { }
+
+local counters = { }
+local names = { }
+
+-- one
+
+local function hook()
+    local f = getinfo(2,"f").func
+    local n = getinfo(2,"Sn")
+--  if n.what == "C" and n.name then print (n.namewhat .. ': ' .. n.name) end
+    if f then
+        local cf = counters[f]
+        if cf == nil then
+            counters[f] = 1
+            names[f] = n
+        else
+            counters[f] = cf + 1
+        end
+    end
+end
+local function getname(func)
+    local n = names[func]
+    if n then
+        if n.what == "C" then
+            return n.name or '<anonymous>'
+        else
+            -- source short_src linedefined what name namewhat nups func
+            local name = n.name or n.namewhat or n.what
+            if not name or name == "" then name = "?" end
+            return format("%s : %s : %s", n.short_src or "unknown source", n.linedefined or "--", name)
+        end
+    else
+        return "unknown"
+    end
+end
+function debugger.showstats(printer,threshold)
+    printer   = printer or texio.write or print
+    threshold = threshold or 0
+    local total, grandtotal, functions = 0, 0, 0
+    printer("\n") -- ugly but ok
+ -- table.sort(counters)
+    for func, count in next, counters do
+        if count > threshold then
+            local name = getname(func)
+            if not find(name,"for generator") then
+                printer(format("%8i  %s", count, name))
+                total = total + count
+            end
+        end
+        grandtotal = grandtotal + count
+        functions = functions + 1
+    end
+    printer(format("functions: %s, total: %s, grand total: %s, threshold: %s\n", functions, total, grandtotal, threshold))
+end
+
+-- two
+
+--~ local function hook()
+--~     local n = getinfo(2)
+--~     if n.what=="C" and not n.name then
+--~         local f = tostring(debug.traceback())
+--~         local cf = counters[f]
+--~         if cf == nil then
+--~             counters[f] = 1
+--~             names[f] = n
+--~         else
+--~             counters[f] = cf + 1
+--~         end
+--~     end
+--~ end
+--~ function debugger.showstats(printer,threshold)
+--~     printer   = printer or texio.write or print
+--~     threshold = threshold or 0
+--~     local total, grandtotal, functions = 0, 0, 0
+--~     printer("\n") -- ugly but ok
+--~  -- table.sort(counters)
+--~     for func, count in next, counters do
+--~         if count > threshold then
+--~             printer(format("%8i  %s", count, func))
+--~             total = total + count
+--~         end
+--~         grandtotal = grandtotal + count
+--~         functions = functions + 1
+--~     end
+--~     printer(format("functions: %s, total: %s, grand total: %s, threshold: %s\n", functions, total, grandtotal, threshold))
+--~ end
+
+-- rest
+
+function debugger.savestats(filename,threshold)
+    local f = io.open(filename,'w')
+    if f then
+        debugger.showstats(function(str) f:write(str) end,threshold)
+        f:close()
+    end
+end
+
+function debugger.enable()
+    debug.sethook(hook,"c")
+end
+
+function debugger.disable()
+    debug.sethook()
+--~ counters[debug.getinfo(2,"f").func] = nil
+end
+
+function debugger.tracing()
+    local n = tonumber(os.env['MTX.TRACE.CALLS']) or tonumber(os.env['MTX_TRACE_CALLS']) or 0
+    if n > 0 then
+        function debugger.tracing() return true  end ; return true
+    else
+        function debugger.tracing() return false end ; return false
+    end
+end
+
+--~ debugger.enable()
+
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+
+--~ debugger.disable()
+
+--~ print("")
+--~ debugger.showstats()
+--~ print("")
+--~ debugger.showstats(print,3)
+
+setters      = setters      or { }
+setters.data = setters.data or { }
+
+--~ local function set(t,what,value)
+--~     local data, done = t.data, t.done
+--~     if type(what) == "string" then
+--~         what = aux.settings_to_array(what) -- inefficient but ok
+--~     end
+--~     for i=1,#what do
+--~         local w = what[i]
+--~         for d, f in next, data do
+--~             if done[d] then
+--~                 -- prevent recursion due to wildcards
+--~             elseif find(d,w) then
+--~                 done[d] = true
+--~                 for i=1,#f do
+--~                     f[i](value)
+--~                 end
+--~             end
+--~         end
+--~     end
+--~ end
+
+local function set(t,what,value)
+    local data, done = t.data, t.done
+    if type(what) == "string" then
+        what = aux.settings_to_hash(what) -- inefficient but ok
+    end
+    for w, v in next, what do
+        if v == "" then
+            v = value
+        else
+            v = toboolean(v)
+        end
+        for d, f in next, data do
+            if done[d] then
+                -- prevent recursion due to wildcards
+            elseif find(d,w) then
+                done[d] = true
+                for i=1,#f do
+                    f[i](v)
+                end
+            end
+        end
+    end
+end
+
+local function reset(t)
+    for d, f in next, t.data do
+        for i=1,#f do
+            f[i](false)
+        end
+    end
+end
+
+local function enable(t,what)
+    set(t,what,true)
+end
+
+local function disable(t,what)
+    local data = t.data
+    if not what or what == "" then
+        t.done = { }
+        reset(t)
+    else
+        set(t,what,false)
+    end
+end
+
+function setters.register(t,what,...)
+    local data = t.data
+    what = lower(what)
+    local w = data[what]
+    if not w then
+        w = { }
+        data[what] = w
+    end
+    for _, fnc in next, { ... } do
+        local typ = type(fnc)
+        if typ == "function" then
+            w[#w+1] = fnc
+        elseif typ == "string" then
+            w[#w+1] = function(value) set(t,fnc,value,nesting) end
+        end
+    end
+end
+
+function setters.enable(t,what)
+    local e = t.enable
+    t.enable, t.done = enable, { }
+    enable(t,string.simpleesc(tostring(what)))
+    t.enable, t.done = e, { }
+end
+
+function setters.disable(t,what)
+    local e = t.disable
+    t.disable, t.done = disable, { }
+    disable(t,string.simpleesc(tostring(what)))
+    t.disable, t.done = e, { }
+end
+
+function setters.reset(t)
+    t.done = { }
+    reset(t)
+end
+
+function setters.list(t) -- pattern
+    local list = table.sortedkeys(t.data)
+    local user, system = { }, { }
+    for l=1,#list do
+        local what = list[l]
+        if find(what,"^%*") then
+            system[#system+1] = what
+        else
+            user[#user+1] = what
+        end
+    end
+    return user, system
+end
+
+function setters.show(t)
+    commands.writestatus("","")
+    local list = setters.list(t)
+    for k=1,#list do
+        commands.writestatus(t.name,list[k])
+    end
+    commands.writestatus("","")
+end
+
+-- we could have used a bit of oo and the trackers:enable syntax but
+-- there is already a lot of code around using the singular tracker
+
+-- we could make this into a module
+
+function setters.new(name)
+    local t
+    t = {
+        data     = { },
+        name     = name,
+        enable   = function(...) setters.enable  (t,...) end,
+        disable  = function(...) setters.disable (t,...) end,
+        register = function(...) setters.register(t,...) end,
+        list     = function(...) setters.list    (t,...) end,
+        show     = function(...) setters.show    (t,...) end,
+    }
+    setters.data[name] = t
+    return t
+end
+
+trackers    = setters.new("trackers")
+directives  = setters.new("directives")
+experiments = setters.new("experiments")
+
+-- nice trick: we overload two of the directives related functions with variants that
+-- do tracing (itself using a tracker) .. proof of concept
+
+local trace_directives  = false local trace_directives  = false  trackers.register("system.directives",  function(v) trace_directives  = v end)
+local trace_experiments = false local trace_experiments = false  trackers.register("system.experiments", function(v) trace_experiments = v end)
+
+local e = directives.enable
+local d = directives.disable
+
+function directives.enable(...)
+    commands.writestatus("directives","enabling: %s",concat({...}," "))
+    e(...)
+end
+
+function directives.disable(...)
+    commands.writestatus("directives","disabling: %s",concat({...}," "))
+    d(...)
+end
+
+local e = experiments.enable
+local d = experiments.disable
+
+function experiments.enable(...)
+    commands.writestatus("experiments","enabling: %s",concat({...}," "))
+    e(...)
+end
+
+function experiments.disable(...)
+    commands.writestatus("experiments","disabling: %s",concat({...}," "))
+    d(...)
+end
+
+-- a useful example
+
+directives.register("system.nostatistics", function(v)
+    statistics.enable = not v
+end)
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['luat-env'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- A former version provided functionality for non embeded core
+-- scripts i.e. runtime library loading. Given the amount of
+-- Lua code we use now, this no longer makes sense. Much of this
+-- evolved before bytecode arrays were available and so a lot of
+-- code has disappeared already.
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+local format, sub, match, gsub, find = string.format, string.sub, string.match, string.gsub, string.find
+local unquote, quote = string.unquote, string.quote
+
+-- precautions
+
+os.setlocale(nil,nil) -- useless feature and even dangerous in luatex
+
+function os.setlocale()
+    -- no way you can mess with it
+end
+
+-- dirty tricks
+
+if arg and (arg[0] == 'luatex' or arg[0] == 'luatex.exe') and arg[1] == "--luaonly" then
+    arg[-1]=arg[0] arg[0]=arg[2] for k=3,#arg do arg[k-2]=arg[k] end arg[#arg]=nil arg[#arg]=nil
+end
+
+if profiler and os.env["MTX_PROFILE_RUN"] == "YES" then
+    profiler.start("luatex-profile.log")
+end
+
+-- environment
+
+environment             = environment or { }
+environment.arguments   = { }
+environment.files       = { }
+environment.sortedflags = nil
+
+if not environment.jobname or environment.jobname == "" then if tex then environment.jobname = tex.jobname end end
+if not environment.version or environment.version == "" then             environment.version = "unknown"   end
+if not environment.jobname                              then             environment.jobname = "unknown"   end
+
+function environment.initialize_arguments(arg)
+    local arguments, files = { }, { }
+    environment.arguments, environment.files, environment.sortedflags = arguments, files, nil
+    for index=1,#arg do
+        local argument = arg[index]
+        if index > 0 then
+            local flag, value = match(argument,"^%-+(.-)=(.-)$")
+            if flag then
+                arguments[flag] = unquote(value or "")
+            else
+                flag = match(argument,"^%-+(.+)")
+                if flag then
+                    arguments[flag] = true
+                else
+                    files[#files+1] = argument
+                end
+            end
+        end
+    end
+    environment.ownname = environment.ownname or arg[0] or 'unknown.lua'
+end
+
+function environment.setargument(name,value)
+    environment.arguments[name] = value
+end
+
+-- todo: defaults, better checks e.g on type (boolean versus string)
+--
+-- tricky: too many hits when we support partials unless we add
+-- a registration of arguments so from now on we have 'partial'
+
+function environment.argument(name,partial)
+    local arguments, sortedflags = environment.arguments, environment.sortedflags
+    if arguments[name] then
+        return arguments[name]
+    elseif partial then
+        if not sortedflags then
+            sortedflags = table.sortedkeys(arguments)
+            for k=1,#sortedflags do
+                sortedflags[k] = "^" .. sortedflags[k]
+            end
+            environment.sortedflags = sortedflags
+        end
+        -- example of potential clash: ^mode ^modefile
+        for k=1,#sortedflags do
+            local v = sortedflags[k]
+            if find(name,v) then
+                return arguments[sub(v,2,#v)]
+            end
+        end
+    end
+    return nil
+end
+
+environment.argument("x",true)
+
+function environment.split_arguments(separator) -- rather special, cut-off before separator
+    local done, before, after = false, { }, { }
+    local original_arguments = environment.original_arguments
+    for k=1,#original_arguments do
+        local v = original_arguments[k]
+        if not done and v == separator then
+            done = true
+        elseif done then
+            after[#after+1] = v
+        else
+            before[#before+1] = v
+        end
+    end
+    return before, after
+end
+
+function environment.reconstruct_commandline(arg,noquote)
+    arg = arg or environment.original_arguments
+    if noquote and #arg == 1 then
+        local a = arg[1]
+        a = resolvers.resolve(a)
+        a = unquote(a)
+        return a
+    elseif #arg > 0 then
+        local result = { }
+        for i=1,#arg do
+            local a = arg[i]
+            a = resolvers.resolve(a)
+            a = unquote(a)
+            a = gsub(a,'"','\\"') -- tricky
+            if find(a," ") then
+                result[#result+1] = quote(a)
+            else
+                result[#result+1] = a
+            end
+        end
+        return table.join(result," ")
+    else
+        return ""
+    end
+end
+
+if arg then
+
+    -- new, reconstruct quoted snippets (maybe better just remove the " then and add them later)
+    local newarg, instring = { }, false
+
+    for index=1,#arg do
+        local argument = arg[index]
+        if find(argument,"^\"") then
+            newarg[#newarg+1] = gsub(argument,"^\"","")
+            if not find(argument,"\"$") then
+                instring = true
+            end
+        elseif find(argument,"\"$") then
+            newarg[#newarg] = newarg[#newarg] .. " " .. gsub(argument,"\"$","")
+            instring = false
+        elseif instring then
+            newarg[#newarg] = newarg[#newarg] .. " " .. argument
+        else
+            newarg[#newarg+1] = argument
+        end
+    end
+    for i=1,-5,-1 do
+        newarg[i] = arg[i]
+    end
+
+    environment.initialize_arguments(newarg)
+    environment.original_arguments = newarg
+    environment.raw_arguments = arg
+
+    arg = { } -- prevent duplicate handling
+
+end
+
+-- weird place ... depends on a not yet loaded module
+
+function environment.texfile(filename)
+    return resolvers.find_file(filename,'tex')
+end
+
+function environment.luafile(filename)
+    local resolved = resolvers.find_file(filename,'tex') or ""
+    if resolved ~= "" then
+        return resolved
+    end
+    resolved = resolvers.find_file(filename,'texmfscripts') or ""
+    if resolved ~= "" then
+        return resolved
+    end
+    return resolvers.find_file(filename,'luatexlibs') or ""
+end
+
+environment.loadedluacode = loadfile -- can be overloaded
+
+--~ function environment.loadedluacode(name)
+--~     if os.spawn("texluac -s -o texluac.luc " .. name) == 0 then
+--~         local chunk = loadstring(io.loaddata("texluac.luc"))
+--~         os.remove("texluac.luc")
+--~         return chunk
+--~     else
+--~         environment.loadedluacode = loadfile -- can be overloaded
+--~         return loadfile(name)
+--~     end
+--~ end
+
+function environment.luafilechunk(filename) -- used for loading lua bytecode in the format
+    filename = file.replacesuffix(filename, "lua")
+    local fullname = environment.luafile(filename)
+    if fullname and fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading file %s", fullname)
+        end
+        return environment.loadedluacode(fullname)
+    else
+        if trace_locating then
+            logs.report("fileio","unknown file %s", filename)
+        end
+        return nil
+    end
+end
+
+-- the next ones can use the previous ones / combine
+
+function environment.loadluafile(filename, version)
+    local lucname, luaname, chunk
+    local basename = file.removesuffix(filename)
+    if basename == filename then
+        lucname, luaname = basename .. ".luc",  basename .. ".lua"
+    else
+        lucname, luaname = nil, basename -- forced suffix
+    end
+    -- when not overloaded by explicit suffix we look for a luc file first
+    local fullname = (lucname and environment.luafile(lucname)) or ""
+    if fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading %s", fullname)
+        end
+        chunk = loadfile(fullname) -- this way we don't need a file exists check
+    end
+    if chunk then
+        assert(chunk)()
+        if version then
+            -- we check of the version number of this chunk matches
+            local v = version -- can be nil
+            if modules and modules[filename] then
+                v = modules[filename].version -- new method
+            elseif versions and versions[filename] then
+                v = versions[filename]        -- old method
+            end
+            if v == version then
+                return true
+            else
+                if trace_locating then
+                    logs.report("fileio","version mismatch for %s: lua=%s, luc=%s", filename, v, version)
+                end
+                environment.loadluafile(filename)
+            end
+        else
+            return true
+        end
+    end
+    fullname = (luaname and environment.luafile(luaname)) or ""
+    if fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading %s", fullname)
+        end
+        chunk = loadfile(fullname) -- this way we don't need a file exists check
+        if not chunk then
+            if trace_locating then
+                logs.report("fileio","unknown file %s", filename)
+            end
+        else
+            assert(chunk)()
+            return true
+        end
+    end
+    return false
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-inf'] = {
+    version   = 1.001,
+    comment   = "companion to trac-inf.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format = string.format
+
+local statusinfo, n, registered = { }, 0, { }
+
+statistics = statistics or { }
+
+statistics.enable    = true
+statistics.threshold = 0.05
+
+-- timing functions
+
+local clock = os.gettimeofday or os.clock
+
+local notimer
+
+function statistics.hastimer(instance)
+    return instance and instance.starttime
+end
+
+function statistics.resettiming(instance)
+    if not instance then
+        notimer = { timing = 0, loadtime = 0 }
+    else
+        instance.timing, instance.loadtime = 0, 0
+    end
+end
+
+function statistics.starttiming(instance)
+    if not instance then
+        notimer = { }
+        instance = notimer
+    end
+    local it = instance.timing
+    if not it then
+        it = 0
+    end
+    if it == 0 then
+        instance.starttime = clock()
+        if not instance.loadtime then
+            instance.loadtime = 0
+        end
+    else
+--~         logs.report("system","nested timing (%s)",tostring(instance))
+    end
+    instance.timing = it + 1
+end
+
+function statistics.stoptiming(instance, report)
+    if not instance then
+        instance = notimer
+    end
+    if instance then
+        local it = instance.timing
+        if it > 1 then
+            instance.timing = it - 1
+        else
+            local starttime = instance.starttime
+            if starttime then
+                local stoptime = clock()
+                local loadtime = stoptime - starttime
+                instance.stoptime = stoptime
+                instance.loadtime = instance.loadtime + loadtime
+                if report then
+                    statistics.report("load time %0.3f",loadtime)
+                end
+                instance.timing = 0
+                return loadtime
+            end
+        end
+    end
+    return 0
+end
+
+function statistics.elapsedtime(instance)
+    if not instance then
+        instance = notimer
+    end
+    return format("%0.3f",(instance and instance.loadtime) or 0)
+end
+
+function statistics.elapsedindeed(instance)
+    if not instance then
+        instance = notimer
+    end
+    local t = (instance and instance.loadtime) or 0
+    return t > statistics.threshold
+end
+
+function statistics.elapsedseconds(instance,rest) -- returns nil if 0 seconds
+    if statistics.elapsedindeed(instance) then
+        return format("%s seconds %s", statistics.elapsedtime(instance),rest or "")
+    end
+end
+
+-- general function
+
+function statistics.register(tag,fnc)
+    if statistics.enable and type(fnc) == "function" then
+        local rt = registered[tag] or (#statusinfo + 1)
+        statusinfo[rt] = { tag, fnc }
+        registered[tag] = rt
+        if #tag > n then n = #tag end
+    end
+end
+
+function statistics.show(reporter)
+    if statistics.enable then
+        if not reporter then reporter = function(tag,data,n) texio.write_nl(tag .. " " .. data) end end
+        -- this code will move
+        local register = statistics.register
+        register("luatex banner", function()
+            return string.lower(status.banner)
+        end)
+        register("control sequences", function()
+            return format("%s of %s", status.cs_count, status.hash_size+status.hash_extra)
+        end)
+        register("callbacks", function()
+            local total, indirect = status.callbacks or 0, status.indirect_callbacks or 0
+            return format("direct: %s, indirect: %s, total: %s", total-indirect, indirect, total)
+        end)
+        register("current memory usage", statistics.memused)
+        register("runtime",statistics.runtime)
+--         --
+        for i=1,#statusinfo do
+            local s = statusinfo[i]
+            local r = s[2]()
+            if r then
+                reporter(s[1],r,n)
+            end
+        end
+        texio.write_nl("") -- final newline
+        statistics.enable = false
+    end
+end
+
+function statistics.show_job_stat(tag,data,n)
+    texio.write_nl(format("%-15s: %s - %s","mkiv lua stats",tag:rpadd(n," "),data))
+end
+
+function statistics.memused() -- no math.round yet -)
+    local round = math.round or math.floor
+    return format("%s MB (ctx: %s MB)",round(collectgarbage("count")/1000), round(status.luastate_bytes/1000000))
+end
+
+if statistics.runtime then
+    -- already loaded and set
+elseif luatex and luatex.starttime then
+    statistics.starttime = luatex.starttime
+    statistics.loadtime = 0
+    statistics.timing = 0
+else
+    statistics.starttiming(statistics)
+end
+
+function statistics.runtime()
+    statistics.stoptiming(statistics)
+    return statistics.formatruntime(statistics.elapsedtime(statistics))
+end
+
+function statistics.formatruntime(runtime)
+    return format("%s seconds", statistics.elapsedtime(statistics))
+end
+
+function statistics.timed(action,report)
+    local timer = { }
+    report = report or logs.simple
+    statistics.starttiming(timer)
+    action()
+    statistics.stoptiming(timer)
+    report("total runtime: %s",statistics.elapsedtime(timer))
+end
+
+-- where, not really the best spot for this:
+
+commands = commands or { }
+
+local timer
+
+function commands.resettimer()
+    statistics.resettiming(timer)
+    statistics.starttiming(timer)
+end
+
+function commands.elapsedtime()
+    statistics.stoptiming(timer)
+    tex.sprint(statistics.elapsedtime(timer))
+end
+
+commands.resettimer()
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-log'] = {
+    version   = 1.001,
+    comment   = "companion to trac-log.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- this is old code that needs an overhaul
+
+--~ io.stdout:setvbuf("no")
+--~ io.stderr:setvbuf("no")
+
+local write_nl, write = texio.write_nl or print, texio.write or io.write
+local format, gmatch = string.format, string.gmatch
+local texcount = tex and tex.count
+
+if texlua then
+    write_nl = print
+    write    = io.write
+end
+
+--[[ldx--
+<p>This is a prelude to a more extensive logging module. For the sake
+of parsing log files, in addition to the standard logging we will
+provide an <l n='xml'/> structured file. Actually, any logging that
+is hooked into callbacks will be \XML\ by default.</p>
+--ldx]]--
+
+logs     = logs     or { }
+logs.xml = logs.xml or { }
+logs.tex = logs.tex or { }
+
+--[[ldx--
+<p>This looks pretty ugly but we need to speed things up a bit.</p>
+--ldx]]--
+
+logs.moreinfo = [[
+more information about ConTeXt and the tools that come with it can be found at:
+
+maillist : ntg-context@ntg.nl / http://www.ntg.nl/mailman/listinfo/ntg-context
+webpage  : http://www.pragma-ade.nl / http://tex.aanhet.net
+wiki     : http://contextgarden.net
+]]
+
+logs.levels = {
+    ['error']   = 1,
+    ['warning'] = 2,
+    ['info']    = 3,
+    ['debug']   = 4,
+}
+
+logs.functions = {
+    'report', 'start', 'stop', 'push', 'pop', 'line', 'direct',
+    'start_run', 'stop_run',
+    'start_page_number', 'stop_page_number',
+    'report_output_pages', 'report_output_log',
+    'report_tex_stat', 'report_job_stat',
+    'show_open', 'show_close', 'show_load',
+}
+
+logs.tracers = {
+}
+
+logs.level = 0
+logs.mode  = string.lower((os.getenv("MTX.LOG.MODE") or os.getenv("MTX_LOG_MODE") or "tex"))
+
+function logs.set_level(level)
+    logs.level = logs.levels[level] or level
+end
+
+function logs.set_method(method)
+    for _, v in next, logs.functions do
+        logs[v] = logs[method][v] or function() end
+    end
+end
+
+-- tex logging
+
+function logs.tex.report(category,fmt,...) -- new
+    if fmt then
+        write_nl(category .. " | " .. format(fmt,...))
+    else
+        write_nl(category .. " |")
+    end
+end
+
+function logs.tex.line(fmt,...) -- new
+    if fmt then
+        write_nl(format(fmt,...))
+    else
+        write_nl("")
+    end
+end
+
+--~ function logs.tex.start_page_number()
+--~     local real, user, sub = texcount.realpageno, texcount.userpageno, texcount.subpageno
+--~     if real > 0 then
+--~         if user > 0 then
+--~             if sub > 0 then
+--~                 write(format("[%s.%s.%s",real,user,sub))
+--~             else
+--~                 write(format("[%s.%s",real,user))
+--~             end
+--~         else
+--~             write(format("[%s",real))
+--~         end
+--~     else
+--~         write("[-")
+--~     end
+--~ end
+
+--~ function logs.tex.stop_page_number()
+--~     write("]")
+--~ end
+
+local real, user, sub
+
+function logs.tex.start_page_number()
+    real, user, sub = texcount.realpageno, texcount.userpageno, texcount.subpageno
+end
+
+function logs.tex.stop_page_number()
+    if real > 0 then
+        if user > 0 then
+            if sub > 0 then
+                logs.report("pages", "flushing realpage %s, userpage %s, subpage %s",real,user,sub)
+            else
+                logs.report("pages", "flushing realpage %s, userpage %s",real,user)
+            end
+        else
+            logs.report("pages", "flushing realpage %s",real)
+        end
+    else
+        logs.report("pages", "flushing page")
+    end
+    io.flush()
+end
+
+logs.tex.report_job_stat = statistics.show_job_stat
+
+-- xml logging
+
+function logs.xml.report(category,fmt,...) -- new
+    if fmt then
+        write_nl(format("<r category='%s'>%s</r>",category,format(fmt,...)))
+    else
+        write_nl(format("<r category='%s'/>",category))
+    end
+end
+function logs.xml.line(fmt,...) -- new
+    if fmt then
+        write_nl(format("<r>%s</r>",format(fmt,...)))
+    else
+        write_nl("<r/>")
+    end
+end
+
+function logs.xml.start() if logs.level > 0 then tw("<%s>" ) end end
+function logs.xml.stop () if logs.level > 0 then tw("</%s>") end end
+function logs.xml.push () if logs.level > 0 then tw("<!-- ") end end
+function logs.xml.pop  () if logs.level > 0 then tw(" -->" ) end end
+
+function logs.xml.start_run()
+    write_nl("<?xml version='1.0' standalone='yes'?>")
+    write_nl("<job>") --  xmlns='www.pragma-ade.com/luatex/schemas/context-job.rng'
+    write_nl("")
+end
+
+function logs.xml.stop_run()
+    write_nl("</job>")
+end
+
+function logs.xml.start_page_number()
+    write_nl(format("<p real='%s' page='%s' sub='%s'", texcount.realpageno, texcount.userpageno, texcount.subpageno))
+end
+
+function logs.xml.stop_page_number()
+    write("/>")
+    write_nl("")
+end
+
+function logs.xml.report_output_pages(p,b)
+    write_nl(format("<v k='pages' v='%s'/>", p))
+    write_nl(format("<v k='bytes' v='%s'/>", b))
+    write_nl("")
+end
+
+function logs.xml.report_output_log()
+end
+
+function logs.xml.report_tex_stat(k,v)
+    texiowrite_nl("log","<v k='"..k.."'>"..tostring(v).."</v>")
+end
+
+local level = 0
+
+function logs.xml.show_open(name)
+    level = level + 1
+    texiowrite_nl(format("<f l='%s' n='%s'>",level,name))
+end
+
+function logs.xml.show_close(name)
+    texiowrite("</f> ")
+    level = level - 1
+end
+
+function logs.xml.show_load(name)
+    texiowrite_nl(format("<f l='%s' n='%s'/>",level+1,name))
+end
+
+--
+
+local name, banner = 'report', 'context'
+
+local function report(category,fmt,...)
+    if fmt then
+        write_nl(format("%s | %s: %s",name,category,format(fmt,...)))
+    elseif category then
+        write_nl(format("%s | %s",name,category))
+    else
+        write_nl(format("%s |",name))
+    end
+end
+
+local function simple(fmt,...)
+    if fmt then
+        write_nl(format("%s | %s",name,format(fmt,...)))
+    else
+        write_nl(format("%s |",name))
+    end
+end
+
+function logs.setprogram(_name_,_banner_,_verbose_)
+    name, banner = _name_, _banner_
+    if _verbose_ then
+        trackers.enable("resolvers.locating")
+    end
+    logs.set_method("tex")
+    logs.report = report -- also used in libraries
+    logs.simple = simple -- only used in scripts !
+    if utils then
+        utils.report = simple
+    end
+    logs.verbose = _verbose_
+end
+
+function logs.setverbose(what)
+    if what then
+        trackers.enable("resolvers.locating")
+    else
+        trackers.disable("resolvers.locating")
+    end
+    logs.verbose = what or false
+end
+
+function logs.extendbanner(_banner_,_verbose_)
+    banner = banner .. " | ".. _banner_
+    if _verbose_ ~= nil then
+        logs.setverbose(what)
+    end
+end
+
+logs.verbose = false
+logs.report  = logs.tex.report
+logs.simple  = logs.tex.report
+
+function logs.reportlines(str) -- todo: <lines></lines>
+    for line in gmatch(str,"(.-)[\n\r]") do
+        logs.report(line)
+    end
+end
+
+function logs.reportline() -- for scripts too
+    logs.report()
+end
+
+logs.simpleline = logs.reportline
+
+function logs.reportbanner() -- for scripts too
+    logs.report(banner)
+end
+
+function logs.help(message,option)
+    logs.reportbanner()
+    logs.reportline()
+    logs.reportlines(message)
+    local moreinfo = logs.moreinfo or ""
+    if moreinfo ~= "" and option ~= "nomoreinfo" then
+        logs.reportline()
+        logs.reportlines(moreinfo)
+    end
+end
+
+logs.set_level('error')
+logs.set_method('tex')
+
+function logs.system(whereto,process,jobname,category,...)
+    for i=1,10 do
+        local f = io.open(whereto,"a")
+        if f then
+            f:write(format("%s %s => %s => %s => %s\r",os.date("%d/%m/%y %H:%m:%S"),process,jobname,category,format(...)))
+            f:close()
+            break
+        else
+            sleep(0.1)
+        end
+    end
+end
+
+--~ local syslogname = "oeps.xxx"
+--~
+--~ for i=1,10 do
+--~     logs.system(syslogname,"context","test","fonts","font %s recached due to newer version (%s)","blabla","123")
+--~ end
+
+function logs.fatal(where,...)
+    logs.report(where,"fatal error: %s, aborting now",format(...))
+    os.exit()
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-inp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files",
+}
+
+-- After a few years using the code the large luat-inp.lua file
+-- has been split up a bit. In the process some functionality was
+-- dropped:
+--
+-- * support for reading lsr files
+-- * selective scanning (subtrees)
+-- * some public auxiliary functions were made private
+--
+-- TODO: os.getenv -> os.env[]
+-- TODO: instances.[hashes,cnffiles,configurations,522]
+-- TODO: check escaping in find etc, too much, too slow
+
+-- This lib is multi-purpose and can be loaded again later on so that
+-- additional functionality becomes available. We will split thislogs.report("fileio",
+-- module in components once we're done with prototyping. This is the
+-- first code I wrote for LuaTeX, so it needs some cleanup. Before changing
+-- something in this module one can best check with Taco or Hans first; there
+-- is some nasty trickery going on that relates to traditional kpse support.
+
+-- To be considered: hash key lowercase, first entry in table filename
+-- (any case), rest paths (so no need for optimization). Or maybe a
+-- separate table that matches lowercase names to mixed case when
+-- present. In that case the lower() cases can go away. I will do that
+-- only when we run into problems with names ... well ... Iwona-Regular.
+
+-- Beware, loading and saving is overloaded in luat-tmp!
+
+local format, gsub, find, lower, upper, match, gmatch = string.format, string.gsub, string.find, string.lower, string.upper, string.match, string.gmatch
+local concat, insert, sortedkeys = table.concat, table.insert, table.sortedkeys
+local next, type = next, type
+local lpegmatch = lpeg.match
+
+local trace_locating, trace_detail, trace_expansions = false, false, false
+
+trackers.register("resolvers.locating",   function(v) trace_locating   = v end)
+trackers.register("resolvers.details",    function(v) trace_detail     = v end)
+trackers.register("resolvers.expansions", function(v) trace_expansions = v end) -- todo
+
+if not resolvers then
+    resolvers = {
+        suffixes     = { },
+        formats      = { },
+        dangerous    = { },
+        suffixmap    = { },
+        alternatives = { },
+        locators     = { },  -- locate databases
+        hashers      = { },  -- load databases
+        generators   = { },  -- generate databases
+    }
+end
+
+local resolvers = resolvers
+
+resolvers.locators  .notfound = { nil }
+resolvers.hashers   .notfound = { nil }
+resolvers.generators.notfound = { nil }
+
+resolvers.cacheversion = '1.0.1'
+resolvers.cnfname      = 'texmf.cnf'
+resolvers.luaname      = 'texmfcnf.lua'
+resolvers.homedir      = os.env[os.type == "windows" and 'USERPROFILE'] or os.env['HOME'] or '~'
+resolvers.cnfdefault   = '{$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,.local,}/web2c}'
+
+local dummy_path_expr = "^!*unset/*$"
+
+local formats      = resolvers.formats
+local suffixes     = resolvers.suffixes
+local dangerous    = resolvers.dangerous
+local suffixmap    = resolvers.suffixmap
+local alternatives = resolvers.alternatives
+
+formats['afm'] = 'AFMFONTS'       suffixes['afm'] = { 'afm' }
+formats['enc'] = 'ENCFONTS'       suffixes['enc'] = { 'enc' }
+formats['fmt'] = 'TEXFORMATS'     suffixes['fmt'] = { 'fmt' }
+formats['map'] = 'TEXFONTMAPS'    suffixes['map'] = { 'map' }
+formats['mp']  = 'MPINPUTS'       suffixes['mp']  = { 'mp' }
+formats['ocp'] = 'OCPINPUTS'      suffixes['ocp'] = { 'ocp' }
+formats['ofm'] = 'OFMFONTS'       suffixes['ofm'] = { 'ofm', 'tfm' }
+formats['otf'] = 'OPENTYPEFONTS'  suffixes['otf'] = { 'otf' } -- 'ttf'
+formats['opl'] = 'OPLFONTS'       suffixes['opl'] = { 'opl' }
+formats['otp'] = 'OTPINPUTS'      suffixes['otp'] = { 'otp' }
+formats['ovf'] = 'OVFFONTS'       suffixes['ovf'] = { 'ovf', 'vf' }
+formats['ovp'] = 'OVPFONTS'       suffixes['ovp'] = { 'ovp' }
+formats['tex'] = 'TEXINPUTS'      suffixes['tex'] = { 'tex' }
+formats['tfm'] = 'TFMFONTS'       suffixes['tfm'] = { 'tfm' }
+formats['ttf'] = 'TTFONTS'        suffixes['ttf'] = { 'ttf', 'ttc', 'dfont' }
+formats['pfb'] = 'T1FONTS'        suffixes['pfb'] = { 'pfb', 'pfa' }
+formats['vf']  = 'VFFONTS'        suffixes['vf']  = { 'vf' }
+
+formats['fea'] = 'FONTFEATURES'   suffixes['fea'] = { 'fea' }
+formats['cid'] = 'FONTCIDMAPS'    suffixes['cid'] = { 'cid', 'cidmap' }
+
+formats ['texmfscripts'] = 'TEXMFSCRIPTS' -- new
+suffixes['texmfscripts'] = { 'rb', 'pl', 'py' } -- 'lua'
+
+formats ['lua'] = 'LUAINPUTS' -- new
+suffixes['lua'] = { 'lua', 'luc', 'tma', 'tmc' }
+
+-- backward compatible ones
+
+alternatives['map files']            = 'map'
+alternatives['enc files']            = 'enc'
+alternatives['cid maps']             = 'cid' -- great, why no cid files
+alternatives['font feature files']   = 'fea' -- and fea files here
+alternatives['opentype fonts']       = 'otf'
+alternatives['truetype fonts']       = 'ttf'
+alternatives['truetype collections'] = 'ttc'
+alternatives['truetype dictionary']  = 'dfont'
+alternatives['type1 fonts']          = 'pfb'
+
+-- obscure ones
+
+formats ['misc fonts'] = ''
+suffixes['misc fonts'] = { }
+
+formats     ['sfd']                      = 'SFDFONTS'
+suffixes    ['sfd']                      = { 'sfd' }
+alternatives['subfont definition files'] = 'sfd'
+
+-- lib paths
+
+formats ['lib'] = 'CLUAINPUTS' -- new (needs checking)
+suffixes['lib'] = (os.libsuffix and { os.libsuffix }) or { 'dll', 'so' }
+
+-- In practice we will work within one tds tree, but i want to keep
+-- the option open to build tools that look at multiple trees, which is
+-- why we keep the tree specific data in a table. We used to pass the
+-- instance but for practical pusposes we now avoid this and use a
+-- instance variable.
+
+-- here we catch a few new thingies (todo: add these paths to context.tmf)
+--
+-- FONTFEATURES  = .;$TEXMF/fonts/fea//
+-- FONTCIDMAPS   = .;$TEXMF/fonts/cid//
+
+-- we always have one instance active
+
+resolvers.instance = resolvers.instance or nil -- the current one (slow access)
+local instance = resolvers.instance or nil -- the current one (fast access)
+
+function resolvers.newinstance()
+
+    -- store once, freeze and faster (once reset we can best use
+    -- instance.environment) maybe better have a register suffix
+    -- function
+
+    for k, v in next, suffixes do
+        for i=1,#v do
+            local vi = v[i]
+            if vi then
+                suffixmap[vi] = k
+            end
+        end
+    end
+
+    -- because vf searching is somewhat dangerous, we want to prevent
+    -- too liberal searching esp because we do a lookup on the current
+    -- path anyway; only tex (or any) is safe
+
+    for k, v in next, formats do
+        dangerous[k] = true
+    end
+    dangerous.tex = nil
+
+    -- the instance
+
+    local newinstance = {
+        rootpath        = '',
+        treepath        = '',
+        progname        = 'context',
+        engine          = 'luatex',
+        format          = '',
+        environment     = { },
+        variables       = { },
+        expansions      = { },
+        files           = { },
+        remap           = { },
+        configuration   = { },
+        setup           = { },
+        order           = { },
+        found           = { },
+        foundintrees    = { },
+        kpsevars        = { },
+        hashes          = { },
+        cnffiles        = { },
+        luafiles        = { },
+        lists           = { },
+        remember        = true,
+        diskcache       = true,
+        renewcache      = false,
+        scandisk        = true,
+        cachepath       = nil,
+        loaderror       = false,
+        sortdata        = false,
+        savelists       = true,
+        cleanuppaths    = true,
+        allresults      = false,
+        pattern         = nil, -- lists
+        data            = { }, -- only for loading
+        force_suffixes  = true,
+        fakepaths       = { },
+    }
+
+    local ne = newinstance.environment
+
+    for k,v in next, os.env do
+        ne[k] = resolvers.bare_variable(v)
+    end
+
+    return newinstance
+
+end
+
+function resolvers.setinstance(someinstance)
+    instance = someinstance
+    resolvers.instance = someinstance
+    return someinstance
+end
+
+function resolvers.reset()
+    return resolvers.setinstance(resolvers.newinstance())
+end
+
+local function reset_hashes()
+    instance.lists = { }
+    instance.found = { }
+end
+
+local function check_configuration() -- not yet ok, no time for debugging now
+    local ie, iv = instance.environment, instance.variables
+    local function fix(varname,default)
+        local proname = varname .. "." .. instance.progname or "crap"
+        local p, v = ie[proname], ie[varname] or iv[varname]
+        if not ((p and p ~= "") or (v and v ~= "")) then
+            iv[varname] = default -- or environment?
+        end
+    end
+    local name = os.name
+    if name == "windows" then
+        fix("OSFONTDIR", "c:/windows/fonts//")
+    elseif name == "macosx" then
+        fix("OSFONTDIR", "$HOME/Library/Fonts//;/Library/Fonts//;/System/Library/Fonts//")
+    else
+        -- bad luck
+    end
+    fix("LUAINPUTS"   , ".;$TEXINPUTS;$TEXMFSCRIPTS") -- no progname, hm
+    -- this will go away some day
+    fix("FONTFEATURES", ".;$TEXMF/fonts/{data,fea}//;$OPENTYPEFONTS;$TTFONTS;$T1FONTS;$AFMFONTS")
+    fix("FONTCIDMAPS" , ".;$TEXMF/fonts/{data,cid}//;$OPENTYPEFONTS;$TTFONTS;$T1FONTS;$AFMFONTS")
+    --
+    fix("LUATEXLIBS"  , ".;$TEXMF/luatex/lua//")
+end
+
+function resolvers.bare_variable(str) -- assumes str is a string
+    return (gsub(str,"\s*([\"\']?)(.+)%1\s*", "%2"))
+end
+
+function resolvers.settrace(n) -- no longer number but: 'locating' or 'detail'
+    if n then
+        trackers.disable("resolvers.*")
+        trackers.enable("resolvers."..n)
+    end
+end
+
+resolvers.settrace(os.getenv("MTX_INPUT_TRACE"))
+
+function resolvers.osenv(key)
+    local ie = instance.environment
+    local value = ie[key]
+    if value == nil then
+     -- local e = os.getenv(key)
+        local e = os.env[key]
+        if e == nil then
+         -- value = "" -- false
+        else
+            value = resolvers.bare_variable(e)
+        end
+        ie[key] = value
+    end
+    return value or ""
+end
+
+function resolvers.env(key)
+    return instance.environment[key] or resolvers.osenv(key)
+end
+
+--
+
+local function expand_vars(lst) -- simple vars
+    local variables, env = instance.variables, resolvers.env
+    local function resolve(a)
+        return variables[a] or env(a)
+    end
+    for k=1,#lst do
+        lst[k] = gsub(lst[k],"%$([%a%d%_%-]+)",resolve)
+    end
+end
+
+local function expanded_var(var) -- simple vars
+    local function resolve(a)
+        return instance.variables[a] or resolvers.env(a)
+    end
+    return (gsub(var,"%$([%a%d%_%-]+)",resolve))
+end
+
+local function entry(entries,name)
+    if name and (name ~= "") then
+        name = gsub(name,'%$','')
+        local result = entries[name..'.'..instance.progname] or entries[name]
+        if result then
+            return result
+        else
+            result = resolvers.env(name)
+            if result then
+                instance.variables[name] = result
+                resolvers.expand_variables()
+                return instance.expansions[name] or ""
+            end
+        end
+    end
+    return ""
+end
+
+local function is_entry(entries,name)
+    if name and name ~= "" then
+        name = gsub(name,'%$','')
+        return (entries[name..'.'..instance.progname] or entries[name]) ~= nil
+    else
+        return false
+    end
+end
+
+-- {a,b,c,d}
+-- a,b,c/{p,q,r},d
+-- a,b,c/{p,q,r}/d/{x,y,z}//
+-- a,b,c/{p,q/{x,y,z},r},d/{p,q,r}
+-- a,b,c/{p,q/{x,y,z},r},d/{p,q,r}
+-- a{b,c}{d,e}f
+-- {a,b,c,d}
+-- {a,b,c/{p,q,r},d}
+-- {a,b,c/{p,q,r}/d/{x,y,z}//}
+-- {a,b,c/{p,q/{x,y,z}},d/{p,q,r}}
+-- {a,b,c/{p,q/{x,y,z},w}v,d/{p,q,r}}
+-- {$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,.local,}/web2c}
+
+-- this one is better and faster, but it took me a while to realize
+-- that this kind of replacement is cleaner than messy parsing and
+-- fuzzy concatenating we can probably gain a bit with selectively
+-- applying lpeg, but experiments with lpeg parsing this proved not to
+-- work that well; the parsing is ok, but dealing with the resulting
+-- table is a pain because we need to work inside-out recursively
+
+local function do_first(a,b)
+    local t = { }
+    for s in gmatch(b,"[^,]+") do t[#t+1] = a .. s end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_second(a,b)
+    local t = { }
+    for s in gmatch(a,"[^,]+") do t[#t+1] = s .. b end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_both(a,b)
+    local t = { }
+    for sa in gmatch(a,"[^,]+") do
+        for sb in gmatch(b,"[^,]+") do
+            t[#t+1] = sa .. sb
+        end
+    end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_three(a,b,c)
+    return a .. b.. c
+end
+
+local function splitpathexpr(str, t, validate)
+    -- no need for further optimization as it is only called a
+    -- few times, we can use lpeg for the sub
+    if trace_expansions then
+        logs.report("fileio","expanding variable '%s'",str)
+    end
+    t = t or { }
+    str = gsub(str,",}",",@}")
+    str = gsub(str,"{,","{@,")
+ -- str = "@" .. str .. "@"
+    local ok, done
+    while true do
+        done = false
+        while true do
+            str, ok = gsub(str,"([^{},]+){([^{}]+)}",do_first)
+            if ok > 0 then done = true else break end
+        end
+        while true do
+            str, ok = gsub(str,"{([^{}]+)}([^{},]+)",do_second)
+            if ok > 0 then done = true else break end
+        end
+        while true do
+            str, ok = gsub(str,"{([^{}]+)}{([^{}]+)}",do_both)
+            if ok > 0 then done = true else break end
+        end
+        str, ok = gsub(str,"({[^{}]*){([^{}]+)}([^{}]*})",do_three)
+        if ok > 0 then done = true end
+        if not done then break end
+    end
+    str = gsub(str,"[{}]", "")
+    str = gsub(str,"@","")
+    if validate then
+        for s in gmatch(str,"[^,]+") do
+            s = validate(s)
+            if s then t[#t+1] = s end
+        end
+    else
+        for s in gmatch(str,"[^,]+") do
+            t[#t+1] = s
+        end
+    end
+    if trace_expansions then
+        for k=1,#t do
+            logs.report("fileio","% 4i: %s",k,t[k])
+        end
+    end
+    return t
+end
+
+local function expanded_path_from_list(pathlist) -- maybe not a list, just a path
+    -- a previous version fed back into pathlist
+    local newlist, ok = { }, false
+    for k=1,#pathlist do
+        if find(pathlist[k],"[{}]") then
+            ok = true
+            break
+        end
+    end
+    if ok then
+        local function validate(s)
+            s = file.collapse_path(s)
+            return s ~= "" and not find(s,dummy_path_expr) and s
+        end
+        for k=1,#pathlist do
+            splitpathexpr(pathlist[k],newlist,validate)
+        end
+    else
+        for k=1,#pathlist do
+            for p in gmatch(pathlist[k],"([^,]+)") do
+                p = file.collapse_path(p)
+                if p ~= "" then newlist[#newlist+1] = p end
+            end
+        end
+    end
+    return newlist
+end
+
+-- we follow a rather traditional approach:
+--
+-- (1) texmf.cnf given in TEXMFCNF
+-- (2) texmf.cnf searched in default variable
+--
+-- also we now follow the stupid route: if not set then just assume *one*
+-- cnf file under texmf (i.e. distribution)
+
+local args = environment and environment.original_arguments or arg -- this needs a cleanup
+
+resolvers.ownbin = resolvers.ownbin or args[-2] or arg[-2] or args[-1] or arg[-1] or arg[0] or "luatex"
+resolvers.ownbin = gsub(resolvers.ownbin,"\\","/")
+
+function resolvers.getownpath()
+    local ownpath = resolvers.ownpath or os.selfdir
+    if not ownpath or ownpath == "" or ownpath == "unset" then
+        ownpath = args[-1] or arg[-1]
+        ownpath = ownpath and file.dirname(gsub(ownpath,"\\","/"))
+        if not ownpath or ownpath == "" then
+            ownpath = args[-0] or arg[-0]
+            ownpath = ownpath and file.dirname(gsub(ownpath,"\\","/"))
+        end
+        local binary = resolvers.ownbin
+        if not ownpath or ownpath == "" then
+            ownpath = ownpath and file.dirname(binary)
+        end
+        if not ownpath or ownpath == "" then
+            if os.binsuffix ~= "" then
+                binary = file.replacesuffix(binary,os.binsuffix)
+            end
+            for p in gmatch(os.getenv("PATH"),"[^"..io.pathseparator.."]+") do
+                local b = file.join(p,binary)
+                if lfs.isfile(b) then
+                    -- we assume that after changing to the path the currentdir function
+                    -- resolves to the real location and use this side effect here; this
+                    -- trick is needed because on the mac installations use symlinks in the
+                    -- path instead of real locations
+                    local olddir = lfs.currentdir()
+                    if lfs.chdir(p) then
+                        local pp = lfs.currentdir()
+                        if trace_locating and p ~= pp then
+                            logs.report("fileio","following symlink '%s' to '%s'",p,pp)
+                        end
+                        ownpath = pp
+                        lfs.chdir(olddir)
+                    else
+                        if trace_locating then
+                            logs.report("fileio","unable to check path '%s'",p)
+                        end
+                        ownpath =  p
+                    end
+                    break
+                end
+            end
+        end
+        if not ownpath or ownpath == "" then
+            ownpath = "."
+            logs.report("fileio","forcing fallback ownpath .")
+        elseif trace_locating then
+            logs.report("fileio","using ownpath '%s'",ownpath)
+        end
+    end
+    resolvers.ownpath = ownpath
+    function resolvers.getownpath()
+        return resolvers.ownpath
+    end
+    return ownpath
+end
+
+local own_places = { "SELFAUTOLOC", "SELFAUTODIR", "SELFAUTOPARENT", "TEXMFCNF" }
+
+local function identify_own()
+    local ownpath = resolvers.getownpath() or dir.current()
+    local ie = instance.environment
+    if ownpath then
+        if resolvers.env('SELFAUTOLOC')    == "" then os.env['SELFAUTOLOC']    = file.collapse_path(ownpath) end
+        if resolvers.env('SELFAUTODIR')    == "" then os.env['SELFAUTODIR']    = file.collapse_path(ownpath .. "/..") end
+        if resolvers.env('SELFAUTOPARENT') == "" then os.env['SELFAUTOPARENT'] = file.collapse_path(ownpath .. "/../..") end
+    else
+        logs.report("fileio","error: unable to locate ownpath")
+        os.exit()
+    end
+    if resolvers.env('TEXMFCNF') == "" then os.env['TEXMFCNF'] = resolvers.cnfdefault end
+    if resolvers.env('TEXOS')    == "" then os.env['TEXOS']    = resolvers.env('SELFAUTODIR') end
+    if resolvers.env('TEXROOT')  == "" then os.env['TEXROOT']  = resolvers.env('SELFAUTOPARENT') end
+    if trace_locating then
+        for i=1,#own_places do
+            local v = own_places[i]
+            logs.report("fileio","variable '%s' set to '%s'",v,resolvers.env(v) or "unknown")
+        end
+    end
+    identify_own = function() end
+end
+
+function resolvers.identify_cnf()
+    if #instance.cnffiles == 0 then
+        -- fallback
+        identify_own()
+        -- the real search
+        resolvers.expand_variables()
+        local t = resolvers.split_path(resolvers.env('TEXMFCNF'))
+        t = expanded_path_from_list(t)
+        expand_vars(t) -- redundant
+        local function locate(filename,list)
+            for i=1,#t do
+                local ti = t[i]
+                local texmfcnf = file.collapse_path(file.join(ti,filename))
+                if lfs.isfile(texmfcnf) then
+                    list[#list+1] = texmfcnf
+                end
+            end
+        end
+        locate(resolvers.luaname,instance.luafiles)
+        locate(resolvers.cnfname,instance.cnffiles)
+    end
+end
+
+local function load_cnf_file(fname)
+    fname = resolvers.clean_path(fname)
+    local lname = file.replacesuffix(fname,'lua')
+    if lfs.isfile(lname) then
+        local dname = file.dirname(fname) -- fname ?
+        if not instance.configuration[dname] then
+            resolvers.load_data(dname,'configuration',lname and file.basename(lname))
+            instance.order[#instance.order+1] = instance.configuration[dname]
+        end
+    else
+        f = io.open(fname)
+        if f then
+            if trace_locating then
+                logs.report("fileio","loading configuration file %s", fname)
+            end
+            local line, data, n, k, v
+            local dname = file.dirname(fname)
+            if not instance.configuration[dname] then
+                instance.configuration[dname] = { }
+                instance.order[#instance.order+1] = instance.configuration[dname]
+            end
+            local data = instance.configuration[dname]
+            while true do
+                local line, n = f:read(), 0
+                if line then
+                    while true do -- join lines
+                        line, n = gsub(line,"\\%s*$", "")
+                        if n > 0 then
+                            line = line .. f:read()
+                        else
+                            break
+                        end
+                    end
+                    if not find(line,"^[%%#]") then
+                        local l = gsub(line,"%s*%%.*$","")
+                        local k, v = match(l,"%s*(.-)%s*=%s*(.-)%s*$")
+                        if k and v and not data[k] then
+                            v = gsub(v,"[%%#].*",'')
+                            data[k] = gsub(v,"~","$HOME")
+                            instance.kpsevars[k] = true
+                        end
+                    end
+                else
+                    break
+                end
+            end
+            f:close()
+        elseif trace_locating then
+            logs.report("fileio","skipping configuration file '%s'", fname)
+        end
+    end
+end
+
+local function collapse_cnf_data() -- potential optimization: pass start index (setup and configuration are shared)
+    local order = instance.order
+    for i=1,#order do
+        local c = order[i]
+        for k,v in next, c do
+            if not instance.variables[k] then
+                if instance.environment[k] then
+                    instance.variables[k] = instance.environment[k]
+                else
+                    instance.kpsevars[k] = true
+                    instance.variables[k] = resolvers.bare_variable(v)
+                end
+            end
+        end
+    end
+end
+
+function resolvers.load_cnf()
+    local function loadoldconfigdata()
+        local cnffiles = instance.cnffiles
+        for i=1,#cnffiles do
+            load_cnf_file(cnffiles[i])
+        end
+    end
+    -- instance.cnffiles contain complete names now !
+    -- we still use a funny mix of cnf and new but soon
+    -- we will switch to lua exclusively as we only use
+    -- the file to collect the tree roots
+    if #instance.cnffiles == 0 then
+        if trace_locating then
+            logs.report("fileio","no cnf files found (TEXMFCNF may not be set/known)")
+        end
+    else
+        local cnffiles = instance.cnffiles
+        instance.rootpath = cnffiles[1]
+        for k=1,#cnffiles do
+            instance.cnffiles[k] = file.collapse_path(cnffiles[k])
+        end
+        for i=1,3 do
+            instance.rootpath = file.dirname(instance.rootpath)
+        end
+        instance.rootpath = file.collapse_path(instance.rootpath)
+        if instance.diskcache and not instance.renewcache then
+            resolvers.loadoldconfig(instance.cnffiles)
+            if instance.loaderror then
+                loadoldconfigdata()
+                resolvers.saveoldconfig()
+            end
+        else
+            loadoldconfigdata()
+            if instance.renewcache then
+                resolvers.saveoldconfig()
+            end
+        end
+        collapse_cnf_data()
+    end
+    check_configuration()
+end
+
+function resolvers.load_lua()
+    if #instance.luafiles == 0 then
+        -- yet harmless
+    else
+        instance.rootpath = instance.luafiles[1]
+        local luafiles = instance.luafiles
+        for k=1,#luafiles do
+            instance.luafiles[k] = file.collapse_path(luafiles[k])
+        end
+        for i=1,3 do
+            instance.rootpath = file.dirname(instance.rootpath)
+        end
+        instance.rootpath = file.collapse_path(instance.rootpath)
+        resolvers.loadnewconfig()
+        collapse_cnf_data()
+    end
+    check_configuration()
+end
+
+-- database loading
+
+function resolvers.load_hash()
+    resolvers.locatelists()
+    if instance.diskcache and not instance.renewcache then
+        resolvers.loadfiles()
+        if instance.loaderror then
+            resolvers.loadlists()
+            resolvers.savefiles()
+        end
+    else
+        resolvers.loadlists()
+        if instance.renewcache then
+            resolvers.savefiles()
+        end
+    end
+end
+
+function resolvers.append_hash(type,tag,name)
+    if trace_locating then
+        logs.report("fileio","hash '%s' appended",tag)
+    end
+    insert(instance.hashes, { ['type']=type, ['tag']=tag, ['name']=name } )
+end
+
+function resolvers.prepend_hash(type,tag,name)
+    if trace_locating then
+        logs.report("fileio","hash '%s' prepended",tag)
+    end
+    insert(instance.hashes, 1, { ['type']=type, ['tag']=tag, ['name']=name } )
+end
+
+function resolvers.extend_texmf_var(specification) -- crap, we could better prepend the hash
+--  local t = resolvers.expanded_path_list('TEXMF') -- full expansion
+    local t = resolvers.split_path(resolvers.env('TEXMF'))
+    insert(t,1,specification)
+    local newspec = concat(t,";")
+    if instance.environment["TEXMF"] then
+        instance.environment["TEXMF"] = newspec
+    elseif instance.variables["TEXMF"] then
+        instance.variables["TEXMF"] = newspec
+    else
+        -- weird
+    end
+    resolvers.expand_variables()
+    reset_hashes()
+end
+
+-- locators
+
+function resolvers.locatelists()
+    local texmfpaths = resolvers.clean_path_list('TEXMF')
+    for i=1,#texmfpaths do
+        local path = texmfpaths[i]
+        if trace_locating then
+            logs.report("fileio","locating list of '%s'",path)
+        end
+        resolvers.locatedatabase(file.collapse_path(path))
+    end
+end
+
+function resolvers.locatedatabase(specification)
+    return resolvers.methodhandler('locators', specification)
+end
+
+function resolvers.locators.tex(specification)
+    if specification and specification ~= '' and lfs.isdir(specification) then
+        if trace_locating then
+            logs.report("fileio","tex locator '%s' found",specification)
+        end
+        resolvers.append_hash('file',specification,filename)
+    elseif trace_locating then
+        logs.report("fileio","tex locator '%s' not found",specification)
+    end
+end
+
+-- hashers
+
+function resolvers.hashdatabase(tag,name)
+    return resolvers.methodhandler('hashers',tag,name)
+end
+
+function resolvers.loadfiles()
+    instance.loaderror = false
+    instance.files = { }
+    if not instance.renewcache then
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            resolvers.hashdatabase(hash.tag,hash.name)
+            if instance.loaderror then break end
+        end
+    end
+end
+
+function resolvers.hashers.tex(tag,name)
+    resolvers.load_data(tag,'files')
+end
+
+-- generators:
+
+function resolvers.loadlists()
+    local hashes = instance.hashes
+    for i=1,#hashes do
+        resolvers.generatedatabase(hashes[i].tag)
+    end
+end
+
+function resolvers.generatedatabase(specification)
+    return resolvers.methodhandler('generators', specification)
+end
+
+-- starting with . or .. etc or funny char
+
+local weird = lpeg.P(".")^1 + lpeg.anywhere(lpeg.S("~`!#$%^&*()={}[]:;\"\'||<>,?\n\r\t"))
+
+--~ local l_forbidden = lpeg.S("~`!#$%^&*()={}[]:;\"\'||\\/<>,?\n\r\t")
+--~ local l_confusing = lpeg.P(" ")
+--~ local l_character = lpeg.patterns.utf8
+--~ local l_dangerous = lpeg.P(".")
+
+--~ local l_normal = (l_character - l_forbidden - l_confusing - l_dangerous) * (l_character - l_forbidden - l_confusing^2)^0 * lpeg.P(-1)
+--~ ----- l_normal = l_normal * lpeg.Cc(true) + lpeg.Cc(false)
+
+--~ local function test(str)
+--~     print(str,lpeg.match(l_normal,str))
+--~ end
+--~ test("ヒラギノ明朝 Pro W3")
+--~ test("..ヒラギノ明朝 Pro W3")
+--~ test(":ヒラギノ明朝 Pro W3;")
+--~ test("ヒラギノ明朝 /Pro W3;")
+--~ test("ヒラギノ明朝 Pro  W3")
+
+function resolvers.generators.tex(specification)
+    local tag = specification
+    if trace_locating then
+        logs.report("fileio","scanning path '%s'",specification)
+    end
+    instance.files[tag] = { }
+    local files = instance.files[tag]
+    local n, m, r = 0, 0, 0
+    local spec = specification .. '/'
+    local attributes = lfs.attributes
+    local directory = lfs.dir
+    local function action(path)
+        local full
+        if path then
+            full = spec .. path .. '/'
+        else
+            full = spec
+        end
+        for name in directory(full) do
+            if not lpegmatch(weird,name) then
+         -- if lpegmatch(l_normal,name) then
+                local mode = attributes(full..name,'mode')
+                if mode == 'file' then
+                    if path then
+                        n = n + 1
+                        local f = files[name]
+                        if f then
+                            if type(f) == 'string' then
+                                files[name] = { f, path }
+                            else
+                                f[#f+1] = path
+                            end
+                        else -- probably unique anyway
+                            files[name] = path
+                            local lower = lower(name)
+                            if name ~= lower then
+                                files["remap:"..lower] = name
+                                r = r + 1
+                            end
+                        end
+                    end
+                elseif mode == 'directory' then
+                    m = m + 1
+                    if path then
+                        action(path..'/'..name)
+                    else
+                        action(name)
+                    end
+                end
+            end
+        end
+    end
+    action()
+    if trace_locating then
+        logs.report("fileio","%s files found on %s directories with %s uppercase remappings",n,m,r)
+    end
+end
+
+-- savers, todo
+
+function resolvers.savefiles()
+    resolvers.save_data('files')
+end
+
+-- A config (optionally) has the paths split in tables. Internally
+-- we join them and split them after the expansion has taken place. This
+-- is more convenient.
+
+--~ local checkedsplit = string.checkedsplit
+
+local cache = { }
+
+local splitter = lpeg.Ct(lpeg.splitat(lpeg.S(os.type == "windows" and ";" or ":;")))
+
+local function split_kpse_path(str) -- beware, this can be either a path or a {specification}
+    local found = cache[str]
+    if not found then
+        if str == "" then
+            found = { }
+        else
+            str = gsub(str,"\\","/")
+--~             local split = (find(str,";") and checkedsplit(str,";")) or checkedsplit(str,io.pathseparator)
+local split = lpegmatch(splitter,str)
+            found = { }
+            for i=1,#split do
+                local s = split[i]
+                if not find(s,"^{*unset}*") then
+                    found[#found+1] = s
+                end
+            end
+            if trace_expansions then
+                logs.report("fileio","splitting path specification '%s'",str)
+                for k=1,#found do
+                    logs.report("fileio","% 4i: %s",k,found[k])
+                end
+            end
+            cache[str] = found
+        end
+    end
+    return found
+end
+
+resolvers.split_kpse_path = split_kpse_path
+
+function resolvers.splitconfig()
+    for i=1,#instance do
+        local c = instance[i]
+        for k,v in next, c do
+            if type(v) == 'string' then
+                local t = split_kpse_path(v)
+                if #t > 1 then
+                    c[k] = t
+                end
+            end
+        end
+    end
+end
+
+function resolvers.joinconfig()
+    local order = instance.order
+    for i=1,#order do
+        local c = order[i]
+        for k,v in next, c do -- indexed?
+            if type(v) == 'table' then
+                c[k] = file.join_path(v)
+            end
+        end
+    end
+end
+
+function resolvers.split_path(str)
+    if type(str) == 'table' then
+        return str
+    else
+        return split_kpse_path(str)
+    end
+end
+
+function resolvers.join_path(str)
+    if type(str) == 'table' then
+        return file.join_path(str)
+    else
+        return str
+    end
+end
+
+function resolvers.splitexpansions()
+    local ie = instance.expansions
+    for k,v in next, ie do
+        local t, h, p = { }, { }, split_kpse_path(v)
+        for kk=1,#p do
+            local vv = p[kk]
+            if vv ~= "" and not h[vv] then
+                t[#t+1] = vv
+                h[vv] = true
+            end
+        end
+        if #t > 1 then
+            ie[k] = t
+        else
+            ie[k] = t[1]
+        end
+    end
+end
+
+-- end of split/join code
+
+function resolvers.saveoldconfig()
+    resolvers.splitconfig()
+    resolvers.save_data('configuration')
+    resolvers.joinconfig()
+end
+
+resolvers.configbanner = [[
+-- This is a Luatex configuration file created by 'luatools.lua' or
+-- 'luatex.exe' directly. For comment, suggestions and questions you can
+-- contact the ConTeXt Development Team. This configuration file is
+-- not copyrighted. [HH & TH]
+]]
+
+function resolvers.serialize(files)
+    -- This version is somewhat optimized for the kind of
+    -- tables that we deal with, so it's much faster than
+    -- the generic serializer. This makes sense because
+    -- luatools and mtxtools are called frequently. Okay,
+    -- we pay a small price for properly tabbed tables.
+    local t = { }
+    local function dump(k,v,m) -- could be moved inline
+        if type(v) == 'string' then
+            return m .. "['" .. k .. "']='" .. v .. "',"
+        elseif #v == 1 then
+            return m .. "['" .. k .. "']='" .. v[1] .. "',"
+        else
+            return m .. "['" .. k .. "']={'" .. concat(v,"','").. "'},"
+        end
+    end
+    t[#t+1] = "return {"
+    if instance.sortdata then
+	local sortedfiles = sortedkeys(files)
+	for i=1,#sortedfiles do
+	    local k = sortedfiles[i]
+            local fk  = files[k]
+            if type(fk) == 'table' then
+                t[#t+1] = "\t['" .. k .. "']={"
+		local sortedfk = sortedkeys(fk)
+        	for j=1,#sortedfk do
+                    local kk = sortedfk[j]
+                    t[#t+1] = dump(kk,fk[kk],"\t\t")
+                end
+                t[#t+1] = "\t},"
+            else
+                t[#t+1] = dump(k,fk,"\t")
+            end
+        end
+    else
+        for k, v in next, files do
+            if type(v) == 'table' then
+                t[#t+1] = "\t['" .. k .. "']={"
+                for kk,vv in next, v do
+                    t[#t+1] = dump(kk,vv,"\t\t")
+                end
+                t[#t+1] = "\t},"
+            else
+                t[#t+1] = dump(k,v,"\t")
+            end
+        end
+    end
+    t[#t+1] = "}"
+    return concat(t,"\n")
+end
+
+local data_state = { }
+
+function resolvers.data_state()
+    return data_state or { }
+end
+
+function resolvers.save_data(dataname, makename) -- untested without cache overload
+    for cachename, files in next, instance[dataname] do
+        local name = (makename or file.join)(cachename,dataname)
+        local luaname, lucname = name .. ".lua", name .. ".luc"
+        if trace_locating then
+            logs.report("fileio","preparing '%s' for '%s'",dataname,cachename)
+        end
+        for k, v in next, files do
+            if type(v) == "table" and #v == 1 then
+                files[k] = v[1]
+            end
+        end
+        local data = {
+            type    = dataname,
+            root    = cachename,
+            version = resolvers.cacheversion,
+            date    = os.date("%Y-%m-%d"),
+            time    = os.date("%H:%M:%S"),
+            content = files,
+            uuid    = os.uuid(),
+        }
+        local ok = io.savedata(luaname,resolvers.serialize(data))
+        if ok then
+            if trace_locating then
+                logs.report("fileio","'%s' saved in '%s'",dataname,luaname)
+            end
+            if utils.lua.compile(luaname,lucname,false,true) then -- no cleanup but strip
+                if trace_locating then
+                    logs.report("fileio","'%s' compiled to '%s'",dataname,lucname)
+                end
+            else
+                if trace_locating then
+                    logs.report("fileio","compiling failed for '%s', deleting file '%s'",dataname,lucname)
+                end
+                os.remove(lucname)
+            end
+        elseif trace_locating then
+            logs.report("fileio","unable to save '%s' in '%s' (access error)",dataname,luaname)
+        end
+    end
+end
+
+function resolvers.load_data(pathname,dataname,filename,makename) -- untested without cache overload
+    filename = ((not filename or (filename == "")) and dataname) or filename
+    filename = (makename and makename(dataname,filename)) or file.join(pathname,filename)
+    local blob = loadfile(filename .. ".luc") or loadfile(filename .. ".lua")
+    if blob then
+        local data = blob()
+        if data and data.content and data.type == dataname and data.version == resolvers.cacheversion then
+            data_state[#data_state+1] = data.uuid
+            if trace_locating then
+                logs.report("fileio","loading '%s' for '%s' from '%s'",dataname,pathname,filename)
+            end
+            instance[dataname][pathname] = data.content
+        else
+            if trace_locating then
+                logs.report("fileio","skipping '%s' for '%s' from '%s'",dataname,pathname,filename)
+            end
+            instance[dataname][pathname] = { }
+            instance.loaderror = true
+        end
+    elseif trace_locating then
+        logs.report("fileio","skipping '%s' for '%s' from '%s'",dataname,pathname,filename)
+    end
+end
+
+-- some day i'll use the nested approach, but not yet (actually we even drop
+-- engine/progname support since we have only luatex now)
+--
+-- first texmfcnf.lua files are located, next the cached texmf.cnf files
+--
+-- return {
+--     TEXMFBOGUS = 'effe checken of dit werkt',
+-- }
+
+function resolvers.resetconfig()
+    identify_own()
+    instance.configuration, instance.setup, instance.order, instance.loaderror = { }, { }, { }, false
+end
+
+function resolvers.loadnewconfig()
+    local luafiles = instance.luafiles
+    for i=1,#luafiles do
+        local cnf = luafiles[i]
+        local pathname = file.dirname(cnf)
+        local filename = file.join(pathname,resolvers.luaname)
+        local blob = loadfile(filename)
+        if blob then
+            local data = blob()
+            if data then
+                if trace_locating then
+                    logs.report("fileio","loading configuration file '%s'",filename)
+                end
+                if true then
+                    -- flatten to variable.progname
+                    local t = { }
+                    for k, v in next, data do -- v = progname
+                        if type(v) == "string" then
+                            t[k] = v
+                        else
+                            for kk, vv in next, v do -- vv = variable
+                                if type(vv) == "string" then
+                                    t[vv.."."..v] = kk
+                                end
+                            end
+                        end
+                    end
+                    instance['setup'][pathname] = t
+                else
+                    instance['setup'][pathname] = data
+                end
+            else
+                if trace_locating then
+                    logs.report("fileio","skipping configuration file '%s'",filename)
+                end
+                instance['setup'][pathname] = { }
+                instance.loaderror = true
+            end
+        elseif trace_locating then
+            logs.report("fileio","skipping configuration file '%s'",filename)
+        end
+        instance.order[#instance.order+1] = instance.setup[pathname]
+        if instance.loaderror then break end
+    end
+end
+
+function resolvers.loadoldconfig()
+    if not instance.renewcache then
+        local cnffiles = instance.cnffiles
+        for i=1,#cnffiles do
+            local cnf = cnffiles[i]
+            local dname = file.dirname(cnf)
+            resolvers.load_data(dname,'configuration')
+            instance.order[#instance.order+1] = instance.configuration[dname]
+            if instance.loaderror then break end
+        end
+    end
+    resolvers.joinconfig()
+end
+
+function resolvers.expand_variables()
+    local expansions, environment, variables = { }, instance.environment, instance.variables
+    local env = resolvers.env
+    instance.expansions = expansions
+    if instance.engine   ~= "" then environment['engine']   = instance.engine   end
+    if instance.progname ~= "" then environment['progname'] = instance.progname end
+    for k,v in next, environment do
+        local a, b = match(k,"^(%a+)%_(.*)%s*$")
+        if a and b then
+            expansions[a..'.'..b] = v
+        else
+            expansions[k] = v
+        end
+    end
+    for k,v in next, environment do -- move environment to expansions
+        if not expansions[k] then expansions[k] = v end
+    end
+    for k,v in next, variables do -- move variables to expansions
+        if not expansions[k] then expansions[k] = v end
+    end
+    local busy = false
+    local function resolve(a)
+        busy = true
+        return expansions[a] or env(a)
+    end
+    while true do
+        busy = false
+        for k,v in next, expansions do
+            local s, n = gsub(v,"%$([%a%d%_%-]+)",resolve)
+            local s, m = gsub(s,"%$%{([%a%d%_%-]+)%}",resolve)
+            if n > 0 or m > 0 then
+                expansions[k]= s
+            end
+        end
+        if not busy then break end
+    end
+    for k,v in next, expansions do
+        expansions[k] = gsub(v,"\\", '/')
+    end
+end
+
+function resolvers.variable(name)
+    return entry(instance.variables,name)
+end
+
+function resolvers.expansion(name)
+    return entry(instance.expansions,name)
+end
+
+function resolvers.is_variable(name)
+    return is_entry(instance.variables,name)
+end
+
+function resolvers.is_expansion(name)
+    return is_entry(instance.expansions,name)
+end
+
+function resolvers.unexpanded_path_list(str)
+    local pth = resolvers.variable(str)
+    local lst = resolvers.split_path(pth)
+    return expanded_path_from_list(lst)
+end
+
+function resolvers.unexpanded_path(str)
+    return file.join_path(resolvers.unexpanded_path_list(str))
+end
+
+do -- no longer needed
+
+    local done = { }
+
+    function resolvers.reset_extra_path()
+        local ep = instance.extra_paths
+        if not ep then
+            ep, done = { }, { }
+            instance.extra_paths = ep
+        elseif #ep > 0 then
+            instance.lists, done = { }, { }
+        end
+    end
+
+    function resolvers.register_extra_path(paths,subpaths)
+        local ep = instance.extra_paths or { }
+        local n = #ep
+        if paths and paths ~= "" then
+            if subpaths and subpaths ~= "" then
+                for p in gmatch(paths,"[^,]+") do
+                    -- we gmatch each step again, not that fast, but used seldom
+                    for s in gmatch(subpaths,"[^,]+") do
+                        local ps = p .. "/" .. s
+                        if not done[ps] then
+                            ep[#ep+1] = resolvers.clean_path(ps)
+                            done[ps] = true
+                        end
+                    end
+                end
+            else
+                for p in gmatch(paths,"[^,]+") do
+                    if not done[p] then
+                        ep[#ep+1] = resolvers.clean_path(p)
+                        done[p] = true
+                    end
+                end
+            end
+        elseif subpaths and subpaths ~= "" then
+            for i=1,n do
+                -- we gmatch each step again, not that fast, but used seldom
+                for s in gmatch(subpaths,"[^,]+") do
+                    local ps = ep[i] .. "/" .. s
+                    if not done[ps] then
+                        ep[#ep+1] = resolvers.clean_path(ps)
+                        done[ps] = true
+                    end
+                end
+            end
+        end
+        if #ep > 0 then
+            instance.extra_paths = ep -- register paths
+        end
+        if #ep > n then
+            instance.lists = { } -- erase the cache
+        end
+    end
+
+end
+
+local function made_list(instance,list)
+    local ep = instance.extra_paths
+    if not ep or #ep == 0 then
+        return list
+    else
+        local done, new = { }, { }
+        -- honour . .. ../.. but only when at the start
+        for k=1,#list do
+            local v = list[k]
+            if not done[v] then
+                if find(v,"^[%.%/]$") then
+                    done[v] = true
+                    new[#new+1] = v
+                else
+                    break
+                end
+            end
+        end
+        -- first the extra paths
+        for k=1,#ep do
+            local v = ep[k]
+            if not done[v] then
+                done[v] = true
+                new[#new+1] = v
+            end
+        end
+        -- next the formal paths
+        for k=1,#list do
+            local v = list[k]
+            if not done[v] then
+                done[v] = true
+                new[#new+1] = v
+            end
+        end
+        return new
+    end
+end
+
+function resolvers.clean_path_list(str)
+    local t = resolvers.expanded_path_list(str)
+    if t then
+        for i=1,#t do
+            t[i] = file.collapse_path(resolvers.clean_path(t[i]))
+        end
+    end
+    return t
+end
+
+function resolvers.expand_path(str)
+    return file.join_path(resolvers.expanded_path_list(str))
+end
+
+function resolvers.expanded_path_list(str)
+    if not str then
+        return ep or { } -- ep ?
+    elseif instance.savelists then
+        -- engine+progname hash
+        str = gsub(str,"%$","")
+        if not instance.lists[str] then -- cached
+            local lst = made_list(instance,resolvers.split_path(resolvers.expansion(str)))
+            instance.lists[str] = expanded_path_from_list(lst)
+        end
+        return instance.lists[str]
+    else
+        local lst = resolvers.split_path(resolvers.expansion(str))
+        return made_list(instance,expanded_path_from_list(lst))
+    end
+end
+
+function resolvers.expanded_path_list_from_var(str) -- brrr
+    local tmp = resolvers.var_of_format_or_suffix(gsub(str,"%$",""))
+    if tmp ~= "" then
+        return resolvers.expanded_path_list(tmp)
+    else
+        return resolvers.expanded_path_list(str)
+    end
+end
+
+function resolvers.expand_path_from_var(str)
+    return file.join_path(resolvers.expanded_path_list_from_var(str))
+end
+
+function resolvers.format_of_var(str)
+    return formats[str] or formats[alternatives[str]] or ''
+end
+function resolvers.format_of_suffix(str)
+    return suffixmap[file.extname(str)] or 'tex'
+end
+
+function resolvers.variable_of_format(str)
+    return formats[str] or formats[alternatives[str]] or ''
+end
+
+function resolvers.var_of_format_or_suffix(str)
+    local v = formats[str]
+    if v then
+        return v
+    end
+    v = formats[alternatives[str]]
+    if v then
+        return v
+    end
+    v = suffixmap[file.extname(str)]
+    if v then
+        return formats[isf]
+    end
+    return ''
+end
+
+function resolvers.expand_braces(str) -- output variable and brace expansion of STRING
+    local ori = resolvers.variable(str)
+    local pth = expanded_path_from_list(resolvers.split_path(ori))
+    return file.join_path(pth)
+end
+
+resolvers.isreadable = { }
+
+function resolvers.isreadable.file(name)
+    local readable = lfs.isfile(name) -- brrr
+    if trace_detail then
+        if readable then
+            logs.report("fileio","file '%s' is readable",name)
+        else
+            logs.report("fileio","file '%s' is not readable", name)
+        end
+    end
+    return readable
+end
+
+resolvers.isreadable.tex = resolvers.isreadable.file
+
+-- name
+-- name/name
+
+local function collect_files(names)
+    local filelist = { }
+    for k=1,#names do
+        local fname = names[k]
+        if trace_detail then
+            logs.report("fileio","checking name '%s'",fname)
+        end
+        local bname = file.basename(fname)
+        local dname = file.dirname(fname)
+        if dname == "" or find(dname,"^%.") then
+            dname = false
+        else
+            dname = "/" .. dname .. "$"
+        end
+        local hashes = instance.hashes
+        for h=1,#hashes do
+            local hash = hashes[h]
+            local blobpath = hash.tag
+            local files = blobpath and instance.files[blobpath]
+            if files then
+                if trace_detail then
+                    logs.report("fileio","deep checking '%s' (%s)",blobpath,bname)
+                end
+                local blobfile = files[bname]
+                if not blobfile then
+                    local rname = "remap:"..bname
+                    blobfile = files[rname]
+                    if blobfile then
+                        bname = files[rname]
+                        blobfile = files[bname]
+                    end
+                end
+                if blobfile then
+                    if type(blobfile) == 'string' then
+                        if not dname or find(blobfile,dname) then
+                            filelist[#filelist+1] = {
+                                hash.type,
+                                file.join(blobpath,blobfile,bname), -- search
+                                resolvers.concatinators[hash.type](blobpath,blobfile,bname) -- result
+                            }
+                        end
+                    else
+                        for kk=1,#blobfile do
+                            local vv = blobfile[kk]
+                            if not dname or find(vv,dname) then
+                                filelist[#filelist+1] = {
+                                    hash.type,
+                                    file.join(blobpath,vv,bname), -- search
+                                    resolvers.concatinators[hash.type](blobpath,vv,bname) -- result
+                                }
+                            end
+                        end
+                    end
+                end
+            elseif trace_locating then
+                logs.report("fileio","no match in '%s' (%s)",blobpath,bname)
+            end
+        end
+    end
+    if #filelist > 0 then
+        return filelist
+    else
+        return nil
+    end
+end
+
+function resolvers.suffix_of_format(str)
+    if suffixes[str] then
+        return suffixes[str][1]
+    else
+        return ""
+    end
+end
+
+function resolvers.suffixes_of_format(str)
+    if suffixes[str] then
+        return suffixes[str]
+    else
+        return {}
+    end
+end
+
+function resolvers.register_in_trees(name)
+    if not find(name,"^%.") then
+        instance.foundintrees[name] = (instance.foundintrees[name] or 0) + 1 -- maybe only one
+    end
+end
+
+-- split the next one up for readability (bu this module needs a cleanup anyway)
+
+local function can_be_dir(name) -- can become local
+    local fakepaths = instance.fakepaths
+    if not fakepaths[name] then
+        if lfs.isdir(name) then
+            fakepaths[name] = 1 -- directory
+        else
+            fakepaths[name] = 2 -- no directory
+        end
+    end
+    return (fakepaths[name] == 1)
+end
+
+local function collect_instance_files(filename,collected) -- todo : plugin (scanners, checkers etc)
+    local result = collected or { }
+    local stamp  = nil
+    filename = file.collapse_path(filename)
+    -- speed up / beware: format problem
+    if instance.remember then
+        stamp = filename .. "--" .. instance.engine .. "--" .. instance.progname .. "--" .. instance.format
+        if instance.found[stamp] then
+            if trace_locating then
+                logs.report("fileio","remembering file '%s'",filename)
+            end
+            return instance.found[stamp]
+        end
+    end
+    if not dangerous[instance.format or "?"] then
+        if resolvers.isreadable.file(filename) then
+            if trace_detail then
+                logs.report("fileio","file '%s' found directly",filename)
+            end
+            instance.found[stamp] = { filename }
+            return { filename }
+        end
+    end
+    if find(filename,'%*') then
+        if trace_locating then
+            logs.report("fileio","checking wildcard '%s'", filename)
+        end
+        result = resolvers.find_wildcard_files(filename)
+    elseif file.is_qualified_path(filename) then
+        if resolvers.isreadable.file(filename) then
+            if trace_locating then
+                logs.report("fileio","qualified name '%s'", filename)
+            end
+            result = { filename }
+        else
+            local forcedname, ok, suffix = "", false, file.extname(filename)
+            if suffix == "" then -- why
+                if instance.format == "" then
+                    forcedname = filename .. ".tex"
+                    if resolvers.isreadable.file(forcedname) then
+                        if trace_locating then
+                            logs.report("fileio","no suffix, forcing standard filetype 'tex'")
+                        end
+                        result, ok = { forcedname }, true
+                    end
+                else
+                    local suffixes = resolvers.suffixes_of_format(instance.format)
+                    for _, s in next, suffixes do
+                        forcedname = filename .. "." .. s
+                        if resolvers.isreadable.file(forcedname) then
+                            if trace_locating then
+                                logs.report("fileio","no suffix, forcing format filetype '%s'", s)
+                            end
+                            result, ok = { forcedname }, true
+                            break
+                        end
+                    end
+                end
+            end
+            if not ok and suffix ~= "" then
+                -- try to find in tree (no suffix manipulation), here we search for the
+                -- matching last part of the name
+                local basename = file.basename(filename)
+                local pattern = gsub(filename .. "$","([%.%-])","%%%1")
+                local savedformat = instance.format
+                local format = savedformat or ""
+                if format == "" then
+                    instance.format = resolvers.format_of_suffix(suffix)
+                end
+                if not format then
+                    instance.format = "othertextfiles" -- kind of everything, maybe texinput is better
+                end
+                --
+                if basename ~= filename then
+                    local resolved = collect_instance_files(basename)
+                    if #result == 0 then
+                        local lowered = lower(basename)
+                        if filename ~= lowered then
+                            resolved = collect_instance_files(lowered)
+                        end
+                    end
+                    resolvers.format = savedformat
+                    --
+                    for r=1,#resolved do
+                        local rr = resolved[r]
+                        if find(rr,pattern) then
+                            result[#result+1], ok = rr, true
+                        end
+                    end
+                end
+                -- a real wildcard:
+                --
+                -- if not ok then
+                --     local filelist = collect_files({basename})
+                --     for f=1,#filelist do
+                --         local ff = filelist[f][3] or ""
+                --         if find(ff,pattern) then
+                --             result[#result+1], ok = ff, true
+                --         end
+                --     end
+                -- end
+            end
+            if not ok and trace_locating then
+                logs.report("fileio","qualified name '%s'", filename)
+            end
+        end
+    else
+        -- search spec
+        local filetype, extra, done, wantedfiles, ext = '', nil, false, { }, file.extname(filename)
+        if ext == "" then
+            if not instance.force_suffixes then
+                wantedfiles[#wantedfiles+1] = filename
+            end
+        else
+            wantedfiles[#wantedfiles+1] = filename
+        end
+        if instance.format == "" then
+            if ext == "" then
+                local forcedname = filename .. '.tex'
+                wantedfiles[#wantedfiles+1] = forcedname
+                filetype = resolvers.format_of_suffix(forcedname)
+                if trace_locating then
+                    logs.report("fileio","forcing filetype '%s'",filetype)
+                end
+            else
+                filetype = resolvers.format_of_suffix(filename)
+                if trace_locating then
+                    logs.report("fileio","using suffix based filetype '%s'",filetype)
+                end
+            end
+        else
+            if ext == "" then
+                local suffixes = resolvers.suffixes_of_format(instance.format)
+                for _, s in next, suffixes do
+                    wantedfiles[#wantedfiles+1] = filename .. "." .. s
+                end
+            end
+            filetype = instance.format
+            if trace_locating then
+                logs.report("fileio","using given filetype '%s'",filetype)
+            end
+        end
+        local typespec = resolvers.variable_of_format(filetype)
+        local pathlist = resolvers.expanded_path_list(typespec)
+        if not pathlist or #pathlist == 0 then
+            -- no pathlist, access check only / todo == wildcard
+            if trace_detail then
+                logs.report("fileio","checking filename '%s', filetype '%s', wanted files '%s'",filename, filetype or '?',concat(wantedfiles," | "))
+            end
+            for k=1,#wantedfiles do
+                local fname = wantedfiles[k]
+                if fname and resolvers.isreadable.file(fname) then
+                    filename, done = fname, true
+                    result[#result+1] = file.join('.',fname)
+                    break
+                end
+            end
+            -- this is actually 'other text files' or 'any' or 'whatever'
+            local filelist = collect_files(wantedfiles)
+            local fl = filelist and filelist[1]
+            if fl then
+                filename = fl[3]
+                result[#result+1] = filename
+                done = true
+            end
+        else
+            -- list search
+            local filelist = collect_files(wantedfiles)
+            local dirlist = { }
+            if filelist then
+                for i=1,#filelist do
+                    dirlist[i] = file.dirname(filelist[i][2]) .. "/"
+                end
+            end
+            if trace_detail then
+                logs.report("fileio","checking filename '%s'",filename)
+            end
+            -- a bit messy ... esp the doscan setting here
+            local doscan
+            for k=1,#pathlist do
+                local path = pathlist[k]
+                if find(path,"^!!") then doscan  = false else doscan  = true  end
+                local pathname = gsub(path,"^!+", '')
+                done = false
+                -- using file list
+                if filelist then
+                    local expression
+                    -- compare list entries with permitted pattern -- /xx /xx//
+                    if not find(pathname,"/$") then
+                        expression = pathname .. "/"
+                    else
+                        expression = pathname
+                    end
+                    expression = gsub(expression,"([%-%.])","%%%1") -- this also influences
+                    expression = gsub(expression,"//+$", '/.*')     -- later usage of pathname
+                    expression = gsub(expression,"//", '/.-/')      -- not ok for /// but harmless
+                    expression = "^" .. expression .. "$"
+                    if trace_detail then
+                        logs.report("fileio","using pattern '%s' for path '%s'",expression,pathname)
+                    end
+                    for k=1,#filelist do
+                        local fl = filelist[k]
+                        local f = fl[2]
+                        local d = dirlist[k]
+                        if find(d,expression) then
+                            --- todo, test for readable
+                            result[#result+1] = fl[3]
+                            resolvers.register_in_trees(f) -- for tracing used files
+                            done = true
+                            if instance.allresults then
+                                if trace_detail then
+                                    logs.report("fileio","match in hash for file '%s' on path '%s', continue scanning",f,d)
+                                end
+                            else
+                                if trace_detail then
+                                    logs.report("fileio","match in hash for file '%s' on path '%s', quit scanning",f,d)
+                                end
+                                break
+                            end
+                        elseif trace_detail then
+                            logs.report("fileio","no match in hash for file '%s' on path '%s'",f,d)
+                        end
+                    end
+                end
+                if not done and doscan then
+                    -- check if on disk / unchecked / does not work at all / also zips
+                    if resolvers.splitmethod(pathname).scheme == 'file' then -- ?
+                        local pname = gsub(pathname,"%.%*$",'')
+                        if not find(pname,"%*") then
+                            local ppname = gsub(pname,"/+$","")
+                            if can_be_dir(ppname) then
+                                for k=1,#wantedfiles do
+                                    local w = wantedfiles[k]
+                                    local fname = file.join(ppname,w)
+                                    if resolvers.isreadable.file(fname) then
+                                        if trace_detail then
+                                            logs.report("fileio","found '%s' by scanning",fname)
+                                        end
+                                        result[#result+1] = fname
+                                        done = true
+                                        if not instance.allresults then break end
+                                    end
+                                end
+                            else
+                                -- no access needed for non existing path, speedup (esp in large tree with lots of fake)
+                            end
+                        end
+                    end
+                end
+                if not done and doscan then
+                    -- todo: slow path scanning
+                end
+                if done and not instance.allresults then break end
+            end
+        end
+    end
+    for k=1,#result do
+        result[k] = file.collapse_path(result[k])
+    end
+    if instance.remember then
+        instance.found[stamp] = result
+    end
+    return result
+end
+
+if not resolvers.concatinators  then resolvers.concatinators = { } end
+
+resolvers.concatinators.tex  = file.join
+resolvers.concatinators.file = resolvers.concatinators.tex
+
+function resolvers.find_files(filename,filetype,mustexist)
+    if type(mustexist) == boolean then
+        -- all set
+    elseif type(filetype) == 'boolean' then
+        filetype, mustexist = nil, false
+    elseif type(filetype) ~= 'string' then
+        filetype, mustexist = nil, false
+    end
+    instance.format = filetype or ''
+    local result = collect_instance_files(filename)
+    if #result == 0 then
+        local lowered = lower(filename)
+        if filename ~= lowered then
+            return collect_instance_files(lowered)
+        end
+    end
+    instance.format = ''
+    return result
+end
+
+function resolvers.find_file(filename,filetype,mustexist)
+    return (resolvers.find_files(filename,filetype,mustexist)[1] or "")
+end
+
+function resolvers.find_given_files(filename)
+    local bname, result = file.basename(filename), { }
+    local hashes = instance.hashes
+    for k=1,#hashes do
+        local hash = hashes[k]
+        local files = instance.files[hash.tag] or { }
+        local blist = files[bname]
+        if not blist then
+            local rname = "remap:"..bname
+            blist = files[rname]
+            if blist then
+                bname = files[rname]
+                blist = files[bname]
+            end
+        end
+        if blist then
+            if type(blist) == 'string' then
+                result[#result+1] = resolvers.concatinators[hash.type](hash.tag,blist,bname) or ""
+                if not instance.allresults then break end
+            else
+                for kk=1,#blist do
+                    local vv = blist[kk]
+                    result[#result+1] = resolvers.concatinators[hash.type](hash.tag,vv,bname) or ""
+                    if not instance.allresults then break end
+                end
+            end
+        end
+    end
+    return result
+end
+
+function resolvers.find_given_file(filename)
+    return (resolvers.find_given_files(filename)[1] or "")
+end
+
+local function doit(path,blist,bname,tag,kind,result,allresults)
+    local done = false
+    if blist and kind then
+        if type(blist) == 'string' then
+            -- make function and share code
+            if find(lower(blist),path) then
+                result[#result+1] = resolvers.concatinators[kind](tag,blist,bname) or ""
+                done = true
+            end
+        else
+            for kk=1,#blist do
+                local vv = blist[kk]
+                if find(lower(vv),path) then
+                    result[#result+1] = resolvers.concatinators[kind](tag,vv,bname) or ""
+                    done = true
+                    if not allresults then break end
+                end
+            end
+        end
+    end
+    return done
+end
+
+function resolvers.find_wildcard_files(filename) -- todo: remap:
+    local result = { }
+    local bname, dname = file.basename(filename), file.dirname(filename)
+    local path = gsub(dname,"^*/","")
+    path = gsub(path,"*",".*")
+    path = gsub(path,"-","%%-")
+    if dname == "" then
+        path = ".*"
+    end
+    local name = bname
+    name = gsub(name,"*",".*")
+    name = gsub(name,"-","%%-")
+    path = lower(path)
+    name = lower(name)
+    local files, allresults, done = instance.files, instance.allresults, false
+    if find(name,"%*") then
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            local tag, kind = hash.tag, hash.type
+            for kk, hh in next, files[hash.tag] do
+                if not find(kk,"^remap:") then
+                    if find(lower(kk),name) then
+                        if doit(path,hh,kk,tag,kind,result,allresults) then done = true end
+                        if done and not allresults then break end
+                    end
+                end
+            end
+        end
+    else
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            local tag, kind = hash.tag, hash.type
+            if doit(path,files[tag][bname],bname,tag,kind,result,allresults) then done = true end
+            if done and not allresults then break end
+        end
+    end
+    -- we can consider also searching the paths not in the database, but then
+    -- we end up with a messy search (all // in all path specs)
+    return result
+end
+
+function resolvers.find_wildcard_file(filename)
+    return (resolvers.find_wildcard_files(filename)[1] or "")
+end
+
+-- main user functions
+
+function resolvers.automount()
+    -- implemented later
+end
+
+function resolvers.load(option)
+    statistics.starttiming(instance)
+    resolvers.resetconfig()
+    resolvers.identify_cnf()
+    resolvers.load_lua() -- will become the new method
+    resolvers.expand_variables()
+    resolvers.load_cnf() -- will be skipped when we have a lua file
+    resolvers.expand_variables()
+    if option ~= "nofiles" then
+        resolvers.load_hash()
+        resolvers.automount()
+    end
+    statistics.stoptiming(instance)
+end
+
+function resolvers.for_files(command, files, filetype, mustexist)
+    if files and #files > 0 then
+        local function report(str)
+            if trace_locating then
+                logs.report("fileio",str) -- has already verbose
+            else
+                print(str)
+            end
+        end
+        if trace_locating then
+            report('') -- ?
+        end
+        for f=1,#files do
+            local file = files[f]
+            local result = command(file,filetype,mustexist)
+            if type(result) == 'string' then
+                report(result)
+            else
+                for i=1,#result do
+                    report(result[i]) -- could be unpack
+                end
+            end
+        end
+    end
+end
+
+-- strtab
+
+resolvers.var_value  = resolvers.variable   -- output the value of variable $STRING.
+resolvers.expand_var = resolvers.expansion  -- output variable expansion of STRING.
+
+function resolvers.show_path(str)     -- output search path for file type NAME
+    return file.join_path(resolvers.expanded_path_list(resolvers.format_of_var(str)))
+end
+
+-- resolvers.find_file(filename)
+-- resolvers.find_file(filename, filetype, mustexist)
+-- resolvers.find_file(filename, mustexist)
+-- resolvers.find_file(filename, filetype)
+
+function resolvers.register_file(files, name, path)
+    if files[name] then
+        if type(files[name]) == 'string' then
+            files[name] = { files[name], path }
+        else
+            files[name] = path
+        end
+    else
+        files[name] = path
+    end
+end
+
+function resolvers.splitmethod(filename)
+    if not filename then
+        return { } -- safeguard
+    elseif type(filename) == "table" then
+        return filename -- already split
+    elseif not find(filename,"://") then
+        return { scheme="file", path = filename, original=filename } -- quick hack
+    else
+        return url.hashed(filename)
+    end
+end
+
+function table.sequenced(t,sep) -- temp here
+    local s = { }
+    for k, v in next, t do -- indexed?
+        s[#s+1] = k .. "=" .. tostring(v)
+    end
+    return concat(s, sep or " | ")
+end
+
+function resolvers.methodhandler(what, filename, filetype) -- ...
+    filename = file.collapse_path(filename)
+    local specification = (type(filename) == "string" and resolvers.splitmethod(filename)) or filename -- no or { }, let it bomb
+    local scheme = specification.scheme
+    if resolvers[what][scheme] then
+        if trace_locating then
+            logs.report("fileio","handler '%s' -> '%s' -> '%s'",specification.original,what,table.sequenced(specification))
+        end
+        return resolvers[what][scheme](filename,filetype) -- todo: specification
+    else
+        return resolvers[what].tex(filename,filetype) -- todo: specification
+    end
+end
+
+function resolvers.clean_path(str)
+    if str then
+        str = gsub(str,"\\","/")
+        str = gsub(str,"^!+","")
+        str = gsub(str,"^~",resolvers.homedir)
+        return str
+    else
+        return nil
+    end
+end
+
+function resolvers.do_with_path(name,func)
+    local pathlist = resolvers.expanded_path_list(name)
+    for i=1,#pathlist do
+        func("^"..resolvers.clean_path(pathlist[i]))
+    end
+end
+
+function resolvers.do_with_var(name,func)
+    func(expanded_var(name))
+end
+
+function resolvers.with_files(pattern,handle)
+    local hashes = instance.hashes
+    for i=1,#hashes do
+        local hash = hashes[i]
+        local blobpath = hash.tag
+        local blobtype = hash.type
+        if blobpath then
+            local files = instance.files[blobpath]
+            if files then
+                for k,v in next, files do
+                    if find(k,"^remap:") then
+                        k = files[k]
+                        v = files[k] -- chained
+                    end
+                    if find(k,pattern) then
+                        if type(v) == "string" then
+                            handle(blobtype,blobpath,v,k)
+                        else
+                            for _,vv in next, v do -- indexed
+                                handle(blobtype,blobpath,vv,k)
+                            end
+                        end
+                    end
+                end
+            end
+        end
+    end
+end
+
+function resolvers.locate_format(name)
+    local barename, fmtname = gsub(name,"%.%a+$",""), ""
+    if resolvers.usecache then
+        local path = file.join(caches.setpath("formats")) -- maybe platform
+        fmtname = file.join(path,barename..".fmt") or ""
+    end
+    if fmtname == "" then
+        fmtname = resolvers.find_files(barename..".fmt")[1] or ""
+    end
+    fmtname = resolvers.clean_path(fmtname)
+    if fmtname ~= "" then
+        local barename = file.removesuffix(fmtname)
+        local luaname, lucname, luiname = barename .. ".lua", barename .. ".luc", barename .. ".lui"
+        if lfs.isfile(luiname) then
+            return barename, luiname
+        elseif lfs.isfile(lucname) then
+            return barename, lucname
+        elseif lfs.isfile(luaname) then
+            return barename, luaname
+        end
+    end
+    return nil, nil
+end
+
+function resolvers.boolean_variable(str,default)
+    local b = resolvers.expansion(str)
+    if b == "" then
+        return default
+    else
+        b = toboolean(b)
+        return (b == nil and default) or b
+    end
+end
+
+texconfig.kpse_init = false
+
+kpse = { original = kpse } setmetatable(kpse, { __index = function(k,v) return resolvers[v] end } )
+
+-- for a while
+
+input = resolvers
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-tmp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+--[[ldx--
+<p>This module deals with caching data. It sets up the paths and
+implements loaders and savers for tables. Best is to set the
+following variable. When not set, the usual paths will be
+checked. Personally I prefer the (users) temporary path.</p>
+
+</code>
+TEXMFCACHE=$TMP;$TEMP;$TMPDIR;$TEMPDIR;$HOME;$TEXMFVAR;$VARTEXMF;.
+</code>
+
+<p>Currently we do no locking when we write files. This is no real
+problem because most caching involves fonts and the chance of them
+being written at the same time is small. We also need to extend
+luatools with a recache feature.</p>
+--ldx]]--
+
+local format, lower, gsub = string.format, string.lower, string.gsub
+
+local trace_cache = false  trackers.register("resolvers.cache", function(v) trace_cache = v end) -- not used yet
+
+caches = caches or { }
+
+caches.path     = caches.path or nil
+caches.base     = caches.base or "luatex-cache"
+caches.more     = caches.more or "context"
+caches.direct   = false -- true is faster but may need huge amounts of memory
+caches.tree     = false
+caches.paths    = caches.paths or nil
+caches.force    = false
+caches.defaults = { "TEXMFCACHE", "TMPDIR", "TEMPDIR", "TMP", "TEMP", "HOME", "HOMEPATH" }
+
+function caches.temp()
+    local cachepath = nil
+    local function check(list,isenv)
+        if not cachepath then
+            for k=1,#list do
+                local v = list[k]
+                cachepath = (isenv and (os.env[v] or "")) or v or ""
+                if cachepath == "" then
+                    -- next
+                else
+                    cachepath = resolvers.clean_path(cachepath)
+                    if lfs.isdir(cachepath) and file.iswritable(cachepath) then -- lfs.attributes(cachepath,"mode") == "directory"
+                        break
+                    elseif caches.force or io.ask(format("\nShould I create the cache path %s?",cachepath), "no", { "yes", "no" }) == "yes" then
+                        dir.mkdirs(cachepath)
+                        if lfs.isdir(cachepath) and file.iswritable(cachepath) then
+                            break
+                        end
+                    end
+                end
+                cachepath = nil
+            end
+        end
+    end
+    check(resolvers.clean_path_list("TEXMFCACHE") or { })
+    check(caches.defaults,true)
+    if not cachepath then
+        print("\nfatal error: there is no valid (writable) cache path defined\n")
+        os.exit()
+    elseif not lfs.isdir(cachepath) then -- lfs.attributes(cachepath,"mode") ~= "directory"
+        print(format("\nfatal error: cache path %s is not a directory\n",cachepath))
+        os.exit()
+    end
+    cachepath = file.collapse_path(cachepath)
+    function caches.temp()
+        return cachepath
+    end
+    return cachepath
+end
+
+function caches.configpath()
+    return table.concat(resolvers.instance.cnffiles,";")
+end
+
+function caches.hashed(tree)
+    return md5.hex(gsub(lower(tree),"[\\\/]+","/"))
+end
+
+function caches.treehash()
+    local tree = caches.configpath()
+    if not tree or tree == "" then
+        return false
+    else
+        return caches.hashed(tree)
+    end
+end
+
+function caches.setpath(...)
+    if not caches.path then
+        if not caches.path then
+            caches.path = caches.temp()
+        end
+        caches.path = resolvers.clean_path(caches.path) -- to be sure
+        caches.tree = caches.tree or caches.treehash()
+        if caches.tree then
+            caches.path = dir.mkdirs(caches.path,caches.base,caches.more,caches.tree)
+        else
+            caches.path = dir.mkdirs(caches.path,caches.base,caches.more)
+        end
+    end
+    if not caches.path then
+        caches.path = '.'
+    end
+    caches.path = resolvers.clean_path(caches.path)
+    local dirs = { ... }
+    if #dirs > 0 then
+        local pth = dir.mkdirs(caches.path,...)
+        return pth
+    end
+    caches.path = dir.expand_name(caches.path)
+    return caches.path
+end
+
+function caches.definepath(category,subcategory)
+    return function()
+        return caches.setpath(category,subcategory)
+    end
+end
+
+function caches.setluanames(path,name)
+    return path .. "/" .. name .. ".tma", path .. "/" .. name .. ".tmc"
+end
+
+function caches.loaddata(path,name)
+    local tmaname, tmcname = caches.setluanames(path,name)
+    local loader = loadfile(tmcname) or loadfile(tmaname)
+    if loader then
+        loader = loader()
+        collectgarbage("step")
+        return loader
+    else
+        return false
+    end
+end
+
+--~ function caches.loaddata(path,name)
+--~     local tmaname, tmcname = caches.setluanames(path,name)
+--~     return dofile(tmcname) or dofile(tmaname)
+--~ end
+
+function caches.iswritable(filepath,filename)
+    local tmaname, tmcname = caches.setluanames(filepath,filename)
+    return file.iswritable(tmaname)
+end
+
+function caches.savedata(filepath,filename,data,raw)
+    local tmaname, tmcname = caches.setluanames(filepath,filename)
+    local reduce, simplify = true, true
+    if raw then
+        reduce, simplify = false, false
+    end
+    data.cache_uuid = os.uuid()
+    if caches.direct then
+        file.savedata(tmaname, table.serialize(data,'return',false,true,false)) -- no hex
+    else
+        table.tofile(tmaname, data,'return',false,true,false) -- maybe not the last true
+    end
+    local cleanup = resolvers.boolean_variable("PURGECACHE", false)
+    local strip = resolvers.boolean_variable("LUACSTRIP", true)
+    utils.lua.compile(tmaname, tmcname, cleanup, strip)
+end
+
+-- here we use the cache for format loading (texconfig.[formatname|jobname])
+
+--~ if tex and texconfig and texconfig.formatname and texconfig.formatname == "" then
+if tex and texconfig and (not texconfig.formatname or texconfig.formatname == "") and input and resolvers.instance then
+    if not texconfig.luaname then texconfig.luaname = "cont-en.lua" end -- or luc
+    texconfig.formatname = caches.setpath("formats") .. "/" .. gsub(texconfig.luaname,"%.lu.$",".fmt")
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-inp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+resolvers.finders = resolvers.finders or { }
+resolvers.openers = resolvers.openers or { }
+resolvers.loaders = resolvers.loaders or { }
+
+resolvers.finders.notfound  = { nil }
+resolvers.openers.notfound  = { nil }
+resolvers.loaders.notfound  = { false, nil, 0 }
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-out'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+outputs = outputs or { }
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-con'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, lower, gsub = string.format, string.lower, string.gsub
+
+local trace_cache      = false  trackers.register("resolvers.cache",      function(v) trace_cache      = v end)
+local trace_containers = false  trackers.register("resolvers.containers", function(v) trace_containers = v end)
+local trace_storage    = false  trackers.register("resolvers.storage",    function(v) trace_storage    = v end)
+
+--[[ldx--
+<p>Once we found ourselves defining similar cache constructs
+several times, containers were introduced. Containers are used
+to collect tables in memory and reuse them when possible based
+on (unique) hashes (to be provided by the calling function).</p>
+
+<p>Caching to disk is disabled by default. Version numbers are
+stored in the saved table which makes it possible to change the
+table structures without bothering about the disk cache.</p>
+
+<p>Examples of usage can be found in the font related code.</p>
+--ldx]]--
+
+containers = containers or { }
+
+containers.usecache = true
+
+local function report(container,tag,name)
+    if trace_cache or trace_containers then
+        logs.report(format("%s cache",container.subcategory),"%s: %s",tag,name or 'invalid')
+    end
+end
+
+local allocated = { }
+
+-- tracing
+
+function containers.define(category, subcategory, version, enabled)
+    return function()
+        if category and subcategory then
+            local c = allocated[category]
+            if not c then
+                c  = { }
+                allocated[category] = c
+            end
+            local s = c[subcategory]
+            if not s then
+                s = {
+                    category = category,
+                    subcategory = subcategory,
+                    storage = { },
+                    enabled = enabled,
+                    version = version or 1.000,
+                    trace = false,
+                    path = caches and caches.setpath and caches.setpath(category,subcategory),
+                }
+                c[subcategory] = s
+            end
+            return s
+        else
+            return nil
+        end
+    end
+end
+
+function containers.is_usable(container, name)
+    return container.enabled and caches and caches.iswritable(container.path, name)
+end
+
+function containers.is_valid(container, name)
+    if name and name ~= "" then
+        local storage = container.storage[name]
+        return storage and storage.cache_version == container.version
+    else
+        return false
+    end
+end
+
+function containers.read(container,name)
+    if container.enabled and caches and not container.storage[name] and containers.usecache then
+        container.storage[name] = caches.loaddata(container.path,name)
+        if containers.is_valid(container,name) then
+            report(container,"loaded",name)
+        else
+            container.storage[name] = nil
+        end
+    end
+    if container.storage[name] then
+        report(container,"reusing",name)
+    end
+    return container.storage[name]
+end
+
+function containers.write(container, name, data)
+    if data then
+        data.cache_version = container.version
+        if container.enabled and caches then
+            local unique, shared = data.unique, data.shared
+            data.unique, data.shared = nil, nil
+            caches.savedata(container.path, name, data)
+            report(container,"saved",name)
+            data.unique, data.shared = unique, shared
+        end
+        report(container,"stored",name)
+        container.storage[name] = data
+    end
+    return data
+end
+
+function containers.content(container,name)
+    return container.storage[name]
+end
+
+function containers.cleanname(name)
+    return (gsub(lower(name),"[^%w%d]+","-"))
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-use'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, lower, gsub, find = string.format, string.lower, string.gsub, string.find
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+-- since we want to use the cache instead of the tree, we will now
+-- reimplement the saver.
+
+local save_data = resolvers.save_data
+local load_data = resolvers.load_data
+
+resolvers.cachepath = nil  -- public, for tracing
+resolvers.usecache  = true -- public, for tracing
+
+function resolvers.save_data(dataname)
+    save_data(dataname, function(cachename,dataname)
+        resolvers.usecache = not toboolean(resolvers.expansion("CACHEINTDS") or "false",true)
+        if resolvers.usecache then
+            resolvers.cachepath = resolvers.cachepath or caches.definepath("trees")
+            return file.join(resolvers.cachepath(),caches.hashed(cachename))
+        else
+            return file.join(cachename,dataname)
+        end
+    end)
+end
+
+function resolvers.load_data(pathname,dataname,filename)
+    load_data(pathname,dataname,filename,function(dataname,filename)
+        resolvers.usecache = not toboolean(resolvers.expansion("CACHEINTDS") or "false",true)
+        if resolvers.usecache then
+            resolvers.cachepath = resolvers.cachepath or caches.definepath("trees")
+            return file.join(resolvers.cachepath(),caches.hashed(pathname))
+        else
+            if not filename or (filename == "") then
+                filename = dataname
+            end
+            return file.join(pathname,filename)
+        end
+    end)
+end
+
+-- we will make a better format, maybe something xml or just text or lua
+
+resolvers.automounted = resolvers.automounted or { }
+
+function resolvers.automount(usecache)
+    local mountpaths = resolvers.clean_path_list(resolvers.expansion('TEXMFMOUNT'))
+    if (not mountpaths or #mountpaths == 0) and usecache then
+        mountpaths = { caches.setpath("mount") }
+    end
+    if mountpaths and #mountpaths > 0 then
+        statistics.starttiming(resolvers.instance)
+        for k=1,#mountpaths do
+            local root = mountpaths[k]
+            local f = io.open(root.."/url.tmi")
+            if f then
+                for line in f:lines() do
+                    if line then
+                        if find(line,"^[%%#%-]") then -- or %W
+                            -- skip
+                        elseif find(line,"^zip://") then
+                            if trace_locating then
+                                logs.report("fileio","mounting %s",line)
+                            end
+                            table.insert(resolvers.automounted,line)
+                            resolvers.usezipfile(line)
+                        end
+                    end
+                end
+                f:close()
+            end
+        end
+        statistics.stoptiming(resolvers.instance)
+    end
+end
+
+-- status info
+
+statistics.register("used config path", function() return caches.configpath()  end)
+statistics.register("used cache path",  function() return caches.temp() or "?" end)
+
+-- experiment (code will move)
+
+function statistics.save_fmt_status(texname,formatbanner,sourcefile) -- texname == formatname
+    local enginebanner = status.list().banner
+    if formatbanner and enginebanner and sourcefile then
+        local luvname = file.replacesuffix(texname,"luv")
+        local luvdata = {
+            enginebanner = enginebanner,
+            formatbanner = formatbanner,
+            sourcehash   = md5.hex(io.loaddata(resolvers.find_file(sourcefile)) or "unknown"),
+            sourcefile   = sourcefile,
+        }
+        io.savedata(luvname,table.serialize(luvdata,true))
+    end
+end
+
+function statistics.check_fmt_status(texname)
+    local enginebanner = status.list().banner
+    if enginebanner and texname then
+        local luvname = file.replacesuffix(texname,"luv")
+        if lfs.isfile(luvname) then
+            local luv = dofile(luvname)
+            if luv and luv.sourcefile then
+                local sourcehash = md5.hex(io.loaddata(resolvers.find_file(luv.sourcefile)) or "unknown")
+                local luvbanner = luv.enginebanner or "?"
+                if luvbanner ~= enginebanner then
+                    return string.format("engine mismatch (luv:%s <> bin:%s)",luvbanner,enginebanner)
+                end
+                local luvhash = luv.sourcehash or "?"
+                if luvhash ~= sourcehash then
+                    return string.format("source mismatch (luv:%s <> bin:%s)",luvhash,sourcehash)
+                end
+            else
+                return "invalid status file"
+            end
+        else
+            return "missing status file"
+        end
+    end
+    return true
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['luat-kps'] = {
+    version   = 1.001,
+    comment   = "companion to luatools.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+--[[ldx--
+<p>This file is used when we want the input handlers to behave like
+<type>kpsewhich</type>. What to do with the following:</p>
+
+<typing>
+{$SELFAUTOLOC,$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,}/web2c}
+$SELFAUTOLOC    : /usr/tex/bin/platform
+$SELFAUTODIR    : /usr/tex/bin
+$SELFAUTOPARENT : /usr/tex
+</typing>
+
+<p>How about just forgetting about them?</p>
+--ldx]]--
+
+local suffixes = resolvers.suffixes
+local formats  = resolvers.formats
+
+suffixes['gf']                       = { '<resolution>gf' }
+suffixes['pk']                       = { '<resolution>pk' }
+suffixes['base']                     = { 'base' }
+suffixes['bib']                      = { 'bib' }
+suffixes['bst']                      = { 'bst' }
+suffixes['cnf']                      = { 'cnf' }
+suffixes['mem']                      = { 'mem' }
+suffixes['mf']                       = { 'mf' }
+suffixes['mfpool']                   = { 'pool' }
+suffixes['mft']                      = { 'mft' }
+suffixes['mppool']                   = { 'pool' }
+suffixes['graphic/figure']           = { 'eps', 'epsi' }
+suffixes['texpool']                  = { 'pool' }
+suffixes['PostScript header']        = { 'pro' }
+suffixes['ist']                      = { 'ist' }
+suffixes['web']                      = { 'web', 'ch' }
+suffixes['cweb']                     = { 'w', 'web', 'ch' }
+suffixes['cmap files']               = { 'cmap' }
+suffixes['lig files']                = { 'lig' }
+suffixes['bitmap font']              = { }
+suffixes['MetaPost support']         = { }
+suffixes['TeX system documentation'] = { }
+suffixes['TeX system sources']       = { }
+suffixes['dvips config']             = { }
+suffixes['type42 fonts']             = { }
+suffixes['web2c files']              = { }
+suffixes['other text files']         = { }
+suffixes['other binary files']       = { }
+suffixes['opentype fonts']           = { 'otf' }
+
+suffixes['fmt']                      = { 'fmt' }
+suffixes['texmfscripts']             = { 'rb','lua','py','pl' }
+
+suffixes['pdftex config']            = { }
+suffixes['Troff fonts']              = { }
+
+suffixes['ls-R']                     = { }
+
+--[[ldx--
+<p>If you wondered abou tsome of the previous mappings, how about
+the next bunch:</p>
+--ldx]]--
+
+formats['bib']                      = ''
+formats['bst']                      = ''
+formats['mft']                      = ''
+formats['ist']                      = ''
+formats['web']                      = ''
+formats['cweb']                     = ''
+formats['MetaPost support']         = ''
+formats['TeX system documentation'] = ''
+formats['TeX system sources']       = ''
+formats['Troff fonts']              = ''
+formats['dvips config']             = ''
+formats['graphic/figure']           = ''
+formats['ls-R']                     = ''
+formats['other text files']         = ''
+formats['other binary files']       = ''
+
+formats['gf']                       = ''
+formats['pk']                       = ''
+formats['base']                     = 'MFBASES'
+formats['cnf']                      = ''
+formats['mem']                      = 'MPMEMS'
+formats['mf']                       = 'MFINPUTS'
+formats['mfpool']                   = 'MFPOOL'
+formats['mppool']                   = 'MPPOOL'
+formats['texpool']                  = 'TEXPOOL'
+formats['PostScript header']        = 'TEXPSHEADERS'
+formats['cmap files']               = 'CMAPFONTS'
+formats['type42 fonts']             = 'T42FONTS'
+formats['web2c files']              = 'WEB2C'
+formats['pdftex config']            = 'PDFTEXCONFIG'
+formats['texmfscripts']             = 'TEXMFSCRIPTS'
+formats['bitmap font']              = ''
+formats['lig files']                = 'LIGFONTS'
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-aux'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local find = string.find
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+function resolvers.update_script(oldname,newname) -- oldname -> own.name, not per se a suffix
+    local scriptpath = "scripts/context/lua"
+    newname = file.addsuffix(newname,"lua")
+    local oldscript = resolvers.clean_path(oldname)
+    if trace_locating then
+        logs.report("fileio","to be replaced old script %s", oldscript)
+    end
+    local newscripts = resolvers.find_files(newname) or { }
+    if #newscripts == 0 then
+        if trace_locating then
+            logs.report("fileio","unable to locate new script")
+        end
+    else
+        for i=1,#newscripts do
+            local newscript = resolvers.clean_path(newscripts[i])
+            if trace_locating then
+                logs.report("fileio","checking new script %s", newscript)
+            end
+            if oldscript == newscript then
+                if trace_locating then
+                    logs.report("fileio","old and new script are the same")
+                end
+            elseif not find(newscript,scriptpath) then
+                if trace_locating then
+                    logs.report("fileio","new script should come from %s",scriptpath)
+                end
+            elseif not (find(oldscript,file.removesuffix(newname).."$") or find(oldscript,newname.."$")) then
+                if trace_locating then
+                    logs.report("fileio","invalid new script name")
+                end
+            else
+                local newdata = io.loaddata(newscript)
+                if newdata then
+                    if trace_locating then
+                        logs.report("fileio","old script content replaced by new content")
+                    end
+                    io.savedata(oldscript,newdata)
+                    break
+                elseif trace_locating then
+                    logs.report("fileio","unable to load new script")
+                end
+            end
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-lst'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- used in mtxrun
+
+local find, concat, upper, format = string.find, table.concat, string.upper, string.format
+
+resolvers.listers = resolvers.listers or { }
+
+local function tabstr(str)
+    if type(str) == 'table' then
+        return concat(str," | ")
+    else
+        return str
+    end
+end
+
+local function list(list,report)
+    local instance = resolvers.instance
+    local pat = upper(pattern or "","")
+    local report = report or texio.write_nl
+    local sorted = table.sortedkeys(list)
+    for i=1,#sorted do
+        local key = sorted[i]
+        if instance.pattern == "" or find(upper(key),pat) then
+            if instance.kpseonly then
+                if instance.kpsevars[key] then
+                    report(format("%s=%s",key,tabstr(list[key])))
+                end
+            else
+                report(format('%s %s=%s',(instance.kpsevars[key] and 'K') or 'E',key,tabstr(list[key])))
+            end
+        end
+    end
+end
+
+function resolvers.listers.variables () list(resolvers.instance.variables ) end
+function resolvers.listers.expansions() list(resolvers.instance.expansions) end
+
+function resolvers.listers.configurations(report)
+    local report = report or texio.write_nl
+    local instance = resolvers.instance
+    local sorted = table.sortedkeys(instance.kpsevars)
+    for i=1,#sorted do
+        local key = sorted[i]
+        if not instance.pattern or (instance.pattern=="") or find(key,instance.pattern) then
+            report(format("%s\n",key))
+            local order = instance.order
+            for i=1,#order do
+                local str = order[i][key]
+                if str then
+                    report(format("\t%s\t%s",i,str))
+                end
+            end
+            report("")
+        end
+    end
+end
+
+
+end -- of closure
+-- end library merge
+
+-- We initialize some characteristics of this program. We need to
+-- do this before we load the libraries, else own.name will not be
+-- properly set (handy for selfcleaning the file). It's an ugly
+-- looking piece of code.
+
+own = { }
+
+own.libs = { -- todo: check which ones are really needed
+    'l-string.lua',
+    'l-lpeg.lua',
+    'l-table.lua',
+    'l-io.lua',
+    'l-number.lua',
+    'l-set.lua',
+    'l-os.lua',
+    'l-file.lua',
+    'l-md5.lua',
+    'l-url.lua',
+    'l-dir.lua',
+    'l-boolean.lua',
+    'l-unicode.lua',
+    'l-math.lua',
+    'l-utils.lua',
+    'l-aux.lua',
+    'trac-tra.lua',
+    'luat-env.lua',
+    'trac-inf.lua',
+    'trac-log.lua',
+    'data-res.lua',
+    'data-tmp.lua',
+--  'data-pre.lua',
+    'data-inp.lua',
+    'data-out.lua',
+    'data-con.lua',
+    'data-use.lua',
+--  'data-tex.lua',
+--  'data-bin.lua',
+--  'data-zip.lua',
+--  'data-crl.lua',
+--  'data-lua.lua',
+    'data-kps.lua', -- so that we can replace kpsewhich
+    'data-aux.lua', -- updater
+    'data-lst.lua', -- lister
+}
+
+-- We need this hack till luatex is fixed.
+
+if arg and arg[0] == 'luatex' and arg[1] == "--luaonly" then
+    arg[-1]=arg[0] arg[0]=arg[2] for k=3,#arg do arg[k-2]=arg[k] end arg[#arg]=nil arg[#arg]=nil
+end
+
+-- End of hack.
+
+own.name = (environment and environment.ownname) or arg[0] or 'luatools.lua'
+own.path = string.match(own.name,"^(.+)[\\/].-$") or "."
+own.list = { '.' }
+
+if own.path ~= '.' then
+    table.insert(own.list,own.path)
+end
+
+table.insert(own.list,own.path.."/../../../tex/context/base")
+table.insert(own.list,own.path.."/mtx")
+table.insert(own.list,own.path.."/../sources")
+
+function locate_libs()
+    for _, lib in pairs(own.libs) do
+        for _, pth in pairs(own.list) do
+            local filename = string.gsub(pth .. "/" .. lib,"\\","/")
+            local codeblob = loadfile(filename)
+            if codeblob then
+                codeblob()
+                own.list = { pth } -- speed up te search
+                break
+            end
+        end
+    end
+end
+
+if not resolvers then
+    locate_libs()
+end
+
+if not resolvers then
+    print("")
+    print("Luatools is unable to start up due to lack of libraries. You may")
+    print("try to run 'lua luatools.lua --selfmerge' in the path where this")
+    print("script is located (normally under ..../scripts/context/lua) which")
+    print("will make luatools library independent.")
+    os.exit()
+end
+
+logs.setprogram('LuaTools',"TDS Management Tool 1.32",environment.arguments["verbose"] or false)
+
+local instance = resolvers.reset()
+
+resolvers.defaultlibs = { -- not all are needed (this will become: context.lus (lua spec)
+    'l-string.lua',
+    'l-lpeg.lua',
+    'l-table.lua',
+    'l-boolean.lua',
+    'l-number.lua',
+    'l-unicode.lua',
+    'l-os.lua',
+    'l-io.lua',
+    'l-file.lua',
+    'l-md5.lua',
+    'l-url.lua',
+    'l-dir.lua',
+    'l-utils.lua',
+    'l-dimen.lua',
+    'trac-inf.lua',
+    'trac-tra.lua',
+    'trac-log.lua',
+    'luat-env.lua', -- here ?
+    'data-res.lua',
+    'data-inp.lua',
+    'data-out.lua',
+    'data-tmp.lua',
+    'data-con.lua',
+    'data-use.lua',
+--  'data-pre.lua',
+    'data-tex.lua',
+    'data-bin.lua',
+--  'data-zip.lua',
+--  'data-clr.lua',
+    'data-lua.lua',
+    'data-ctx.lua',
+    'luat-fio.lua',
+    'luat-cnf.lua',
+}
+
+instance.engine     =     environment.arguments["engine"]   or 'luatex'
+instance.progname   =     environment.arguments["progname"] or 'context'
+instance.luaname    =     environment.arguments["luafile"]  or "" -- environment.ownname or ""
+instance.lualibs    =     environment.arguments["lualibs"]  or table.concat(resolvers.defaultlibs,",")
+instance.allresults =     environment.arguments["all"]      or false
+instance.pattern    =     environment.arguments["pattern"]  or nil
+instance.sortdata   =     environment.arguments["sort"]     or false
+instance.kpseonly   = not environment.arguments["all"]      or false
+instance.my_format  =     environment.arguments["format"]   or instance.format
+
+if type(instance.pattern) == 'boolean' then
+    logs.simple("invalid pattern specification")
+    instance.pattern = nil
+end
+
+if environment.arguments["trace"] then resolvers.settrace(environment.arguments["trace"]) end
+
+local trackspec = environment.argument("trackers") or environment.argument("track")
+
+if trackspec then
+    trackers.enable(trackspec)
+end
+
+runners  = runners  or { }
+messages = messages or { }
+
+messages.no_ini_file = [[
+There is no lua initialization file found. This file can be forced by the
+"--progname" directive, or specified with "--luaname", or it is derived
+automatically from the formatname (aka jobname). It may be that you have
+to regenerate the file database using "luatools --generate".
+]]
+
+messages.help = [[
+--generate        generate file database
+--variables       show configuration variables
+--expansions      show expanded variables
+--configurations  show configuration order
+--expand-braces   expand complex variable
+--expand-path     expand variable (resolve paths)
+--expand-var      expand variable (resolve references)
+--show-path       show path expansion of ...
+--var-value       report value of variable
+--find-file       report file location
+--find-path       report path of file
+--make or --ini   make luatex format
+--run or --fmt=   run luatex format
+--luafile=str     lua inifile (default is <progname>.lua)
+--lualibs=list    libraries to assemble (optional when --compile)
+--compile         assemble and compile lua inifile
+--verbose         give a bit more info
+--all             show all found files
+--sort            sort cached data
+--engine=str      target engine
+--progname=str    format or backend
+--pattern=str     filter variables
+--trackers=list   enable given trackers
+]]
+
+function runners.make_format(texname)
+    local instance = resolvers.instance
+    if texname and texname ~= "" then
+        if resolvers.usecache then
+            local path = file.join(caches.setpath("formats")) -- maybe platform
+            if path and lfs then
+                lfs.chdir(path)
+            end
+        end
+        local barename = texname:gsub("%.%a+$","")
+        if barename == texname then
+            texname = texname .. ".tex"
+        end
+        local fullname = resolvers.find_files(texname)[1] or ""
+        if fullname == "" then
+            logs.simple("no tex file with name: %s",texname)
+        else
+            local luaname, lucname, luapath, lualibs = "", "", "", { }
+            -- the following is optional, since context.lua can also
+            -- handle this collect and compile business
+            if environment.arguments["compile"] then
+                if luaname == "" then luaname = barename end
+                logs.simple("creating initialization file: %s",luaname)
+                luapath = file.dirname(luaname)
+                if luapath == "" then
+                    luapath = file.dirname(texname)
+                end
+                if luapath == "" then
+                    luapath = file.dirname(resolvers.find_files(texname)[1] or "")
+                end
+                lualibs = string.split(instance.lualibs,",")
+                luaname = file.basename(barename .. ".lua")
+                lucname = file.basename(barename .. ".luc")
+                -- todo: when this fails, we can just copy the merged libraries from
+                -- luatools since they are normally the same, at least for context
+                if lualibs[1] then
+                    local firstlib = file.join(luapath,lualibs[1])
+                    if not lfs.isfile(firstlib) then
+                        local foundname = resolvers.find_files(lualibs[1])[1]
+                        if foundname then
+                            logs.simple("located library path: %s",luapath)
+                            luapath = file.dirname(foundname)
+                        end
+                    end
+                end
+                logs.simple("using library path: %s",luapath)
+                logs.simple("using lua libraries: %s",table.join(lualibs," "))
+                utils.merger.selfcreate(lualibs,luapath,luaname)
+                local strip = resolvers.boolean_variable("LUACSTRIP", true)
+                if utils.lua.compile(luaname,lucname,false,strip) and io.exists(lucname) then
+                    luaname = lucname
+                    logs.simple("using compiled initialization file: %s",lucname)
+                else
+                    logs.simple("using uncompiled initialization file: %s",luaname)
+                end
+            else
+                local what = { instance.luaname, instance.progname, barename }
+                for k=1,#what do
+                    local v = string.gsub(what[k]..".lua","%.lua%.lua$",".lua")
+                    if v and (v ~= "") then
+                        luaname = resolvers.find_files(v)[1] or ""
+                        if luaname ~= "" then
+                            break
+                        end
+                    end
+                end
+            end
+            if environment.arguments["noluc"] then
+                luaname = luaname:gsub("%.luc$",".lua") -- make this an option
+            end
+            if luaname == "" then
+                if logs.verbose then
+                    logs.simplelines(messages.no_ini_file)
+                    logs.simple("texname : %s",texname)
+                    logs.simple("luaname : %s",instance.luaname)
+                    logs.simple("progname: %s",instance.progname)
+                    logs.simple("barename: %s",barename)
+                end
+            else
+                logs.simple("using lua initialization file: %s",luaname)
+                local mp = dir.glob(file.removesuffix(file.basename(luaname)).."-*.mem")
+                if mp and #mp > 0 then
+                    for i=1,#mp do
+                        local name = mp[i]
+                        logs.simple("removing related mplib format %s", file.basename(name))
+                        os.remove(name)
+                    end
+                end
+                local flags = {
+                    "--ini",
+                    "--lua=" .. string.quote(luaname)
+                }
+                local bs = (os.platform == "unix" and "\\\\") or "\\" -- todo: make a function
+                local command = "luatex ".. table.concat(flags," ")  .. " " .. string.quote(fullname) .. " " .. bs .. "dump"
+                logs.simple("running command: %s\n",command)
+                os.spawn(command)
+                -- todo: do a dummy run that generates the related metafun and mfplain formats
+            end
+        end
+    else
+        logs.simple("no tex file given")
+    end
+end
+
+function runners.run_format(name,data,more)
+ -- hm, rather old code here; we can now use the file.whatever functions
+    if name and (name ~= "") then
+        local barename = name:gsub("%.%a+$","")
+        local fmtname = ""
+        if resolvers.usecache then
+            local path = file.join(caches.setpath("formats")) -- maybe platform
+            fmtname = file.join(path,barename..".fmt") or ""
+        end
+        if fmtname == "" then
+            fmtname = resolvers.find_files(barename..".fmt")[1] or ""
+        end
+        fmtname = resolvers.clean_path(fmtname)
+        barename = fmtname:gsub("%.%a+$","")
+        if fmtname == "" then
+            logs.simple("no format with name: %s",name)
+        else
+            local luaname = barename .. ".luc"
+            local f = io.open(luaname)
+            if not f then
+                luaname = barename .. ".lua"
+                f = io.open(luaname)
+            end
+            if f then
+                f:close()
+                local command = "luatex --fmt=" .. string.quote(barename) .. " --lua=" .. string.quote(luaname) .. " " .. string.quote(data) .. " " .. (more ~= "" and string.quote(more) or "")
+                logs.simple("running command: %s",command)
+                os.spawn(command)
+            else
+                logs.simple("using format name: %s",fmtname)
+                logs.simple("no luc/lua with name: %s",barename)
+            end
+        end
+    end
+end
+
+local ok = true
+
+-- private option --noluc for testing errors in the stub
+
+if environment.arguments["find-file"] then
+    resolvers.load()
+    instance.format  = environment.arguments["format"] or instance.format
+    if instance.pattern then
+        instance.allresults = true
+        resolvers.for_files(resolvers.find_files, { instance.pattern }, instance.my_format)
+    else
+        resolvers.for_files(resolvers.find_files, environment.files, instance.my_format)
+    end
+elseif environment.arguments["find-path"] then
+    resolvers.load()
+    local path = resolvers.find_file(environment.files[1], instance.my_format)
+    if logs.verbose then
+        logs.simple(file.dirname(path))
+    else
+        print(file.dirname(path))
+    end
+elseif environment.arguments["run"] then
+    resolvers.load("nofiles") -- ! no need for loading databases
+    logs.setverbose(true)
+    runners.run_format(environment.files[1] or "",environment.files[2] or "",environment.files[3] or "")
+elseif environment.arguments["fmt"] then
+    resolvers.load("nofiles") -- ! no need for loading databases
+    logs.setverbose(true)
+    runners.run_format(environment.arguments["fmt"], environment.files[1] or "",environment.files[2] or "")
+elseif environment.arguments["expand-braces"] then
+    resolvers.load("nofiles")
+    resolvers.for_files(resolvers.expand_braces, environment.files)
+elseif environment.arguments["expand-path"] then
+    resolvers.load("nofiles")
+    resolvers.for_files(resolvers.expand_path, environment.files)
+elseif environment.arguments["expand-var"] or environment.arguments["expand-variable"] then
+    resolvers.load("nofiles")
+    resolvers.for_files(resolvers.expand_var, environment.files)
+elseif environment.arguments["show-path"] or environment.arguments["path-value"] then
+    resolvers.load("nofiles")
+    resolvers.for_files(resolvers.show_path, environment.files)
+elseif environment.arguments["var-value"] or environment.arguments["show-value"] then
+    resolvers.load("nofiles")
+    resolvers.for_files(resolvers.var_value, environment.files)
+elseif environment.arguments["format-path"] then
+    resolvers.load()
+    logs.simple(caches.setpath("format"))
+elseif instance.pattern then -- brrr
+    resolvers.load()
+    instance.format = environment.arguments["format"] or instance.format
+    instance.allresults = true
+    resolvers.for_files(resolvers.find_files, { instance.pattern }, instance.my_format)
+elseif environment.arguments["generate"] then
+    instance.renewcache = true
+    logs.setverbose(true)
+    resolvers.load()
+elseif environment.arguments["make"] or environment.arguments["ini"] or environment.arguments["compile"] then
+    resolvers.load()
+    logs.setverbose(true)
+    runners.make_format(environment.files[1] or "")
+elseif environment.arguments["selfmerge"] then
+    utils.merger.selfmerge(own.name,own.libs,own.list)
+elseif environment.arguments["selfclean"] then
+    utils.merger.selfclean(own.name)
+elseif environment.arguments["selfupdate"] then
+    resolvers.load()
+    logs.setverbose(true)
+    resolvers.update_script(own.name,"luatools")
+elseif environment.arguments["variables"] or environment.arguments["show-variables"] then
+    resolvers.load("nofiles")
+    resolvers.listers.variables()
+elseif environment.arguments["expansions"] or environment.arguments["show-expansions"] then
+    resolvers.load("nofiles")
+    resolvers.listers.expansions()
+elseif environment.arguments["configurations"] or environment.arguments["show-configurations"] then
+    resolvers.load("nofiles")
+    resolvers.listers.configurations()
+elseif environment.arguments["help"] or (environment.files[1]=='help') or (#environment.files==0) then
+    logs.help(messages.help)
+else
+    resolvers.load()
+    resolvers.for_files(resolvers.find_files, environment.files, instance.my_format)
+end
+
+if logs.verbose then
+    logs.simpleline()
+    logs.simple("runtime: %0.3f seconds",os.runtime())
+end
+
+if os.platform == "unix" then
+    io.write("\n")
+end
diff --git a/scripts/context/stubs/unix/metatex b/scripts/context/stubs/unix/metatex
new file mode 100644
index 000000000..f0c6b65d4
--- /dev/null
+++ b/scripts/context/stubs/unix/metatex
@@ -0,0 +1,2 @@
+#!/bin/sh
+mtxrun --script metatex "$@"
diff --git a/scripts/context/stubs/unix/mtxrun b/scripts/context/stubs/unix/mtxrun
new file mode 100644
index 000000000..b99327692
--- /dev/null
+++ b/scripts/context/stubs/unix/mtxrun
@@ -0,0 +1,12639 @@
+#!/usr/bin/env texlua
+
+if not modules then modules = { } end modules ['mtxrun'] = {
+    version   = 1.001,
+    comment   = "runner, lua replacement for texmfstart.rb",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+
+-- one can make a stub:
+--
+-- #!/bin/sh
+-- env LUATEXDIR=/....../texmf/scripts/context/lua luatex --luaonly mtxrun.lua "$@"
+
+-- filename : mtxrun.lua
+-- comment  : companion to context.tex
+-- author   : Hans Hagen, PRAGMA-ADE, Hasselt NL
+-- copyright: PRAGMA ADE / ConTeXt Development Team
+-- license  : see context related readme files
+
+-- This script is based on texmfstart.rb but does not use kpsewhich to
+-- locate files. Although kpse is a library it never came to opening up
+-- its interface to other programs (esp scripting languages) and so we
+-- do it ourselves. The lua variant evolved out of an experimental ruby
+-- one. Interesting is that using a scripting language instead of c does
+-- not have a speed penalty. Actually the lua variant is more efficient,
+-- especially when multiple calls to kpsewhich are involved. The lua
+-- library also gives way more control.
+
+-- to be done / considered
+--
+-- support for --exec or make it default
+-- support for jar files (or maybe not, never used, too messy)
+-- support for $RUBYINPUTS cum suis (if still needed)
+-- remember for subruns: _CTX_K_V_#{original}_
+-- remember for subruns: _CTX_K_S_#{original}_
+-- remember for subruns: TEXMFSTART.#{original} [tex.rb texmfstart.rb]
+
+texlua = true
+
+-- begin library merge
+
+
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-string'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local sub, gsub, find, match, gmatch, format, char, byte, rep, lower = string.sub, string.gsub, string.find, string.match, string.gmatch, string.format, string.char, string.byte, string.rep, string.lower
+local lpegmatch = lpeg.match
+
+-- some functions may disappear as they are not used anywhere
+
+if not string.split then
+
+    -- this will be overloaded by a faster lpeg variant
+
+    function string:split(pattern)
+        if #self > 0 then
+            local t = { }
+            for s in gmatch(self..pattern,"(.-)"..pattern) do
+                t[#t+1] = s
+            end
+            return t
+        else
+            return { }
+        end
+    end
+
+end
+
+local chr_to_esc = {
+    ["%"] = "%%",
+    ["."] = "%.",
+    ["+"] = "%+", ["-"] = "%-", ["*"] = "%*",
+    ["^"] = "%^", ["$"] = "%$",
+    ["["] = "%[", ["]"] = "%]",
+    ["("] = "%(", [")"] = "%)",
+    ["{"] = "%{", ["}"] = "%}"
+}
+
+string.chr_to_esc = chr_to_esc
+
+function string:esc() -- variant 2
+    return (gsub(self,"(.)",chr_to_esc))
+end
+
+function string:unquote()
+    return (gsub(self,"^([\"\'])(.*)%1$","%2"))
+end
+
+--~ function string:unquote()
+--~     if find(self,"^[\'\"]") then
+--~         return sub(self,2,-2)
+--~     else
+--~         return self
+--~     end
+--~ end
+
+function string:quote() -- we could use format("%q")
+    return format("%q",self)
+end
+
+function string:count(pattern) -- variant 3
+    local n = 0
+    for _ in gmatch(self,pattern) do
+        n = n + 1
+    end
+    return n
+end
+
+function string:limit(n,sentinel)
+    if #self > n then
+        sentinel = sentinel or " ..."
+        return sub(self,1,(n-#sentinel)) .. sentinel
+    else
+        return self
+    end
+end
+
+--~ function string:strip() -- the .- is quite efficient
+--~  -- return match(self,"^%s*(.-)%s*$") or ""
+--~  -- return match(self,'^%s*(.*%S)') or '' -- posted on lua list
+--~     return find(s,'^%s*$') and '' or match(s,'^%s*(.*%S)')
+--~ end
+
+do -- roberto's variant:
+    local space    = lpeg.S(" \t\v\n")
+    local nospace  = 1 - space
+    local stripper = space^0 * lpeg.C((space^0 * nospace^1)^0)
+    function string.strip(str)
+        return lpegmatch(stripper,str) or ""
+    end
+end
+
+function string:is_empty()
+    return not find(self,"%S")
+end
+
+function string:enhance(pattern,action)
+    local ok, n = true, 0
+    while ok do
+        ok = false
+        self = gsub(self,pattern, function(...)
+            ok, n = true, n + 1
+            return action(...)
+        end)
+    end
+    return self, n
+end
+
+local chr_to_hex, hex_to_chr = { }, { }
+
+for i=0,255 do
+    local c, h = char(i), format("%02X",i)
+    chr_to_hex[c], hex_to_chr[h] = h, c
+end
+
+function string:to_hex()
+    return (gsub(self or "","(.)",chr_to_hex))
+end
+
+function string:from_hex()
+    return (gsub(self or "","(..)",hex_to_chr))
+end
+
+if not string.characters then
+
+    local function nextchar(str, index)
+        index = index + 1
+        return (index <= #str) and index or nil, sub(str,index,index)
+    end
+    function string:characters()
+        return nextchar, self, 0
+    end
+    local function nextbyte(str, index)
+        index = index + 1
+        return (index <= #str) and index or nil, byte(sub(str,index,index))
+    end
+    function string:bytes()
+        return nextbyte, self, 0
+    end
+
+end
+
+-- we can use format for this (neg n)
+
+function string:rpadd(n,chr)
+    local m = n-#self
+    if m > 0 then
+        return self .. rep(chr or " ",m)
+    else
+        return self
+    end
+end
+
+function string:lpadd(n,chr)
+    local m = n-#self
+    if m > 0 then
+        return rep(chr or " ",m) .. self
+    else
+        return self
+    end
+end
+
+string.padd = string.rpadd
+
+function is_number(str) -- tonumber
+    return find(str,"^[%-%+]?[%d]-%.?[%d+]$") == 1
+end
+
+--~ print(is_number("1"))
+--~ print(is_number("1.1"))
+--~ print(is_number(".1"))
+--~ print(is_number("-0.1"))
+--~ print(is_number("+0.1"))
+--~ print(is_number("-.1"))
+--~ print(is_number("+.1"))
+
+function string:split_settings() -- no {} handling, see l-aux for lpeg variant
+    if find(self,"=") then
+        local t = { }
+        for k,v in gmatch(self,"(%a+)=([^%,]*)") do
+            t[k] = v
+        end
+        return t
+    else
+        return nil
+    end
+end
+
+local patterns_escapes = {
+    ["-"] = "%-",
+    ["."] = "%.",
+    ["+"] = "%+",
+    ["*"] = "%*",
+    ["%"] = "%%",
+    ["("] = "%)",
+    [")"] = "%)",
+    ["["] = "%[",
+    ["]"] = "%]",
+}
+
+function string:pattesc()
+    return (gsub(self,".",patterns_escapes))
+end
+
+local simple_escapes = {
+    ["-"] = "%-",
+    ["."] = "%.",
+    ["?"] = ".",
+    ["*"] = ".*",
+}
+
+function string:simpleesc()
+    return (gsub(self,".",simple_escapes))
+end
+
+function string:tohash()
+    local t = { }
+    for s in gmatch(self,"([^, ]+)") do -- lpeg
+        t[s] = true
+    end
+    return t
+end
+
+local pattern = lpeg.Ct(lpeg.C(1)^0)
+
+function string:totable()
+    return lpegmatch(pattern,self)
+end
+
+--~ local t = {
+--~     "1234567123456712345671234567",
+--~     "a\tb\tc",
+--~     "aa\tbb\tcc",
+--~     "aaa\tbbb\tccc",
+--~     "aaaa\tbbbb\tcccc",
+--~     "aaaaa\tbbbbb\tccccc",
+--~     "aaaaaa\tbbbbbb\tcccccc",
+--~ }
+--~ for k,v do
+--~     print(string.tabtospace(t[k]))
+--~ end
+
+function string.tabtospace(str,tab)
+    -- we don't handle embedded newlines
+    while true do
+        local s = find(str,"\t")
+        if s then
+            if not tab then tab = 7 end -- only when found
+            local d = tab-(s-1) % tab
+            if d > 0 then
+                str = gsub(str,"\t",rep(" ",d),1)
+            else
+                str = gsub(str,"\t","",1)
+            end
+        else
+            break
+        end
+    end
+    return str
+end
+
+function string:compactlong() -- strips newlines and leading spaces
+    self = gsub(self,"[\n\r]+ *","")
+    self = gsub(self,"^ *","")
+    return self
+end
+
+function string:striplong() -- strips newlines and leading spaces
+    self = gsub(self,"^%s*","")
+    self = gsub(self,"[\n\r]+ *","\n")
+    return self
+end
+
+function string:topattern(lowercase,strict)
+    if lowercase then
+        self = lower(self)
+    end
+    self = gsub(self,".",simple_escapes)
+    if self == "" then
+        self = ".*"
+    elseif strict then
+        self = "^" .. self .. "$"
+    end
+    return self
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-lpeg'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local lpeg = require("lpeg")
+
+lpeg.patterns  = lpeg.patterns or { } -- so that we can share
+local patterns = lpeg.patterns
+
+local P, R, S, Ct, C, Cs, Cc, V = lpeg.P, lpeg.R, lpeg.S, lpeg.Ct, lpeg.C, lpeg.Cs, lpeg.Cc, lpeg.V
+local match = lpeg.match
+
+local digit, sign      = R('09'), S('+-')
+local cr, lf, crlf     = P("\r"), P("\n"), P("\r\n")
+local utf8byte         = R("\128\191")
+
+patterns.utf8byte      = utf8byte
+patterns.utf8one       = R("\000\127")
+patterns.utf8two       = R("\194\223") * utf8byte
+patterns.utf8three     = R("\224\239") * utf8byte * utf8byte
+patterns.utf8four      = R("\240\244") * utf8byte * utf8byte * utf8byte
+
+patterns.digit         = digit
+patterns.sign          = sign
+patterns.cardinal      = sign^0 * digit^1
+patterns.integer       = sign^0 * digit^1
+patterns.float         = sign^0 * digit^0 * P('.') * digit^1
+patterns.number        = patterns.float + patterns.integer
+patterns.oct           = P("0") * R("07")^1
+patterns.octal         = patterns.oct
+patterns.HEX           = P("0x") * R("09","AF")^1
+patterns.hex           = P("0x") * R("09","af")^1
+patterns.hexadecimal   = P("0x") * R("09","AF","af")^1
+patterns.lowercase     = R("az")
+patterns.uppercase     = R("AZ")
+patterns.letter        = patterns.lowercase + patterns.uppercase
+patterns.space         = S(" ")
+patterns.eol           = S("\n\r")
+patterns.spacer        = S(" \t\f\v")  -- + string.char(0xc2, 0xa0) if we want utf (cf mail roberto)
+patterns.newline       = crlf + cr + lf
+patterns.nonspace      = 1 - patterns.space
+patterns.nonspacer     = 1 - patterns.spacer
+patterns.whitespace    = patterns.eol + patterns.spacer
+patterns.nonwhitespace = 1 - patterns.whitespace
+patterns.utf8          = patterns.utf8one + patterns.utf8two + patterns.utf8three + patterns.utf8four
+patterns.utfbom        = P('\000\000\254\255') + P('\255\254\000\000') + P('\255\254') + P('\254\255') + P('\239\187\191')
+
+function lpeg.anywhere(pattern) --slightly adapted from website
+    return P { P(pattern) + 1 * V(1) } -- why so complex?
+end
+
+function lpeg.splitter(pattern, action)
+    return (((1-P(pattern))^1)/action+1)^0
+end
+
+local spacing  = patterns.spacer^0 * patterns.newline -- sort of strip
+local empty    = spacing * Cc("")
+local nonempty = Cs((1-spacing)^1) * spacing^-1
+local content  = (empty + nonempty)^1
+
+local capture = Ct(content^0)
+
+function string:splitlines()
+    return match(capture,self)
+end
+
+patterns.textline = content
+
+--~ local p = lpeg.splitat("->",false)  print(match(p,"oeps->what->more"))  -- oeps what more
+--~ local p = lpeg.splitat("->",true)   print(match(p,"oeps->what->more"))  -- oeps what->more
+--~ local p = lpeg.splitat("->",false)  print(match(p,"oeps"))              -- oeps
+--~ local p = lpeg.splitat("->",true)   print(match(p,"oeps"))              -- oeps
+
+local splitters_s, splitters_m = { }, { }
+
+local function splitat(separator,single)
+    local splitter = (single and splitters_s[separator]) or splitters_m[separator]
+    if not splitter then
+        separator = P(separator)
+        if single then
+            local other, any = C((1 - separator)^0), P(1)
+            splitter = other * (separator * C(any^0) + "") -- ?
+            splitters_s[separator] = splitter
+        else
+            local other = C((1 - separator)^0)
+            splitter = other * (separator * other)^0
+            splitters_m[separator] = splitter
+        end
+    end
+    return splitter
+end
+
+lpeg.splitat = splitat
+
+local cache = { }
+
+function lpeg.split(separator,str)
+    local c = cache[separator]
+    if not c then
+        c = Ct(splitat(separator))
+        cache[separator] = c
+    end
+    return match(c,str)
+end
+
+function string:split(separator)
+    local c = cache[separator]
+    if not c then
+        c = Ct(splitat(separator))
+        cache[separator] = c
+    end
+    return match(c,self)
+end
+
+lpeg.splitters = cache
+
+local cache = { }
+
+function lpeg.checkedsplit(separator,str)
+    local c = cache[separator]
+    if not c then
+        separator = P(separator)
+        local other = C((1 - separator)^0)
+        c = Ct(separator^0 * other * (separator^1 * other)^0)
+        cache[separator] = c
+    end
+    return match(c,str)
+end
+
+function string:checkedsplit(separator)
+    local c = cache[separator]
+    if not c then
+        separator = P(separator)
+        local other = C((1 - separator)^0)
+        c = Ct(separator^0 * other * (separator^1 * other)^0)
+        cache[separator] = c
+    end
+    return match(c,self)
+end
+
+--~ function lpeg.append(list,pp)
+--~     local p = pp
+--~     for l=1,#list do
+--~         if p then
+--~             p = p + P(list[l])
+--~         else
+--~             p = P(list[l])
+--~         end
+--~     end
+--~     return p
+--~ end
+
+--~ from roberto's site:
+
+local f1 = string.byte
+
+local function f2(s) local c1, c2         = f1(s,1,2) return   c1 * 64 + c2                       -    12416 end
+local function f3(s) local c1, c2, c3     = f1(s,1,3) return  (c1 * 64 + c2) * 64 + c3            -   925824 end
+local function f4(s) local c1, c2, c3, c4 = f1(s,1,4) return ((c1 * 64 + c2) * 64 + c3) * 64 + c4 - 63447168 end
+
+patterns.utf8byte = patterns.utf8one/f1 + patterns.utf8two/f2 + patterns.utf8three/f3 + patterns.utf8four/f4
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-table'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+table.join = table.concat
+
+local concat, sort, insert, remove = table.concat, table.sort, table.insert, table.remove
+local format, find, gsub, lower, dump, match = string.format, string.find, string.gsub, string.lower, string.dump, string.match
+local getmetatable, setmetatable = getmetatable, setmetatable
+local type, next, tostring, tonumber, ipairs = type, next, tostring, tonumber, ipairs
+
+-- Starting with version 5.2 Lua no longer provide ipairs, which makes
+-- sense. As we already used the for loop and # in most places the
+-- impact on ConTeXt was not that large; the remaining ipairs already
+-- have been replaced. In a similar fashio we also hardly used pairs.
+--
+-- Just in case, we provide the fallbacks as discussed in Programming
+-- in Lua (http://www.lua.org/pil/7.3.html):
+
+if not ipairs then
+
+    -- for k, v in ipairs(t) do                ... end
+    -- for k=1,#t            do local v = t[k] ... end
+
+    local function iterate(a,i)
+        i = i + 1
+        local v = a[i]
+        if v ~= nil then
+            return i, v --, nil
+        end
+    end
+
+    function ipairs(a)
+        return iterate, a, 0
+    end
+
+end
+
+if not pairs then
+
+    -- for k, v in pairs(t) do ... end
+    -- for k, v in next, t  do ... end
+
+    function pairs(t)
+        return next, t -- , nil
+    end
+
+end
+
+-- Also, unpack has been moved to the table table, and for compatiility
+-- reasons we provide both now.
+
+if not table.unpack then
+    table.unpack = _G.unpack
+elseif not unpack then
+    _G.unpack = table.unpack
+end
+
+-- extra functions, some might go (when not used)
+
+function table.strip(tab)
+    local lst = { }
+    for i=1,#tab do
+        local s = gsub(tab[i],"^%s*(.-)%s*$","%1")
+        if s == "" then
+            -- skip this one
+        else
+            lst[#lst+1] = s
+        end
+    end
+    return lst
+end
+
+function table.keys(t)
+    local k = { }
+    for key, _ in next, t do
+        k[#k+1] = key
+    end
+    return k
+end
+
+local function compare(a,b)
+    return (tostring(a) < tostring(b))
+end
+
+local function sortedkeys(tab)
+    local srt, kind = { }, 0 -- 0=unknown 1=string, 2=number 3=mixed
+    for key,_ in next, tab do
+        srt[#srt+1] = key
+        if kind == 3 then
+            -- no further check
+        else
+            local tkey = type(key)
+            if tkey == "string" then
+            --  if kind == 2 then kind = 3 else kind = 1 end
+                kind = (kind == 2 and 3) or 1
+            elseif tkey == "number" then
+            --  if kind == 1 then kind = 3 else kind = 2 end
+                kind = (kind == 1 and 3) or 2
+            else
+                kind = 3
+            end
+        end
+    end
+    if kind == 0 or kind == 3 then
+        sort(srt,compare)
+    else
+        sort(srt)
+    end
+    return srt
+end
+
+local function sortedhashkeys(tab) -- fast one
+    local srt = { }
+    for key,_ in next, tab do
+        srt[#srt+1] = key
+    end
+    sort(srt)
+    return srt
+end
+
+table.sortedkeys     = sortedkeys
+table.sortedhashkeys = sortedhashkeys
+
+function table.sortedhash(t)
+    local s = sortedhashkeys(t) -- maybe just sortedkeys
+    local n = 0
+    local function kv(s)
+        n = n + 1
+        local k = s[n]
+        return k, t[k]
+    end
+    return kv, s
+end
+
+table.sortedpairs = table.sortedhash
+
+function table.append(t, list)
+    for _,v in next, list do
+        insert(t,v)
+    end
+end
+
+function table.prepend(t, list)
+    for k,v in next, list do
+        insert(t,k,v)
+    end
+end
+
+function table.merge(t, ...) -- first one is target
+    t = t or {}
+    local lst = {...}
+    for i=1,#lst do
+        for k, v in next, lst[i] do
+            t[k] = v
+        end
+    end
+    return t
+end
+
+function table.merged(...)
+    local tmp, lst = { }, {...}
+    for i=1,#lst do
+        for k, v in next, lst[i] do
+            tmp[k] = v
+        end
+    end
+    return tmp
+end
+
+function table.imerge(t, ...)
+    local lst = {...}
+    for i=1,#lst do
+        local nst = lst[i]
+        for j=1,#nst do
+            t[#t+1] = nst[j]
+        end
+    end
+    return t
+end
+
+function table.imerged(...)
+    local tmp, lst = { }, {...}
+    for i=1,#lst do
+        local nst = lst[i]
+        for j=1,#nst do
+            tmp[#tmp+1] = nst[j]
+        end
+    end
+    return tmp
+end
+
+local function fastcopy(old) -- fast one
+    if old then
+        local new = { }
+        for k,v in next, old do
+            if type(v) == "table" then
+                new[k] = fastcopy(v) -- was just table.copy
+            else
+                new[k] = v
+            end
+        end
+        -- optional second arg
+        local mt = getmetatable(old)
+        if mt then
+            setmetatable(new,mt)
+        end
+        return new
+    else
+        return { }
+    end
+end
+
+local function copy(t, tables) -- taken from lua wiki, slightly adapted
+    tables = tables or { }
+    local tcopy = {}
+    if not tables[t] then
+        tables[t] = tcopy
+    end
+    for i,v in next, t do -- brrr, what happens with sparse indexed
+        if type(i) == "table" then
+            if tables[i] then
+                i = tables[i]
+            else
+                i = copy(i, tables)
+            end
+        end
+        if type(v) ~= "table" then
+            tcopy[i] = v
+        elseif tables[v] then
+            tcopy[i] = tables[v]
+        else
+            tcopy[i] = copy(v, tables)
+        end
+    end
+    local mt = getmetatable(t)
+    if mt then
+        setmetatable(tcopy,mt)
+    end
+    return tcopy
+end
+
+table.fastcopy = fastcopy
+table.copy     = copy
+
+-- roughly: copy-loop : unpack : sub == 0.9 : 0.4 : 0.45 (so in critical apps, use unpack)
+
+function table.sub(t,i,j)
+    return { unpack(t,i,j) }
+end
+
+function table.replace(a,b)
+    for k,v in next, b do
+        a[k] = v
+    end
+end
+
+-- slower than #t on indexed tables (#t only returns the size of the numerically indexed slice)
+
+function table.is_empty(t) -- obolete, use inline code instead
+    return not t or not next(t)
+end
+
+function table.one_entry(t) -- obolete, use inline code instead
+    local n = next(t)
+    return n and not next(t,n)
+end
+
+--~ function table.starts_at(t) -- obsolete, not nice anyway
+--~     return ipairs(t,1)(t,0)
+--~ end
+
+function table.tohash(t,value)
+    local h = { }
+    if t then
+        if value == nil then value = true end
+        for _, v in next, t do -- no ipairs here
+            h[v] = value
+        end
+    end
+    return h
+end
+
+function table.fromhash(t)
+    local h = { }
+    for k, v in next, t do -- no ipairs here
+        if v then h[#h+1] = k end
+    end
+    return h
+end
+
+--~ print(table.serialize(t), "\n")
+--~ print(table.serialize(t,"name"), "\n")
+--~ print(table.serialize(t,false), "\n")
+--~ print(table.serialize(t,true), "\n")
+--~ print(table.serialize(t,"name",true), "\n")
+--~ print(table.serialize(t,"name",true,true), "\n")
+
+table.serialize_functions = true
+table.serialize_compact   = true
+table.serialize_inline    = true
+
+local noquotes, hexify, handle, reduce, compact, inline, functions
+
+local reserved = table.tohash { -- intercept a language flaw, no reserved words as key
+    'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'if',
+    'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while',
+}
+
+local function simple_table(t)
+    if #t > 0 then
+        local n = 0
+        for _,v in next, t do
+            n = n + 1
+        end
+        if n == #t then
+            local tt = { }
+            for i=1,#t do
+                local v = t[i]
+                local tv = type(v)
+                if tv == "number" then
+                    if hexify then
+                        tt[#tt+1] = format("0x%04X",v)
+                    else
+                        tt[#tt+1] = tostring(v) -- tostring not needed
+                    end
+                elseif tv == "boolean" then
+                    tt[#tt+1] = tostring(v)
+                elseif tv == "string" then
+                    tt[#tt+1] = format("%q",v)
+                else
+                    tt = nil
+                    break
+                end
+            end
+            return tt
+        end
+    end
+    return nil
+end
+
+-- Because this is a core function of mkiv I moved some function calls
+-- inline.
+--
+-- twice as fast in a test:
+--
+-- local propername = lpeg.P(lpeg.R("AZ","az","__") * lpeg.R("09","AZ","az", "__")^0 * lpeg.P(-1) )
+
+-- problem: there no good number_to_string converter with the best resolution
+
+local function do_serialize(root,name,depth,level,indexed)
+    if level > 0 then
+        depth = depth .. " "
+        if indexed then
+            handle(format("%s{",depth))
+        elseif name then
+        --~ handle(format("%s%s={",depth,key(name)))
+            if type(name) == "number" then -- or find(k,"^%d+$") then
+                if hexify then
+                    handle(format("%s[0x%04X]={",depth,name))
+                else
+                    handle(format("%s[%s]={",depth,name))
+                end
+            elseif noquotes and not reserved[name] and find(name,"^%a[%w%_]*$") then
+                handle(format("%s%s={",depth,name))
+            else
+                handle(format("%s[%q]={",depth,name))
+            end
+        else
+            handle(format("%s{",depth))
+        end
+    end
+    -- we could check for k (index) being number (cardinal)
+    if root and next(root) then
+        local first, last = nil, 0 -- #root cannot be trusted here (will be ok in 5.2 when ipairs is gone)
+        if compact then
+            -- NOT: for k=1,#root do (we need to quit at nil)
+            for k,v in ipairs(root) do -- can we use next?
+                if not first then first = k end
+                last = last + 1
+            end
+        end
+        local sk = sortedkeys(root)
+        for i=1,#sk do
+            local k = sk[i]
+            local v = root[k]
+            --~ if v == root then
+                -- circular
+            --~ else
+            local t = type(v)
+            if compact and first and type(k) == "number" and k >= first and k <= last then
+                if t == "number" then
+                    if hexify then
+                        handle(format("%s 0x%04X,",depth,v))
+                    else
+                        handle(format("%s %s,",depth,v)) -- %.99g
+                    end
+                elseif t == "string" then
+                    if reduce and tonumber(v) then
+                        handle(format("%s %s,",depth,v))
+                    else
+                        handle(format("%s %q,",depth,v))
+                    end
+                elseif t == "table" then
+                    if not next(v) then
+                        handle(format("%s {},",depth))
+                    elseif inline then -- and #t > 0
+                        local st = simple_table(v)
+                        if st then
+                            handle(format("%s { %s },",depth,concat(st,", ")))
+                        else
+                            do_serialize(v,k,depth,level+1,true)
+                        end
+                    else
+                        do_serialize(v,k,depth,level+1,true)
+                    end
+                elseif t == "boolean" then
+                    handle(format("%s %s,",depth,tostring(v)))
+                elseif t == "function" then
+                    if functions then
+                        handle(format('%s loadstring(%q),',depth,dump(v)))
+                    else
+                        handle(format('%s "function",',depth))
+                    end
+                else
+                    handle(format("%s %q,",depth,tostring(v)))
+                end
+            elseif k == "__p__" then -- parent
+                if false then
+                    handle(format("%s __p__=nil,",depth))
+                end
+            elseif t == "number" then
+            --~ if hexify then
+            --~     handle(format("%s %s=0x%04X,",depth,key(k),v))
+            --~ else
+            --~     handle(format("%s %s=%s,",depth,key(k),v)) -- %.99g
+            --~ end
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s [%s]=%s,",depth,k,v)) -- %.99g
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    if hexify then
+                        handle(format("%s %s=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s %s=%s,",depth,k,v)) -- %.99g
+                    end
+                else
+                    if hexify then
+                        handle(format("%s [%q]=0x%04X,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%s,",depth,k,v)) -- %.99g
+                    end
+                end
+            elseif t == "string" then
+                if reduce and tonumber(v) then
+                --~ handle(format("%s %s=%s,",depth,key(k),v))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=%s,",depth,k,v))
+                        else
+                            handle(format("%s [%s]=%s,",depth,k,v))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=%s,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%s,",depth,k,v))
+                    end
+                else
+                --~ handle(format("%s %s=%q,",depth,key(k),v))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=%q,",depth,k,v))
+                        else
+                            handle(format("%s [%s]=%q,",depth,k,v))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=%q,",depth,k,v))
+                    else
+                        handle(format("%s [%q]=%q,",depth,k,v))
+                    end
+                end
+            elseif t == "table" then
+                if not next(v) then
+                    --~ handle(format("%s %s={},",depth,key(k)))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]={},",depth,k))
+                        else
+                            handle(format("%s [%s]={},",depth,k))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s={},",depth,k))
+                    else
+                        handle(format("%s [%q]={},",depth,k))
+                    end
+                elseif inline then
+                    local st = simple_table(v)
+                    if st then
+                    --~ handle(format("%s %s={ %s },",depth,key(k),concat(st,", ")))
+                        if type(k) == "number" then -- or find(k,"^%d+$") then
+                            if hexify then
+                                handle(format("%s [0x%04X]={ %s },",depth,k,concat(st,", ")))
+                            else
+                                handle(format("%s [%s]={ %s },",depth,k,concat(st,", ")))
+                            end
+                        elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                            handle(format("%s %s={ %s },",depth,k,concat(st,", ")))
+                        else
+                            handle(format("%s [%q]={ %s },",depth,k,concat(st,", ")))
+                        end
+                    else
+                        do_serialize(v,k,depth,level+1)
+                    end
+                else
+                    do_serialize(v,k,depth,level+1)
+                end
+            elseif t == "boolean" then
+            --~ handle(format("%s %s=%s,",depth,key(k),tostring(v)))
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=%s,",depth,k,tostring(v)))
+                    else
+                        handle(format("%s [%s]=%s,",depth,k,tostring(v)))
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    handle(format("%s %s=%s,",depth,k,tostring(v)))
+                else
+                    handle(format("%s [%q]=%s,",depth,k,tostring(v)))
+                end
+            elseif t == "function" then
+                if functions then
+                    --~ handle(format('%s %s=loadstring(%q),',depth,key(k),dump(v)))
+                    if type(k) == "number" then -- or find(k,"^%d+$") then
+                        if hexify then
+                            handle(format("%s [0x%04X]=loadstring(%q),",depth,k,dump(v)))
+                        else
+                            handle(format("%s [%s]=loadstring(%q),",depth,k,dump(v)))
+                        end
+                    elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                        handle(format("%s %s=loadstring(%q),",depth,k,dump(v)))
+                    else
+                        handle(format("%s [%q]=loadstring(%q),",depth,k,dump(v)))
+                    end
+                end
+            else
+                --~ handle(format("%s %s=%q,",depth,key(k),tostring(v)))
+                if type(k) == "number" then -- or find(k,"^%d+$") then
+                    if hexify then
+                        handle(format("%s [0x%04X]=%q,",depth,k,tostring(v)))
+                    else
+                        handle(format("%s [%s]=%q,",depth,k,tostring(v)))
+                    end
+                elseif noquotes and not reserved[k] and find(k,"^%a[%w%_]*$") then
+                    handle(format("%s %s=%q,",depth,k,tostring(v)))
+                else
+                    handle(format("%s [%q]=%q,",depth,k,tostring(v)))
+                end
+            end
+            --~ end
+        end
+    end
+   if level > 0 then
+        handle(format("%s},",depth))
+    end
+end
+
+-- replacing handle by a direct t[#t+1] = ... (plus test) is not much
+-- faster (0.03 on 1.00 for zapfino.tma)
+
+local function serialize(root,name,_handle,_reduce,_noquotes,_hexify)
+    noquotes = _noquotes
+    hexify = _hexify
+    handle = _handle or print
+    reduce = _reduce or false
+    compact = table.serialize_compact
+    inline  = compact and table.serialize_inline
+    functions = table.serialize_functions
+    local tname = type(name)
+    if tname == "string" then
+        if name == "return" then
+            handle("return {")
+        else
+            handle(name .. "={")
+        end
+    elseif tname == "number" then
+        if hexify then
+            handle(format("[0x%04X]={",name))
+        else
+            handle("[" .. name .. "]={")
+        end
+    elseif tname == "boolean" then
+        if name then
+            handle("return {")
+        else
+            handle("{")
+        end
+    else
+        handle("t={")
+    end
+    if root and next(root) then
+        do_serialize(root,name,"",0,indexed)
+    end
+    handle("}")
+end
+
+--~ name:
+--~
+--~ true     : return     { }
+--~ false    :            { }
+--~ nil      : t        = { }
+--~ string   : string   = { }
+--~ 'return' : return     { }
+--~ number   : [number] = { }
+
+function table.serialize(root,name,reduce,noquotes,hexify)
+    local t = { }
+    local function flush(s)
+        t[#t+1] = s
+    end
+    serialize(root,name,flush,reduce,noquotes,hexify)
+    return concat(t,"\n")
+end
+
+function table.tohandle(handle,root,name,reduce,noquotes,hexify)
+    serialize(root,name,handle,reduce,noquotes,hexify)
+end
+
+-- sometimes tables are real use (zapfino extra pro is some 85M) in which
+-- case a stepwise serialization is nice; actually, we could consider:
+--
+-- for line in table.serializer(root,name,reduce,noquotes) do
+--    ...(line)
+-- end
+--
+-- so this is on the todo list
+
+table.tofile_maxtab = 2*1024
+
+function table.tofile(filename,root,name,reduce,noquotes,hexify)
+    local f = io.open(filename,'w')
+    if f then
+        local maxtab = table.tofile_maxtab
+        if maxtab > 1 then
+            local t = { }
+            local function flush(s)
+                t[#t+1] = s
+                if #t > maxtab then
+                    f:write(concat(t,"\n"),"\n") -- hm, write(sometable) should be nice
+                    t = { }
+                end
+            end
+            serialize(root,name,flush,reduce,noquotes,hexify)
+            f:write(concat(t,"\n"),"\n")
+        else
+            local function flush(s)
+                f:write(s,"\n")
+            end
+            serialize(root,name,flush,reduce,noquotes,hexify)
+        end
+        f:close()
+    end
+end
+
+local function flatten(t,f,complete) -- is this used? meybe a variant with next, ...
+    for i=1,#t do
+        local v = t[i]
+        if type(v) == "table" then
+            if complete or type(v[1]) == "table" then
+                flatten(v,f,complete)
+            else
+                f[#f+1] = v
+            end
+        else
+            f[#f+1] = v
+        end
+    end
+end
+
+function table.flatten(t)
+    local f = { }
+    flatten(t,f,true)
+    return f
+end
+
+function table.unnest(t) -- bad name
+    local f = { }
+    flatten(t,f,false)
+    return f
+end
+
+table.flatten_one_level = table.unnest
+
+-- a better one:
+
+local function flattened(t,f)
+    if not f then
+        f = { }
+    end
+    for k, v in next, t do
+        if type(v) == "table" then
+            flattened(v,f)
+        else
+            f[k] = v
+        end
+    end
+    return f
+end
+
+table.flattened = flattened
+
+-- the next three may disappear
+
+function table.remove_value(t,value) -- todo: n
+    if value then
+        for i=1,#t do
+            if t[i] == value then
+                remove(t,i)
+                -- remove all, so no: return
+            end
+        end
+    end
+end
+
+function table.insert_before_value(t,value,str)
+    if str then
+        if value then
+            for i=1,#t do
+                if t[i] == value then
+                    insert(t,i,str)
+                    return
+                end
+            end
+        end
+        insert(t,1,str)
+    elseif value then
+        insert(t,1,value)
+    end
+end
+
+function table.insert_after_value(t,value,str)
+    if str then
+        if value then
+            for i=1,#t do
+                if t[i] == value then
+                    insert(t,i+1,str)
+                    return
+                end
+            end
+        end
+        t[#t+1] = str
+    elseif value then
+        t[#t+1] = value
+    end
+end
+
+local function are_equal(a,b,n,m) -- indexed
+    if a and b and #a == #b then
+        n = n or 1
+        m = m or #a
+        for i=n,m do
+            local ai, bi = a[i], b[i]
+            if ai==bi then
+                -- same
+            elseif type(ai)=="table" and type(bi)=="table" then
+                if not are_equal(ai,bi) then
+                    return false
+                end
+            else
+                return false
+            end
+        end
+        return true
+    else
+        return false
+    end
+end
+
+local function identical(a,b) -- assumes same structure
+    for ka, va in next, a do
+        local vb = b[k]
+        if va == vb then
+            -- same
+        elseif type(va) == "table" and  type(vb) == "table" then
+            if not identical(va,vb) then
+                return false
+            end
+        else
+            return false
+        end
+    end
+    return true
+end
+
+table.are_equal = are_equal
+table.identical = identical
+
+-- maybe also make a combined one
+
+function table.compact(t)
+    if t then
+        for k,v in next, t do
+            if not next(v) then
+                t[k] = nil
+            end
+        end
+    end
+end
+
+function table.contains(t, v)
+    if t then
+        for i=1, #t do
+            if t[i] == v then
+                return i
+            end
+        end
+    end
+    return false
+end
+
+function table.count(t)
+    local n, e = 0, next(t)
+    while e do
+        n, e = n + 1, next(t,e)
+    end
+    return n
+end
+
+function table.swapped(t)
+    local s = { }
+    for k, v in next, t do
+        s[v] = k
+    end
+    return s
+end
+
+--~ function table.are_equal(a,b)
+--~     return table.serialize(a) == table.serialize(b)
+--~ end
+
+function table.clone(t,p) -- t is optional or nil or table
+    if not p then
+        t, p = { }, t or { }
+    elseif not t then
+        t = { }
+    end
+    setmetatable(t, { __index = function(_,key) return p[key] end }) -- why not __index = p ?
+    return t
+end
+
+function table.hexed(t,seperator)
+    local tt = { }
+    for i=1,#t do tt[i] = format("0x%04X",t[i]) end
+    return concat(tt,seperator or " ")
+end
+
+function table.reverse_hash(h)
+    local r = { }
+    for k,v in next, h do
+        r[v] = lower(gsub(k," ",""))
+    end
+    return r
+end
+
+function table.reverse(t)
+    local tt = { }
+    if #t > 0 then
+        for i=#t,1,-1 do
+            tt[#tt+1] = t[i]
+        end
+    end
+    return tt
+end
+
+function table.insert_before_value(t,value,extra)
+    for i=1,#t do
+        if t[i] == extra then
+            remove(t,i)
+        end
+    end
+    for i=1,#t do
+        if t[i] == value then
+            insert(t,i,extra)
+            return
+        end
+    end
+    insert(t,1,extra)
+end
+
+function table.insert_after_value(t,value,extra)
+    for i=1,#t do
+        if t[i] == extra then
+            remove(t,i)
+        end
+    end
+    for i=1,#t do
+        if t[i] == value then
+            insert(t,i+1,extra)
+            return
+        end
+    end
+    insert(t,#t+1,extra)
+end
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-io'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local byte, find, gsub = string.byte, string.find, string.gsub
+
+if string.find(os.getenv("PATH"),";") then
+    io.fileseparator, io.pathseparator = "\\", ";"
+else
+    io.fileseparator, io.pathseparator = "/" , ":"
+end
+
+function io.loaddata(filename,textmode)
+    local f = io.open(filename,(textmode and 'r') or 'rb')
+    if f then
+    --  collectgarbage("step") -- sometimes makes a big difference in mem consumption
+        local data = f:read('*all')
+    --  garbagecollector.check(data)
+        f:close()
+        return data
+    else
+        return nil
+    end
+end
+
+function io.savedata(filename,data,joiner)
+    local f = io.open(filename,"wb")
+    if f then
+        if type(data) == "table" then
+            f:write(table.join(data,joiner or ""))
+        elseif type(data) == "function" then
+            data(f)
+        else
+            f:write(data or "")
+        end
+        f:close()
+        return true
+    else
+        return false
+    end
+end
+
+function io.exists(filename)
+    local f = io.open(filename)
+    if f == nil then
+        return false
+    else
+        assert(f:close())
+        return true
+    end
+end
+
+function io.size(filename)
+    local f = io.open(filename)
+    if f == nil then
+        return 0
+    else
+        local s = f:seek("end")
+        assert(f:close())
+        return s
+    end
+end
+
+function io.noflines(f)
+    local n = 0
+    for _ in f:lines() do
+        n = n + 1
+    end
+    f:seek('set',0)
+    return n
+end
+
+local nextchar = {
+    [ 4] = function(f)
+        return f:read(1,1,1,1)
+    end,
+    [ 2] = function(f)
+        return f:read(1,1)
+    end,
+    [ 1] = function(f)
+        return f:read(1)
+    end,
+    [-2] = function(f)
+        local a, b = f:read(1,1)
+        return b, a
+    end,
+    [-4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        return d, c, b, a
+    end
+}
+
+function io.characters(f,n)
+    if f then
+        return nextchar[n or 1], f
+    else
+        return nil, nil
+    end
+end
+
+local nextbyte = {
+    [4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        if d then
+            return byte(a), byte(b), byte(c), byte(d)
+        else
+            return nil, nil, nil, nil
+        end
+    end,
+    [2] = function(f)
+        local a, b = f:read(1,1)
+        if b then
+            return byte(a), byte(b)
+        else
+            return nil, nil
+        end
+    end,
+    [1] = function (f)
+        local a = f:read(1)
+        if a then
+            return byte(a)
+        else
+            return nil
+        end
+    end,
+    [-2] = function (f)
+        local a, b = f:read(1,1)
+        if b then
+            return byte(b), byte(a)
+        else
+            return nil, nil
+        end
+    end,
+    [-4] = function(f)
+        local a, b, c, d = f:read(1,1,1,1)
+        if d then
+            return byte(d), byte(c), byte(b), byte(a)
+        else
+            return nil, nil, nil, nil
+        end
+    end
+}
+
+function io.bytes(f,n)
+    if f then
+        return nextbyte[n or 1], f
+    else
+        return nil, nil
+    end
+end
+
+function io.ask(question,default,options)
+    while true do
+        io.write(question)
+        if options then
+            io.write(string.format(" [%s]",table.concat(options,"|")))
+        end
+        if default then
+            io.write(string.format(" [%s]",default))
+        end
+        io.write(string.format(" "))
+        local answer = io.read()
+        answer = gsub(answer,"^%s*(.*)%s*$","%1")
+        if answer == "" and default then
+            return default
+        elseif not options then
+            return answer
+        else
+            for k=1,#options do
+                if options[k] == answer then
+                    return answer
+                end
+            end
+            local pattern = "^" .. answer
+            for k=1,#options do
+                local v = options[k]
+                if find(v,pattern) then
+                    return v
+                end
+            end
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-number'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local tostring = tostring
+local format, floor, insert, match = string.format, math.floor, table.insert, string.match
+local lpegmatch = lpeg.match
+
+number = number or { }
+
+-- a,b,c,d,e,f = number.toset(100101)
+
+function number.toset(n)
+    return match(tostring(n),"(.?)(.?)(.?)(.?)(.?)(.?)(.?)(.?)")
+end
+
+function number.toevenhex(n)
+    local s = format("%X",n)
+    if #s % 2 == 0 then
+        return s
+    else
+        return "0" .. s
+    end
+end
+
+-- the lpeg way is slower on 8 digits, but faster on 4 digits, some 7.5%
+-- on
+--
+-- for i=1,1000000 do
+--     local a,b,c,d,e,f,g,h = number.toset(12345678)
+--     local a,b,c,d         = number.toset(1234)
+--     local a,b,c           = number.toset(123)
+-- end
+--
+-- of course dedicated "(.)(.)(.)(.)" matches are even faster
+
+local one = lpeg.C(1-lpeg.S(''))^1
+
+function number.toset(n)
+    return lpegmatch(one,tostring(n))
+end
+
+function number.bits(n,zero)
+    local t, i = { }, (zero and 0) or 1
+    while n > 0 do
+        local m = n % 2
+        if m > 0 then
+            insert(t,1,i)
+        end
+        n = floor(n/2)
+        i = i + 1
+    end
+    return t
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-set'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+set = set or { }
+
+local nums   = { }
+local tabs   = { }
+local concat = table.concat
+local next, type = next, type
+
+set.create = table.tohash
+
+function set.tonumber(t)
+    if next(t) then
+        local s = ""
+    --  we could save mem by sorting, but it slows down
+        for k, v in next, t do
+            if v then
+            --  why bother about the leading space
+                s = s .. " " .. k
+            end
+        end
+        local n = nums[s]
+        if not n then
+            n = #tabs + 1
+            tabs[n] = t
+            nums[s] = n
+        end
+        return n
+    else
+        return 0
+    end
+end
+
+function set.totable(n)
+    if n == 0 then
+        return { }
+    else
+        return tabs[n] or { }
+    end
+end
+
+function set.tolist(n)
+    if n == 0 or not tabs[n] then
+        return ""
+    else
+        local t = { }
+        for k, v in next, tabs[n] do
+            if v then
+                t[#t+1] = k
+            end
+        end
+        return concat(t," ")
+    end
+end
+
+function set.contains(n,s)
+    if type(n) == "table" then
+        return n[s]
+    elseif n == 0 then
+        return false
+    else
+        local t = tabs[n]
+        return t and t[s]
+    end
+end
+
+--~ local c = set.create{'aap','noot','mies'}
+--~ local s = set.tonumber(c)
+--~ local t = set.totable(s)
+--~ print(t['aap'])
+--~ local c = set.create{'zus','wim','jet'}
+--~ local s = set.tonumber(c)
+--~ local t = set.totable(s)
+--~ print(t['aap'])
+--~ print(t['jet'])
+--~ print(set.contains(t,'jet'))
+--~ print(set.contains(t,'aap'))
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-os'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- maybe build io.flush in os.execute
+
+local find, format, gsub = string.find, string.format, string.gsub
+local random, ceil = math.random, math.ceil
+
+local execute, spawn, exec, ioflush = os.execute, os.spawn or os.execute, os.exec or os.execute, io.flush
+
+function os.execute(...) ioflush() return execute(...) end
+function os.spawn  (...) ioflush() return spawn  (...) end
+function os.exec   (...) ioflush() return exec   (...) end
+
+function os.resultof(command)
+    ioflush() -- else messed up logging
+    local handle = io.popen(command,"r")
+    if not handle then
+    --  print("unknown command '".. command .. "' in os.resultof")
+        return ""
+    else
+        return handle:read("*all") or ""
+    end
+end
+
+--~ os.type     : windows | unix (new, we already guessed os.platform)
+--~ os.name     : windows | msdos | linux | macosx | solaris | .. | generic (new)
+--~ os.platform : extended os.name with architecture
+
+if not io.fileseparator then
+    if find(os.getenv("PATH"),";") then
+        io.fileseparator, io.pathseparator, os.type = "\\", ";", os.type or "mswin"
+    else
+        io.fileseparator, io.pathseparator, os.type = "/" , ":", os.type or "unix"
+    end
+end
+
+os.type = os.type or (io.pathseparator == ";"       and "windows") or "unix"
+os.name = os.name or (os.type          == "windows" and "mswin"  ) or "linux"
+
+if os.type == "windows" then
+    os.libsuffix, os.binsuffix = 'dll', 'exe'
+else
+    os.libsuffix, os.binsuffix = 'so', ''
+end
+
+function os.launch(str)
+    if os.type == "windows" then
+        os.execute("start " .. str) -- os.spawn ?
+    else
+        os.execute(str .. " &")     -- os.spawn ?
+    end
+end
+
+if not os.times then
+    -- utime  = user time
+    -- stime  = system time
+    -- cutime = children user time
+    -- cstime = children system time
+    function os.times()
+        return {
+            utime  = os.gettimeofday(), -- user
+            stime  = 0,                 -- system
+            cutime = 0,                 -- children user
+            cstime = 0,                 -- children system
+        }
+    end
+end
+
+os.gettimeofday = os.gettimeofday or os.clock
+
+local startuptime = os.gettimeofday()
+
+function os.runtime()
+    return os.gettimeofday() - startuptime
+end
+
+--~ print(os.gettimeofday()-os.time())
+--~ os.sleep(1.234)
+--~ print (">>",os.runtime())
+--~ print(os.date("%H:%M:%S",os.gettimeofday()))
+--~ print(os.date("%H:%M:%S",os.time()))
+
+-- no need for function anymore as we have more clever code and helpers now
+-- this metatable trickery might as well disappear
+
+os.resolvers = os.resolvers or { }
+
+local resolvers = os.resolvers
+
+local osmt = getmetatable(os) or { __index = function(t,k) t[k] = "unset" return "unset" end } -- maybe nil
+local osix = osmt.__index
+
+osmt.__index = function(t,k)
+    return (resolvers[k] or osix)(t,k)
+end
+
+setmetatable(os,osmt)
+
+if not os.setenv then
+
+    -- we still store them but they won't be seen in
+    -- child processes although we might pass them some day
+    -- using command concatination
+
+    local env, getenv = { }, os.getenv
+
+    function os.setenv(k,v)
+        env[k] = v
+    end
+
+    function os.getenv(k)
+        return env[k] or getenv(k)
+    end
+
+end
+
+-- we can use HOSTTYPE on some platforms
+
+local name, platform = os.name or "linux", os.getenv("MTX_PLATFORM") or ""
+
+local function guess()
+    local architecture = os.resultof("uname -m") or ""
+    if architecture ~= "" then
+        return architecture
+    end
+    architecture = os.getenv("HOSTTYPE") or ""
+    if architecture ~= "" then
+        return architecture
+    end
+    return os.resultof("echo $HOSTTYPE") or ""
+end
+
+if platform ~= "" then
+
+    os.platform = platform
+
+elseif os.type == "windows" then
+
+    -- we could set the variable directly, no function needed here
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.getenv("PROCESSOR_ARCHITECTURE") or ""
+        if find(architecture,"AMD64") then
+            platform = "mswin-64"
+        else
+            platform = "mswin"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "linux" then
+
+    function os.resolvers.platform(t,k)
+        -- we sometims have HOSTTYPE set so let's check that first
+        local platform, architecture = "", os.getenv("HOSTTYPE") or os.resultof("uname -m") or ""
+        if find(architecture,"x86_64") then
+            platform = "linux-64"
+        elseif find(architecture,"ppc") then
+            platform = "linux-ppc"
+        else
+            platform = "linux"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "macosx" then
+
+    --[[
+        Identifying the architecture of OSX is quite a mess and this
+        is the best we can come up with. For some reason $HOSTTYPE is
+        a kind of pseudo environment variable, not known to the current
+        environment. And yes, uname cannot be trusted either, so there
+        is a change that you end up with a 32 bit run on a 64 bit system.
+        Also, some proper 64 bit intel macs are too cheap (low-end) and
+        therefore not permitted to run the 64 bit kernel.
+      ]]--
+
+    function os.resolvers.platform(t,k)
+     -- local platform, architecture = "", os.getenv("HOSTTYPE") or ""
+     -- if architecture == "" then
+     --     architecture = os.resultof("echo $HOSTTYPE") or ""
+     -- end
+        local platform, architecture = "", os.resultof("echo $HOSTTYPE") or ""
+        if architecture == "" then
+         -- print("\nI have no clue what kind of OSX you're running so let's assume an 32 bit intel.\n")
+            platform = "osx-intel"
+        elseif find(architecture,"i386") then
+            platform = "osx-intel"
+        elseif find(architecture,"x86_64") then
+            platform = "osx-64"
+        else
+            platform = "osx-ppc"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "sunos" then
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.resultof("uname -m") or ""
+        if find(architecture,"sparc") then
+            platform = "solaris-sparc"
+        else -- if architecture == 'i86pc'
+            platform = "solaris-intel"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "freebsd" then
+
+    function os.resolvers.platform(t,k)
+        local platform, architecture = "", os.resultof("uname -m") or ""
+        if find(architecture,"amd64") then
+            platform = "freebsd-amd64"
+        else
+            platform = "freebsd"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+elseif name == "kfreebsd" then
+
+    function os.resolvers.platform(t,k)
+        -- we sometims have HOSTTYPE set so let's check that first
+        local platform, architecture = "", os.getenv("HOSTTYPE") or os.resultof("uname -m") or ""
+        if find(architecture,"x86_64") then
+            platform = "kfreebsd-64"
+        else
+            platform = "kfreebsd-i386"
+        end
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+else
+
+    -- platform = "linux"
+    -- os.setenv("MTX_PLATFORM",platform)
+    -- os.platform = platform
+
+    function os.resolvers.platform(t,k)
+        local platform = "linux"
+        os.setenv("MTX_PLATFORM",platform)
+        os.platform = platform
+        return platform
+    end
+
+end
+
+-- beware, we set the randomseed
+
+-- from wikipedia: Version 4 UUIDs use a scheme relying only on random numbers. This algorithm sets the
+-- version number as well as two reserved bits. All other bits are set using a random or pseudorandom
+-- data source. Version 4 UUIDs have the form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx with hexadecimal
+-- digits x and hexadecimal digits 8, 9, A, or B for y. e.g. f47ac10b-58cc-4372-a567-0e02b2c3d479.
+--
+-- as we don't call this function too often there is not so much risk on repetition
+
+local t = { 8, 9, "a", "b" }
+
+function os.uuid()
+    return format("%04x%04x-4%03x-%s%03x-%04x-%04x%04x%04x",
+        random(0xFFFF),random(0xFFFF),
+        random(0x0FFF),
+        t[ceil(random(4))] or 8,random(0x0FFF),
+        random(0xFFFF),
+        random(0xFFFF),random(0xFFFF),random(0xFFFF)
+    )
+end
+
+local d
+
+function os.timezone(delta)
+    d = d or tonumber(tonumber(os.date("%H")-os.date("!%H")))
+    if delta then
+        if d > 0 then
+            return format("+%02i:00",d)
+        else
+            return format("-%02i:00",-d)
+        end
+    else
+        return 1
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-file'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- needs a cleanup
+
+file = file or { }
+
+local concat = table.concat
+local find, gmatch, match, gsub, sub, char = string.find, string.gmatch, string.match, string.gsub, string.sub, string.char
+local lpegmatch = lpeg.match
+
+function file.removesuffix(filename)
+    return (gsub(filename,"%.[%a%d]+$",""))
+end
+
+function file.addsuffix(filename, suffix)
+    if not suffix or suffix == "" then
+        return filename
+    elseif not find(filename,"%.[%a%d]+$") then
+        return filename .. "." .. suffix
+    else
+        return filename
+    end
+end
+
+function file.replacesuffix(filename, suffix)
+    return (gsub(filename,"%.[%a%d]+$","")) .. "." .. suffix
+end
+
+function file.dirname(name,default)
+    return match(name,"^(.+)[/\\].-$") or (default or "")
+end
+
+function file.basename(name)
+    return match(name,"^.+[/\\](.-)$") or name
+end
+
+function file.nameonly(name)
+    return (gsub(match(name,"^.+[/\\](.-)$") or name,"%..*$",""))
+end
+
+function file.extname(name,default)
+    return match(name,"^.+%.([^/\\]-)$") or default or ""
+end
+
+file.suffix = file.extname
+
+--~ function file.join(...)
+--~     local pth = concat({...},"/")
+--~     pth = gsub(pth,"\\","/")
+--~     local a, b = match(pth,"^(.*://)(.*)$")
+--~     if a and b then
+--~         return a .. gsub(b,"//+","/")
+--~     end
+--~     a, b = match(pth,"^(//)(.*)$")
+--~     if a and b then
+--~         return a .. gsub(b,"//+","/")
+--~     end
+--~     return (gsub(pth,"//+","/"))
+--~ end
+
+local trick_1 = char(1)
+local trick_2 = "^" .. trick_1 .. "/+"
+
+function file.join(...)
+    local lst = { ... }
+    local a, b = lst[1], lst[2]
+    if a == "" then
+        lst[1] = trick_1
+    elseif b and find(a,"^/+$") and find(b,"^/") then
+        lst[1] = ""
+        lst[2] = gsub(b,"^/+","")
+    end
+    local pth = concat(lst,"/")
+    pth = gsub(pth,"\\","/")
+    local a, b = match(pth,"^(.*://)(.*)$")
+    if a and b then
+        return a .. gsub(b,"//+","/")
+    end
+    a, b = match(pth,"^(//)(.*)$")
+    if a and b then
+        return a .. gsub(b,"//+","/")
+    end
+    pth = gsub(pth,trick_2,"")
+    return (gsub(pth,"//+","/"))
+end
+
+--~ print(file.join("//","/y"))
+--~ print(file.join("/","/y"))
+--~ print(file.join("","/y"))
+--~ print(file.join("/x/","/y"))
+--~ print(file.join("x/","/y"))
+--~ print(file.join("http://","/y"))
+--~ print(file.join("http://a","/y"))
+--~ print(file.join("http:///a","/y"))
+--~ print(file.join("//nas-1","/y"))
+
+function file.iswritable(name)
+    local a = lfs.attributes(name) or lfs.attributes(file.dirname(name,"."))
+    return a and sub(a.permissions,2,2) == "w"
+end
+
+function file.isreadable(name)
+    local a = lfs.attributes(name)
+    return a and sub(a.permissions,1,1) == "r"
+end
+
+file.is_readable = file.isreadable
+file.is_writable = file.iswritable
+
+-- todo: lpeg
+
+--~ function file.split_path(str)
+--~     local t = { }
+--~     str = gsub(str,"\\", "/")
+--~     str = gsub(str,"(%a):([;/])", "%1\001%2")
+--~     for name in gmatch(str,"([^;:]+)") do
+--~         if name ~= "" then
+--~             t[#t+1] = gsub(name,"\001",":")
+--~         end
+--~     end
+--~     return t
+--~ end
+
+local checkedsplit = string.checkedsplit
+
+function file.split_path(str,separator)
+    str = gsub(str,"\\","/")
+    return checkedsplit(str,separator or io.pathseparator)
+end
+
+function file.join_path(tab)
+    return concat(tab,io.pathseparator) -- can have trailing //
+end
+
+-- we can hash them weakly
+
+function file.collapse_path(str)
+    str = gsub(str,"\\","/")
+    if find(str,"/") then
+        str = gsub(str,"^%./",(gsub(lfs.currentdir(),"\\","/")) .. "/") -- ./xx in qualified
+        str = gsub(str,"/%./","/")
+        local n, m = 1, 1
+        while n > 0 or m > 0 do
+            str, n = gsub(str,"[^/%.]+/%.%.$","")
+            str, m = gsub(str,"[^/%.]+/%.%./","")
+        end
+        str = gsub(str,"([^/])/$","%1")
+    --  str = gsub(str,"^%./","") -- ./xx in qualified
+        str = gsub(str,"/%.$","")
+    end
+    if str == "" then str = "." end
+    return str
+end
+
+--~ print(file.collapse_path("/a"))
+--~ print(file.collapse_path("a/./b/.."))
+--~ print(file.collapse_path("a/aa/../b/bb"))
+--~ print(file.collapse_path("a/../.."))
+--~ print(file.collapse_path("a/.././././b/.."))
+--~ print(file.collapse_path("a/./././b/.."))
+--~ print(file.collapse_path("a/b/c/../.."))
+
+function file.robustname(str)
+    return (gsub(str,"[^%a%d%/%-%.\\]+","-"))
+end
+
+file.readdata = io.loaddata
+file.savedata = io.savedata
+
+function file.copy(oldname,newname)
+    file.savedata(newname,io.loaddata(oldname))
+end
+
+-- lpeg variants, slightly faster, not always
+
+--~ local period    = lpeg.P(".")
+--~ local slashes   = lpeg.S("\\/")
+--~ local noperiod  = 1-period
+--~ local noslashes = 1-slashes
+--~ local name      = noperiod^1
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.C(noperiod^1) * -1
+
+--~ function file.extname(name)
+--~     return lpegmatch(pattern,name) or ""
+--~ end
+
+--~ local pattern = lpeg.Cs(((period * noperiod^1 * -1)/"" + 1)^1)
+
+--~ function file.removesuffix(name)
+--~     return lpegmatch(pattern,name)
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^1 * lpeg.C(noslashes^1) * -1
+
+--~ function file.basename(name)
+--~     return lpegmatch(pattern,name) or name
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^1 * lpeg.Cp() * noslashes^1 * -1
+
+--~ function file.dirname(name)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return sub(name,1,p-2)
+--~     else
+--~         return ""
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.Cp() * noperiod^1 * -1
+
+--~ function file.addsuffix(name, suffix)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return name
+--~     else
+--~         return name .. "." .. suffix
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * (noperiod^1 * period)^1 * lpeg.Cp() * noperiod^1 * -1
+
+--~ function file.replacesuffix(name,suffix)
+--~     local p = lpegmatch(pattern,name)
+--~     if p then
+--~         return sub(name,1,p-2) .. "." .. suffix
+--~     else
+--~         return name .. "." .. suffix
+--~     end
+--~ end
+
+--~ local pattern = (noslashes^0 * slashes)^0 * lpeg.Cp() * ((noperiod^1 * period)^1 * lpeg.Cp() + lpeg.P(true)) * noperiod^1 * -1
+
+--~ function file.nameonly(name)
+--~     local a, b = lpegmatch(pattern,name)
+--~     if b then
+--~         return sub(name,a,b-2)
+--~     elseif a then
+--~         return sub(name,a)
+--~     else
+--~         return name
+--~     end
+--~ end
+
+--~ local test = file.extname
+--~ local test = file.basename
+--~ local test = file.dirname
+--~ local test = file.addsuffix
+--~ local test = file.replacesuffix
+--~ local test = file.nameonly
+
+--~ print(1,test("./a/b/c/abd.def.xxx","!!!"))
+--~ print(2,test("./../b/c/abd.def.xxx","!!!"))
+--~ print(3,test("a/b/c/abd.def.xxx","!!!"))
+--~ print(4,test("a/b/c/def.xxx","!!!"))
+--~ print(5,test("a/b/c/def","!!!"))
+--~ print(6,test("def","!!!"))
+--~ print(7,test("def.xxx","!!!"))
+
+--~ local tim = os.clock() for i=1,250000 do local ext = test("abd.def.xxx","!!!") end print(os.clock()-tim)
+
+-- also rewrite previous
+
+local letter    = lpeg.R("az","AZ") + lpeg.S("_-+")
+local separator = lpeg.P("://")
+
+local qualified = lpeg.P(".")^0 * lpeg.P("/") + letter*lpeg.P(":") + letter^1*separator + letter^1 * lpeg.P("/")
+local rootbased = lpeg.P("/") + letter*lpeg.P(":")
+
+-- ./name ../name  /name c: :// name/name
+
+function file.is_qualified_path(filename)
+    return lpegmatch(qualified,filename) ~= nil
+end
+
+function file.is_rootbased_path(filename)
+    return lpegmatch(rootbased,filename) ~= nil
+end
+
+local slash  = lpeg.S("\\/")
+local period = lpeg.P(".")
+local drive  = lpeg.C(lpeg.R("az","AZ")) * lpeg.P(":")
+local path   = lpeg.C(((1-slash)^0 * slash)^0)
+local suffix = period * lpeg.C(lpeg.P(1-period)^0 * lpeg.P(-1))
+local base   = lpeg.C((1-suffix)^0)
+
+local pattern = (drive + lpeg.Cc("")) * (path + lpeg.Cc("")) * (base + lpeg.Cc("")) * (suffix + lpeg.Cc(""))
+
+function file.splitname(str) -- returns drive, path, base, suffix
+    return lpegmatch(pattern,str)
+end
+
+-- function test(t) for k, v in next, t do print(v, "=>", file.splitname(v)) end end
+--
+-- test { "c:", "c:/aa", "c:/aa/bb", "c:/aa/bb/cc", "c:/aa/bb/cc.dd", "c:/aa/bb/cc.dd.ee" }
+-- test { "c:", "c:aa", "c:aa/bb", "c:aa/bb/cc", "c:aa/bb/cc.dd", "c:aa/bb/cc.dd.ee" }
+-- test { "/aa", "/aa/bb", "/aa/bb/cc", "/aa/bb/cc.dd", "/aa/bb/cc.dd.ee" }
+-- test { "aa", "aa/bb", "aa/bb/cc", "aa/bb/cc.dd", "aa/bb/cc.dd.ee" }
+
+--~ -- todo:
+--~
+--~ if os.type == "windows" then
+--~     local currentdir = lfs.currentdir
+--~     function lfs.currentdir()
+--~         return (gsub(currentdir(),"\\","/"))
+--~     end
+--~ end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-md5'] = {
+    version   = 1.001,
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- This also provides file checksums and checkers.
+
+local gsub, format, byte = string.gsub, string.format, string.byte
+
+local function convert(str,fmt)
+    return (gsub(md5.sum(str),".",function(chr) return format(fmt,byte(chr)) end))
+end
+
+if not md5.HEX then function md5.HEX(str) return convert(str,"%02X") end end
+if not md5.hex then function md5.hex(str) return convert(str,"%02x") end end
+if not md5.dec then function md5.dec(str) return convert(str,"%03i") end end
+
+--~ if not md5.HEX then
+--~     local function remap(chr) return format("%02X",byte(chr)) end
+--~     function md5.HEX(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+--~ if not md5.hex then
+--~     local function remap(chr) return format("%02x",byte(chr)) end
+--~     function md5.hex(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+--~ if not md5.dec then
+--~     local function remap(chr) return format("%03i",byte(chr)) end
+--~     function md5.dec(str) return (gsub(md5.sum(str),".",remap)) end
+--~ end
+
+file.needs_updating_threshold = 1
+
+function file.needs_updating(oldname,newname) -- size modification access change
+    local oldtime = lfs.attributes(oldname, modification)
+    local newtime = lfs.attributes(newname, modification)
+    if newtime >= oldtime then
+        return false
+    elseif oldtime - newtime < file.needs_updating_threshold then
+        return false
+    else
+        return true
+    end
+end
+
+function file.checksum(name)
+    if md5 then
+        local data = io.loaddata(name)
+        if data then
+            return md5.HEX(data)
+        end
+    end
+    return nil
+end
+
+function file.loadchecksum(name)
+    if md5 then
+        local data = io.loaddata(name .. ".md5")
+        return data and (gsub(data,"%s",""))
+    end
+    return nil
+end
+
+function file.savechecksum(name, checksum)
+    if not checksum then checksum = file.checksum(name) end
+    if checksum then
+        io.savedata(name .. ".md5",checksum)
+        return checksum
+    end
+    return nil
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-url'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local char, gmatch, gsub = string.char, string.gmatch, string.gsub
+local tonumber, type = tonumber, type
+local lpegmatch = lpeg.match
+
+-- from the spec (on the web):
+--
+--     foo://example.com:8042/over/there?name=ferret#nose
+--     \_/   \______________/\_________/ \_________/ \__/
+--      |           |            |            |        |
+--   scheme     authority       path        query   fragment
+--      |   _____________________|__
+--     / \ /                        \
+--     urn:example:animal:ferret:nose
+
+url = url or { }
+
+local function tochar(s)
+    return char(tonumber(s,16))
+end
+
+local colon, qmark, hash, slash, percent, endofstring = lpeg.P(":"), lpeg.P("?"), lpeg.P("#"), lpeg.P("/"), lpeg.P("%"), lpeg.P(-1)
+
+local hexdigit  = lpeg.R("09","AF","af")
+local plus      = lpeg.P("+")
+local escaped   = (plus / " ") + (percent * lpeg.C(hexdigit * hexdigit) / tochar)
+
+-- we assume schemes with more than 1 character (in order to avoid problems with windows disks)
+
+local scheme    =                 lpeg.Cs((escaped+(1-colon-slash-qmark-hash))^2) * colon + lpeg.Cc("")
+local authority = slash * slash * lpeg.Cs((escaped+(1-      slash-qmark-hash))^0)         + lpeg.Cc("")
+local path      = slash *         lpeg.Cs((escaped+(1-            qmark-hash))^0)         + lpeg.Cc("")
+local query     = qmark         * lpeg.Cs((escaped+(1-                  hash))^0)         + lpeg.Cc("")
+local fragment  = hash          * lpeg.Cs((escaped+(1-           endofstring))^0)         + lpeg.Cc("")
+
+local parser = lpeg.Ct(scheme * authority * path * query * fragment)
+
+-- todo: reconsider Ct as we can as well have five return values (saves a table)
+-- so we can have two parsers, one with and one without
+
+function url.split(str)
+    return (type(str) == "string" and lpegmatch(parser,str)) or str
+end
+
+-- todo: cache them
+
+function url.hashed(str)
+    local s = url.split(str)
+    local somescheme = s[1] ~= ""
+    return {
+        scheme    = (somescheme and s[1]) or "file",
+        authority = s[2],
+        path      = s[3],
+        query     = s[4],
+        fragment  = s[5],
+        original  = str,
+        noscheme  = not somescheme,
+    }
+end
+
+function url.hasscheme(str)
+    return url.split(str)[1] ~= ""
+end
+
+function url.addscheme(str,scheme)
+    return (url.hasscheme(str) and str) or ((scheme or "file:///") .. str)
+end
+
+function url.construct(hash)
+    local fullurl = hash.sheme .. "://".. hash.authority .. hash.path
+    if hash.query then
+        fullurl = fullurl .. "?".. hash.query
+    end
+    if hash.fragment then
+        fullurl = fullurl .. "?".. hash.fragment
+    end
+    return fullurl
+end
+
+function url.filename(filename)
+    local t = url.hashed(filename)
+    return (t.scheme == "file" and (gsub(t.path,"^/([a-zA-Z])([:|])/)","%1:"))) or filename
+end
+
+function url.query(str)
+    if type(str) == "string" then
+        local t = { }
+        for k, v in gmatch(str,"([^&=]*)=([^&=]*)") do
+            t[k] = v
+        end
+        return t
+    else
+        return str
+    end
+end
+
+--~ print(url.filename("file:///c:/oeps.txt"))
+--~ print(url.filename("c:/oeps.txt"))
+--~ print(url.filename("file:///oeps.txt"))
+--~ print(url.filename("file:///etc/test.txt"))
+--~ print(url.filename("/oeps.txt"))
+
+--~ from the spec on the web (sort of):
+--~
+--~ function test(str)
+--~     print(table.serialize(url.hashed(str)))
+--~ end
+--~
+--~ test("%56pass%20words")
+--~ test("file:///c:/oeps.txt")
+--~ test("file:///c|/oeps.txt")
+--~ test("file:///etc/oeps.txt")
+--~ test("file://./etc/oeps.txt")
+--~ test("file:////etc/oeps.txt")
+--~ test("ftp://ftp.is.co.za/rfc/rfc1808.txt")
+--~ test("http://www.ietf.org/rfc/rfc2396.txt")
+--~ test("ldap://[2001:db8::7]/c=GB?objectClass?one#what")
+--~ test("mailto:John.Doe@example.com")
+--~ test("news:comp.infosystems.www.servers.unix")
+--~ test("tel:+1-816-555-1212")
+--~ test("telnet://192.0.2.16:80/")
+--~ test("urn:oasis:names:specification:docbook:dtd:xml:4.1.2")
+--~ test("/etc/passwords")
+--~ test("http://www.pragma-ade.com/spaced%20name")
+
+--~ test("zip:///oeps/oeps.zip#bla/bla.tex")
+--~ test("zip:///oeps/oeps.zip?bla/bla.tex")
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-dir'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- dir.expand_name will be merged with cleanpath and collapsepath
+
+local type = type
+local find, gmatch, match, gsub = string.find, string.gmatch, string.match, string.gsub
+local lpegmatch = lpeg.match
+
+dir = dir or { }
+
+-- handy
+
+function dir.current()
+    return (gsub(lfs.currentdir(),"\\","/"))
+end
+
+-- optimizing for no string.find (*) does not save time
+
+local attributes = lfs.attributes
+local walkdir    = lfs.dir
+
+local function glob_pattern(path,patt,recurse,action)
+    local ok, scanner
+    if path == "/" then
+        ok, scanner = xpcall(function() return walkdir(path..".") end, function() end) -- kepler safe
+    else
+        ok, scanner = xpcall(function() return walkdir(path)      end, function() end) -- kepler safe
+    end
+    if ok and type(scanner) == "function" then
+        if not find(path,"/$") then path = path .. '/' end
+        for name in scanner do
+            local full = path .. name
+            local mode = attributes(full,'mode')
+            if mode == 'file' then
+                if find(full,patt) then
+                    action(full)
+                end
+            elseif recurse and (mode == "directory") and (name ~= '.') and (name ~= "..") then
+                glob_pattern(full,patt,recurse,action)
+            end
+        end
+    end
+end
+
+dir.glob_pattern = glob_pattern
+
+local function collect_pattern(path,patt,recurse,result)
+    local ok, scanner
+    result = result or { }
+    if path == "/" then
+        ok, scanner = xpcall(function() return walkdir(path..".") end, function() end) -- kepler safe
+    else
+        ok, scanner = xpcall(function() return walkdir(path)      end, function() end) -- kepler safe
+    end
+    if ok and type(scanner) == "function" then
+        if not find(path,"/$") then path = path .. '/' end
+        for name in scanner do
+            local full = path .. name
+            local attr = attributes(full)
+            local mode = attr.mode
+            if mode == 'file' then
+                if find(full,patt) then
+                    result[name] = attr
+                end
+            elseif recurse and (mode == "directory") and (name ~= '.') and (name ~= "..") then
+                attr.list = collect_pattern(full,patt,recurse)
+                result[name] = attr
+            end
+        end
+    end
+    return result
+end
+
+dir.collect_pattern = collect_pattern
+
+local P, S, R, C, Cc, Cs, Ct, Cv, V = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.Cc, lpeg.Cs, lpeg.Ct, lpeg.Cv, lpeg.V
+
+local pattern = Ct {
+    [1] = (C(P(".") + P("/")^1) + C(R("az","AZ") * P(":") * P("/")^0) + Cc("./")) * V(2) * V(3),
+    [2] = C(((1-S("*?/"))^0 * P("/"))^0),
+    [3] = C(P(1)^0)
+}
+
+local filter = Cs ( (
+    P("**") / ".*" +
+    P("*")  / "[^/]*" +
+    P("?")  / "[^/]" +
+    P(".")  / "%%." +
+    P("+")  / "%%+" +
+    P("-")  / "%%-" +
+    P(1)
+)^0 )
+
+local function glob(str,t)
+    if type(t) == "function" then
+        if type(str) == "table" then
+            for s=1,#str do
+                glob(str[s],t)
+            end
+        elseif lfs.isfile(str) then
+            t(str)
+        else
+            local split = lpegmatch(pattern,str)
+            if split then
+                local root, path, base = split[1], split[2], split[3]
+                local recurse = find(base,"%*%*")
+                local start = root .. path
+                local result = lpegmatch(filter,start .. base)
+                glob_pattern(start,result,recurse,t)
+            end
+        end
+    else
+        if type(str) == "table" then
+            local t = t or { }
+            for s=1,#str do
+                glob(str[s],t)
+            end
+            return t
+        elseif lfs.isfile(str) then
+            local t = t or { }
+            t[#t+1] = str
+            return t
+        else
+            local split = lpegmatch(pattern,str)
+            if split then
+                local t = t or { }
+                local action = action or function(name) t[#t+1] = name end
+                local root, path, base = split[1], split[2], split[3]
+                local recurse = find(base,"%*%*")
+                local start = root .. path
+                local result = lpegmatch(filter,start .. base)
+                glob_pattern(start,result,recurse,action)
+                return t
+            else
+                return { }
+            end
+        end
+    end
+end
+
+dir.glob = glob
+
+--~ list = dir.glob("**/*.tif")
+--~ list = dir.glob("/**/*.tif")
+--~ list = dir.glob("./**/*.tif")
+--~ list = dir.glob("oeps/**/*.tif")
+--~ list = dir.glob("/oeps/**/*.tif")
+
+local function globfiles(path,recurse,func,files) -- func == pattern or function
+    if type(func) == "string" then
+        local s = func -- alas, we need this indirect way
+        func = function(name) return find(name,s) end
+    end
+    files = files or { }
+    for name in walkdir(path) do
+        if find(name,"^%.") then
+            --- skip
+        else
+            local mode = attributes(name,'mode')
+            if mode == "directory" then
+                if recurse then
+                    globfiles(path .. "/" .. name,recurse,func,files)
+                end
+            elseif mode == "file" then
+                if func then
+                    if func(name) then
+                        files[#files+1] = path .. "/" .. name
+                    end
+                else
+                    files[#files+1] = path .. "/" .. name
+                end
+            end
+        end
+    end
+    return files
+end
+
+dir.globfiles = globfiles
+
+-- t = dir.glob("c:/data/develop/context/sources/**/????-*.tex")
+-- t = dir.glob("c:/data/develop/tex/texmf/**/*.tex")
+-- t = dir.glob("c:/data/develop/context/texmf/**/*.tex")
+-- t = dir.glob("f:/minimal/tex/**/*")
+-- print(dir.ls("f:/minimal/tex/**/*"))
+-- print(dir.ls("*.tex"))
+
+function dir.ls(pattern)
+    return table.concat(glob(pattern),"\n")
+end
+
+--~ mkdirs("temp")
+--~ mkdirs("a/b/c")
+--~ mkdirs(".","/a/b/c")
+--~ mkdirs("a","b","c")
+
+local make_indeed = true -- false
+
+if string.find(os.getenv("PATH"),";") then -- os.type == "windows"
+
+    function dir.mkdirs(...)
+        local str, pth, t = "", "", { ... }
+        for i=1,#t do
+            local s = t[i]
+            if s ~= "" then
+                if str ~= "" then
+                    str = str .. "/" .. s
+                else
+                    str = s
+                end
+            end
+        end
+        local first, middle, last
+        local drive = false
+        first, middle, last = match(str,"^(//)(//*)(.*)$")
+        if first then
+            -- empty network path == local path
+        else
+            first, last = match(str,"^(//)/*(.-)$")
+            if first then
+                middle, last = match(str,"([^/]+)/+(.-)$")
+                if middle then
+                    pth = "//" .. middle
+                else
+                    pth = "//" .. last
+                    last = ""
+                end
+            else
+                first, middle, last = match(str,"^([a-zA-Z]:)(/*)(.-)$")
+                if first then
+                    pth, drive = first .. middle, true
+                else
+                    middle, last = match(str,"^(/*)(.-)$")
+                    if not middle then
+                        last = str
+                    end
+                end
+            end
+        end
+        for s in gmatch(last,"[^/]+") do
+            if pth == "" then
+                pth = s
+            elseif drive then
+                pth, drive = pth .. s, false
+            else
+                pth = pth .. "/" .. s
+            end
+            if make_indeed and not lfs.isdir(pth) then
+                lfs.mkdir(pth)
+            end
+        end
+        return pth, (lfs.isdir(pth) == true)
+    end
+
+--~         print(dir.mkdirs("","","a","c"))
+--~         print(dir.mkdirs("a"))
+--~         print(dir.mkdirs("a:"))
+--~         print(dir.mkdirs("a:/b/c"))
+--~         print(dir.mkdirs("a:b/c"))
+--~         print(dir.mkdirs("a:/bbb/c"))
+--~         print(dir.mkdirs("/a/b/c"))
+--~         print(dir.mkdirs("/aaa/b/c"))
+--~         print(dir.mkdirs("//a/b/c"))
+--~         print(dir.mkdirs("///a/b/c"))
+--~         print(dir.mkdirs("a/bbb//ccc/"))
+
+    function dir.expand_name(str) -- will be merged with cleanpath and collapsepath
+        local first, nothing, last = match(str,"^(//)(//*)(.*)$")
+        if first then
+            first = dir.current() .. "/"
+        end
+        if not first then
+            first, last = match(str,"^(//)/*(.*)$")
+        end
+        if not first then
+            first, last = match(str,"^([a-zA-Z]:)(.*)$")
+            if first and not find(last,"^/") then
+                local d = lfs.currentdir()
+                if lfs.chdir(first) then
+                    first = dir.current()
+                end
+                lfs.chdir(d)
+            end
+        end
+        if not first then
+            first, last = dir.current(), str
+        end
+        last = gsub(last,"//","/")
+        last = gsub(last,"/%./","/")
+        last = gsub(last,"^/*","")
+        first = gsub(first,"/*$","")
+        if last == "" then
+            return first
+        else
+            return first .. "/" .. last
+        end
+    end
+
+else
+
+    function dir.mkdirs(...)
+        local str, pth, t = "", "", { ... }
+        for i=1,#t do
+            local s = t[i]
+            if s ~= "" then
+                if str ~= "" then
+                    str = str .. "/" .. s
+                else
+                    str = s
+                end
+            end
+        end
+        str = gsub(str,"/+","/")
+        if find(str,"^/") then
+            pth = "/"
+            for s in gmatch(str,"[^/]+") do
+                local first = (pth == "/")
+                if first then
+                    pth = pth .. s
+                else
+                    pth = pth .. "/" .. s
+                end
+                if make_indeed and not first and not lfs.isdir(pth) then
+                    lfs.mkdir(pth)
+                end
+            end
+        else
+            pth = "."
+            for s in gmatch(str,"[^/]+") do
+                pth = pth .. "/" .. s
+                if make_indeed and not lfs.isdir(pth) then
+                    lfs.mkdir(pth)
+                end
+            end
+        end
+        return pth, (lfs.isdir(pth) == true)
+    end
+
+--~         print(dir.mkdirs("","","a","c"))
+--~         print(dir.mkdirs("a"))
+--~         print(dir.mkdirs("/a/b/c"))
+--~         print(dir.mkdirs("/aaa/b/c"))
+--~         print(dir.mkdirs("//a/b/c"))
+--~         print(dir.mkdirs("///a/b/c"))
+--~         print(dir.mkdirs("a/bbb//ccc/"))
+
+    function dir.expand_name(str) -- will be merged with cleanpath and collapsepath
+        if not find(str,"^/") then
+            str = lfs.currentdir() .. "/" .. str
+        end
+        str = gsub(str,"//","/")
+        str = gsub(str,"/%./","/")
+        return str
+    end
+
+end
+
+dir.makedirs = dir.mkdirs
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-boolean'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+boolean = boolean or { }
+
+local type, tonumber = type, tonumber
+
+function boolean.tonumber(b)
+    if b then return 1 else return 0 end
+end
+
+function toboolean(str,tolerant)
+    if tolerant then
+        local tstr = type(str)
+        if tstr == "string" then
+            return str == "true" or str == "yes" or str == "on" or str == "1" or str == "t"
+        elseif tstr == "number" then
+            return tonumber(str) ~= 0
+        elseif tstr == "nil" then
+            return false
+        else
+            return str
+        end
+    elseif str == "true" then
+        return true
+    elseif str == "false" then
+        return false
+    else
+        return str
+    end
+end
+
+function string.is_boolean(str)
+    if type(str) == "string" then
+        if str == "true" or str == "yes" or str == "on" or str == "t" then
+            return true
+        elseif str == "false" or str == "no" or str == "off" or str == "f" then
+            return false
+        end
+    end
+    return nil
+end
+
+function boolean.alwaystrue()
+    return true
+end
+
+function boolean.falsetrue()
+    return false
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-math'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local floor, sin, cos, tan = math.floor, math.sin, math.cos, math.tan
+
+if not math.round then
+    function math.round(x)
+        return floor(x + 0.5)
+    end
+end
+
+if not math.div then
+    function math.div(n,m)
+        return floor(n/m)
+    end
+end
+
+if not math.mod then
+    function math.mod(n,m)
+        return n % m
+    end
+end
+
+local pipi = 2*math.pi/360
+
+function math.sind(d)
+    return sin(d*pipi)
+end
+
+function math.cosd(d)
+    return cos(d*pipi)
+end
+
+function math.tand(d)
+    return tan(d*pipi)
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-utils'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- hm, quite unreadable
+
+local gsub = string.gsub
+local concat = table.concat
+local type, next = type, next
+
+if not utils        then utils        = { } end
+if not utils.merger then utils.merger = { } end
+if not utils.lua    then utils.lua    = { } end
+
+utils.merger.m_begin = "begin library merge"
+utils.merger.m_end   = "end library merge"
+utils.merger.pattern =
+    "%c+" ..
+    "%-%-%s+" .. utils.merger.m_begin ..
+    "%c+(.-)%c+" ..
+    "%-%-%s+" .. utils.merger.m_end ..
+    "%c+"
+
+function utils.merger._self_fake_()
+    return
+        "-- " .. "created merged file" .. "\n\n" ..
+        "-- " .. utils.merger.m_begin  .. "\n\n" ..
+        "-- " .. utils.merger.m_end    .. "\n\n"
+end
+
+function utils.report(...)
+    print(...)
+end
+
+utils.merger.strip_comment = true
+
+function utils.merger._self_load_(name)
+    local f, data = io.open(name), ""
+    if f then
+        utils.report("reading merge from %s",name)
+        data = f:read("*all")
+        f:close()
+    else
+        utils.report("unknown file to merge %s",name)
+    end
+    if data and utils.merger.strip_comment then
+        -- saves some 20K
+        data = gsub(data,"%-%-~[^\n\r]*[\r\n]", "")
+    end
+    return data or ""
+end
+
+function utils.merger._self_save_(name, data)
+    if data ~= "" then
+        local f = io.open(name,'w')
+        if f then
+            utils.report("saving merge from %s",name)
+            f:write(data)
+            f:close()
+        end
+    end
+end
+
+function utils.merger._self_swap_(data,code)
+    if data ~= "" then
+        return (gsub(data,utils.merger.pattern, function(s)
+            return "\n\n" .. "-- "..utils.merger.m_begin .. "\n" .. code .. "\n" .. "-- "..utils.merger.m_end .. "\n\n"
+        end, 1))
+    else
+        return ""
+    end
+end
+
+--~ stripper:
+--~
+--~ data = gsub(data,"%-%-~[^\n]*\n","")
+--~ data = gsub(data,"\n\n+","\n")
+
+function utils.merger._self_libs_(libs,list)
+    local result, f, frozen = { }, nil, false
+    result[#result+1] = "\n"
+    if type(libs) == 'string' then libs = { libs } end
+    if type(list) == 'string' then list = { list } end
+    local foundpath = nil
+    for i=1,#libs do
+        local lib = libs[i]
+        for j=1,#list do
+            local pth = gsub(list[j],"\\","/") -- file.clean_path
+            utils.report("checking library path %s",pth)
+            local name = pth .. "/" .. lib
+            if lfs.isfile(name) then
+                foundpath = pth
+            end
+        end
+        if foundpath then break end
+    end
+    if foundpath then
+        utils.report("using library path %s",foundpath)
+        local right, wrong = { }, { }
+        for i=1,#libs do
+            local lib = libs[i]
+            local fullname = foundpath .. "/" .. lib
+            if lfs.isfile(fullname) then
+            --  right[#right+1] = lib
+                utils.report("merging library %s",fullname)
+                result[#result+1] = "do -- create closure to overcome 200 locals limit"
+                result[#result+1] = io.loaddata(fullname,true)
+                result[#result+1] = "end -- of closure"
+            else
+            --  wrong[#wrong+1] = lib
+                utils.report("no library %s",fullname)
+            end
+        end
+        if #right > 0 then
+            utils.report("merged libraries: %s",concat(right," "))
+        end
+        if #wrong > 0 then
+            utils.report("skipped libraries: %s",concat(wrong," "))
+        end
+    else
+        utils.report("no valid library path found")
+    end
+    return concat(result, "\n\n")
+end
+
+function utils.merger.selfcreate(libs,list,target)
+    if target then
+        utils.merger._self_save_(
+            target,
+            utils.merger._self_swap_(
+                utils.merger._self_fake_(),
+                utils.merger._self_libs_(libs,list)
+            )
+        )
+    end
+end
+
+function utils.merger.selfmerge(name,libs,list,target)
+    utils.merger._self_save_(
+        target or name,
+        utils.merger._self_swap_(
+            utils.merger._self_load_(name),
+            utils.merger._self_libs_(libs,list)
+        )
+    )
+end
+
+function utils.merger.selfclean(name)
+    utils.merger._self_save_(
+        name,
+        utils.merger._self_swap_(
+            utils.merger._self_load_(name),
+            ""
+        )
+    )
+end
+
+function utils.lua.compile(luafile, lucfile, cleanup, strip) -- defaults: cleanup=false strip=true
+ -- utils.report("compiling",luafile,"into",lucfile)
+    os.remove(lucfile)
+    local command = "-o " .. string.quote(lucfile) .. " " .. string.quote(luafile)
+    if strip ~= false then
+        command = "-s " .. command
+    end
+    local done = (os.spawn("texluac " .. command) == 0) or (os.spawn("luac " .. command) == 0)
+    if done and cleanup == true and lfs.isfile(lucfile) and lfs.isfile(luafile) then
+     -- utils.report("removing",luafile)
+        os.remove(luafile)
+    end
+    return done
+end
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['l-aux'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- for inline, no store split : for s in string.gmatch(str,",* *([^,]+)") do .. end
+
+aux = aux or { }
+
+local concat, format, gmatch = table.concat, string.format, string.gmatch
+local tostring, type = tostring, type
+local lpegmatch = lpeg.match
+
+local P, R, V = lpeg.P, lpeg.R, lpeg.V
+
+local escape, left, right = P("\\"), P('{'), P('}')
+
+lpeg.patterns.balanced = P {
+    [1] = ((escape * (left+right)) + (1 - (left+right)) + V(2))^0,
+    [2] = left * V(1) * right
+}
+
+local space     = lpeg.P(' ')
+local equal     = lpeg.P("=")
+local comma     = lpeg.P(",")
+local lbrace    = lpeg.P("{")
+local rbrace    = lpeg.P("}")
+local nobrace   = 1 - (lbrace+rbrace)
+local nested    = lpeg.P { lbrace * (nobrace + lpeg.V(1))^0 * rbrace }
+local spaces    = space^0
+
+local value     = lpeg.P(lbrace * lpeg.C((nobrace + nested)^0) * rbrace) + lpeg.C((nested + (1-comma))^0)
+
+local key       = lpeg.C((1-equal-comma)^1)
+local pattern_a = (space+comma)^0 * (key * equal * value + key * lpeg.C(""))
+local pattern_c = (space+comma)^0 * (key * equal * value)
+
+local key       = lpeg.C((1-space-equal-comma)^1)
+local pattern_b = spaces * comma^0 * spaces * (key * ((spaces * equal * spaces * value) + lpeg.C("")))
+
+-- "a=1, b=2, c=3, d={a{b,c}d}, e=12345, f=xx{a{b,c}d}xx, g={}" : outer {} removes, leading spaces ignored
+
+local hash = { }
+
+local function set(key,value) -- using Carg is slower here
+    hash[key] = value
+end
+
+local pattern_a_s = (pattern_a/set)^1
+local pattern_b_s = (pattern_b/set)^1
+local pattern_c_s = (pattern_c/set)^1
+
+aux.settings_to_hash_pattern_a = pattern_a_s
+aux.settings_to_hash_pattern_b = pattern_b_s
+aux.settings_to_hash_pattern_c = pattern_c_s
+
+function aux.make_settings_to_hash_pattern(set,how)
+    if how == "strict" then
+        return (pattern_c/set)^1
+    elseif how == "tolerant" then
+        return (pattern_b/set)^1
+    else
+        return (pattern_a/set)^1
+    end
+end
+
+function aux.settings_to_hash(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        if moretolerant then
+            lpegmatch(pattern_b_s,str)
+        else
+            lpegmatch(pattern_a_s,str)
+        end
+        return hash
+    else
+        return { }
+    end
+end
+
+function aux.settings_to_hash_tolerant(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        lpegmatch(pattern_b_s,str)
+        return hash
+    else
+        return { }
+    end
+end
+
+function aux.settings_to_hash_strict(str,existing)
+    if str and str ~= "" then
+        hash = existing or { }
+        lpegmatch(pattern_c_s,str)
+        return next(hash) and hash
+    else
+        return nil
+    end
+end
+
+local separator = comma * space^0
+local value     = lpeg.P(lbrace * lpeg.C((nobrace + nested)^0) * rbrace) + lpeg.C((nested + (1-comma))^0)
+local pattern   = lpeg.Ct(value*(separator*value)^0)
+
+-- "aap, {noot}, mies" : outer {} removes, leading spaces ignored
+
+aux.settings_to_array_pattern = pattern
+
+-- we could use a weak table as cache
+
+function aux.settings_to_array(str)
+    if not str or str == "" then
+        return { }
+    else
+        return lpegmatch(pattern,str)
+    end
+end
+
+local function set(t,v)
+    t[#t+1] = v
+end
+
+local value   = lpeg.P(lpeg.Carg(1)*value) / set
+local pattern = value*(separator*value)^0 * lpeg.Carg(1)
+
+function aux.add_settings_to_array(t,str)
+    return lpegmatch(pattern,str,nil,t)
+end
+
+function aux.hash_to_string(h,separator,yes,no,strict,omit)
+    if h then
+        local t, s = { }, table.sortedkeys(h)
+        omit = omit and table.tohash(omit)
+        for i=1,#s do
+            local key = s[i]
+            if not omit or not omit[key] then
+                local value = h[key]
+                if type(value) == "boolean" then
+                    if yes and no then
+                        if value then
+                            t[#t+1] = key .. '=' .. yes
+                        elseif not strict then
+                            t[#t+1] = key .. '=' .. no
+                        end
+                    elseif value or not strict then
+                        t[#t+1] = key .. '=' .. tostring(value)
+                    end
+                else
+                    t[#t+1] = key .. '=' .. value
+                end
+            end
+        end
+        return concat(t,separator or ",")
+    else
+        return ""
+    end
+end
+
+function aux.array_to_string(a,separator)
+    if a then
+        return concat(a,separator or ",")
+    else
+        return ""
+    end
+end
+
+function aux.settings_to_set(str,t)
+    t = t or { }
+    for s in gmatch(str,"%s*([^,]+)") do
+        t[s] = true
+    end
+    return t
+end
+
+local value     = lbrace * lpeg.C((nobrace + nested)^0) * rbrace
+local pattern   = lpeg.Ct((space + value)^0)
+
+function aux.arguments_to_table(str)
+    return lpegmatch(pattern,str)
+end
+
+-- temporary here
+
+function aux.getparameters(self,class,parentclass,settings)
+    local sc = self[class]
+    if not sc then
+        sc = table.clone(self[parent])
+        self[class] = sc
+    end
+    aux.settings_to_hash(settings,sc)
+end
+
+-- temporary here
+
+local digit         = lpeg.R("09")
+local period        = lpeg.P(".")
+local zero          = lpeg.P("0")
+local trailingzeros = zero^0 * -digit -- suggested by Roberto R
+local case_1        = period * trailingzeros / ""
+local case_2        = period * (digit - trailingzeros)^1 * (trailingzeros / "")
+local number        = digit^1 * (case_1 + case_2)
+local stripper      = lpeg.Cs((number + 1)^0)
+
+--~ local sample = "bla 11.00 bla 11 bla 0.1100 bla 1.00100 bla 0.00 bla 0.001 bla 1.1100 bla 0.100100100 bla 0.00100100100"
+--~ collectgarbage("collect")
+--~ str = string.rep(sample,10000)
+--~ local ts = os.clock()
+--~ lpegmatch(stripper,str)
+--~ print(#str, os.clock()-ts, lpegmatch(stripper,sample))
+
+lpeg.patterns.strip_zeros = stripper
+
+function aux.strip_zeros(str)
+    return lpegmatch(stripper,str)
+end
+
+function aux.definetable(target) -- defines undefined tables
+    local composed, t = nil, { }
+    for name in gmatch(target,"([^%.]+)") do
+        if composed then
+            composed = composed .. "." .. name
+        else
+            composed = name
+        end
+        t[#t+1] = format("%s = %s or { }",composed,composed)
+    end
+    return concat(t,"\n")
+end
+
+function aux.accesstable(target)
+    local t = _G
+    for name in gmatch(target,"([^%.]+)") do
+        t = t[name]
+    end
+    return t
+end
+
+--~ function string.commaseparated(str)
+--~     return gmatch(str,"([^,%s]+)")
+--~ end
+
+-- as we use this a lot ...
+
+--~ function aux.cachefunction(action,weak)
+--~     local cache = { }
+--~     if weak then
+--~         setmetatable(cache, { __mode = "kv" } )
+--~     end
+--~     local function reminder(str)
+--~         local found = cache[str]
+--~         if not found then
+--~             found = action(str)
+--~             cache[str] = found
+--~         end
+--~         return found
+--~     end
+--~     return reminder, cache
+--~ end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-tra'] = {
+    version   = 1.001,
+    comment   = "companion to trac-tra.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- the <anonymous> tag is kind of generic and used for functions that are not
+-- bound to a variable, like node.new, node.copy etc (contrary to for instance
+-- node.has_attribute which is bound to a has_attribute local variable in mkiv)
+
+local debug = require "debug"
+
+local getinfo = debug.getinfo
+local type, next = type, next
+local concat = table.concat
+local format, find, lower, gmatch, gsub = string.format, string.find, string.lower, string.gmatch, string.gsub
+
+debugger = debugger or { }
+
+local counters = { }
+local names = { }
+
+-- one
+
+local function hook()
+    local f = getinfo(2,"f").func
+    local n = getinfo(2,"Sn")
+--  if n.what == "C" and n.name then print (n.namewhat .. ': ' .. n.name) end
+    if f then
+        local cf = counters[f]
+        if cf == nil then
+            counters[f] = 1
+            names[f] = n
+        else
+            counters[f] = cf + 1
+        end
+    end
+end
+local function getname(func)
+    local n = names[func]
+    if n then
+        if n.what == "C" then
+            return n.name or '<anonymous>'
+        else
+            -- source short_src linedefined what name namewhat nups func
+            local name = n.name or n.namewhat or n.what
+            if not name or name == "" then name = "?" end
+            return format("%s : %s : %s", n.short_src or "unknown source", n.linedefined or "--", name)
+        end
+    else
+        return "unknown"
+    end
+end
+function debugger.showstats(printer,threshold)
+    printer   = printer or texio.write or print
+    threshold = threshold or 0
+    local total, grandtotal, functions = 0, 0, 0
+    printer("\n") -- ugly but ok
+ -- table.sort(counters)
+    for func, count in next, counters do
+        if count > threshold then
+            local name = getname(func)
+            if not find(name,"for generator") then
+                printer(format("%8i  %s", count, name))
+                total = total + count
+            end
+        end
+        grandtotal = grandtotal + count
+        functions = functions + 1
+    end
+    printer(format("functions: %s, total: %s, grand total: %s, threshold: %s\n", functions, total, grandtotal, threshold))
+end
+
+-- two
+
+--~ local function hook()
+--~     local n = getinfo(2)
+--~     if n.what=="C" and not n.name then
+--~         local f = tostring(debug.traceback())
+--~         local cf = counters[f]
+--~         if cf == nil then
+--~             counters[f] = 1
+--~             names[f] = n
+--~         else
+--~             counters[f] = cf + 1
+--~         end
+--~     end
+--~ end
+--~ function debugger.showstats(printer,threshold)
+--~     printer   = printer or texio.write or print
+--~     threshold = threshold or 0
+--~     local total, grandtotal, functions = 0, 0, 0
+--~     printer("\n") -- ugly but ok
+--~  -- table.sort(counters)
+--~     for func, count in next, counters do
+--~         if count > threshold then
+--~             printer(format("%8i  %s", count, func))
+--~             total = total + count
+--~         end
+--~         grandtotal = grandtotal + count
+--~         functions = functions + 1
+--~     end
+--~     printer(format("functions: %s, total: %s, grand total: %s, threshold: %s\n", functions, total, grandtotal, threshold))
+--~ end
+
+-- rest
+
+function debugger.savestats(filename,threshold)
+    local f = io.open(filename,'w')
+    if f then
+        debugger.showstats(function(str) f:write(str) end,threshold)
+        f:close()
+    end
+end
+
+function debugger.enable()
+    debug.sethook(hook,"c")
+end
+
+function debugger.disable()
+    debug.sethook()
+--~ counters[debug.getinfo(2,"f").func] = nil
+end
+
+function debugger.tracing()
+    local n = tonumber(os.env['MTX.TRACE.CALLS']) or tonumber(os.env['MTX_TRACE_CALLS']) or 0
+    if n > 0 then
+        function debugger.tracing() return true  end ; return true
+    else
+        function debugger.tracing() return false end ; return false
+    end
+end
+
+--~ debugger.enable()
+
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+--~ print(math.sin(1*.5))
+
+--~ debugger.disable()
+
+--~ print("")
+--~ debugger.showstats()
+--~ print("")
+--~ debugger.showstats(print,3)
+
+setters      = setters      or { }
+setters.data = setters.data or { }
+
+--~ local function set(t,what,value)
+--~     local data, done = t.data, t.done
+--~     if type(what) == "string" then
+--~         what = aux.settings_to_array(what) -- inefficient but ok
+--~     end
+--~     for i=1,#what do
+--~         local w = what[i]
+--~         for d, f in next, data do
+--~             if done[d] then
+--~                 -- prevent recursion due to wildcards
+--~             elseif find(d,w) then
+--~                 done[d] = true
+--~                 for i=1,#f do
+--~                     f[i](value)
+--~                 end
+--~             end
+--~         end
+--~     end
+--~ end
+
+local function set(t,what,value)
+    local data, done = t.data, t.done
+    if type(what) == "string" then
+        what = aux.settings_to_hash(what) -- inefficient but ok
+    end
+    for w, v in next, what do
+        if v == "" then
+            v = value
+        else
+            v = toboolean(v)
+        end
+        for d, f in next, data do
+            if done[d] then
+                -- prevent recursion due to wildcards
+            elseif find(d,w) then
+                done[d] = true
+                for i=1,#f do
+                    f[i](v)
+                end
+            end
+        end
+    end
+end
+
+local function reset(t)
+    for d, f in next, t.data do
+        for i=1,#f do
+            f[i](false)
+        end
+    end
+end
+
+local function enable(t,what)
+    set(t,what,true)
+end
+
+local function disable(t,what)
+    local data = t.data
+    if not what or what == "" then
+        t.done = { }
+        reset(t)
+    else
+        set(t,what,false)
+    end
+end
+
+function setters.register(t,what,...)
+    local data = t.data
+    what = lower(what)
+    local w = data[what]
+    if not w then
+        w = { }
+        data[what] = w
+    end
+    for _, fnc in next, { ... } do
+        local typ = type(fnc)
+        if typ == "function" then
+            w[#w+1] = fnc
+        elseif typ == "string" then
+            w[#w+1] = function(value) set(t,fnc,value,nesting) end
+        end
+    end
+end
+
+function setters.enable(t,what)
+    local e = t.enable
+    t.enable, t.done = enable, { }
+    enable(t,string.simpleesc(tostring(what)))
+    t.enable, t.done = e, { }
+end
+
+function setters.disable(t,what)
+    local e = t.disable
+    t.disable, t.done = disable, { }
+    disable(t,string.simpleesc(tostring(what)))
+    t.disable, t.done = e, { }
+end
+
+function setters.reset(t)
+    t.done = { }
+    reset(t)
+end
+
+function setters.list(t) -- pattern
+    local list = table.sortedkeys(t.data)
+    local user, system = { }, { }
+    for l=1,#list do
+        local what = list[l]
+        if find(what,"^%*") then
+            system[#system+1] = what
+        else
+            user[#user+1] = what
+        end
+    end
+    return user, system
+end
+
+function setters.show(t)
+    commands.writestatus("","")
+    local list = setters.list(t)
+    for k=1,#list do
+        commands.writestatus(t.name,list[k])
+    end
+    commands.writestatus("","")
+end
+
+-- we could have used a bit of oo and the trackers:enable syntax but
+-- there is already a lot of code around using the singular tracker
+
+-- we could make this into a module
+
+function setters.new(name)
+    local t
+    t = {
+        data     = { },
+        name     = name,
+        enable   = function(...) setters.enable  (t,...) end,
+        disable  = function(...) setters.disable (t,...) end,
+        register = function(...) setters.register(t,...) end,
+        list     = function(...) setters.list    (t,...) end,
+        show     = function(...) setters.show    (t,...) end,
+    }
+    setters.data[name] = t
+    return t
+end
+
+trackers    = setters.new("trackers")
+directives  = setters.new("directives")
+experiments = setters.new("experiments")
+
+-- nice trick: we overload two of the directives related functions with variants that
+-- do tracing (itself using a tracker) .. proof of concept
+
+local trace_directives  = false local trace_directives  = false  trackers.register("system.directives",  function(v) trace_directives  = v end)
+local trace_experiments = false local trace_experiments = false  trackers.register("system.experiments", function(v) trace_experiments = v end)
+
+local e = directives.enable
+local d = directives.disable
+
+function directives.enable(...)
+    commands.writestatus("directives","enabling: %s",concat({...}," "))
+    e(...)
+end
+
+function directives.disable(...)
+    commands.writestatus("directives","disabling: %s",concat({...}," "))
+    d(...)
+end
+
+local e = experiments.enable
+local d = experiments.disable
+
+function experiments.enable(...)
+    commands.writestatus("experiments","enabling: %s",concat({...}," "))
+    e(...)
+end
+
+function experiments.disable(...)
+    commands.writestatus("experiments","disabling: %s",concat({...}," "))
+    d(...)
+end
+
+-- a useful example
+
+directives.register("system.nostatistics", function(v)
+    statistics.enable = not v
+end)
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['lxml-tab'] = {
+    version   = 1.001,
+    comment   = "this module is the basis for the lxml-* ones",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- this module needs a cleanup: check latest lpeg, passing args, (sub)grammar, etc etc
+-- stripping spaces from e.g. cont-en.xml saves .2 sec runtime so it's not worth the
+-- trouble
+
+local trace_entities = false  trackers.register("xml.entities", function(v) trace_entities = v end)
+
+--[[ldx--
+<p>The parser used here is inspired by the variant discussed in the lua book, but
+handles comment and processing instructions, has a different structure, provides
+parent access; a first version used different trickery but was less optimized to we
+went this route. First we had a find based parser, now we have an <l n='lpeg'/> based one.
+The find based parser can be found in l-xml-edu.lua along with other older code.</p>
+
+<p>Beware, the interface may change. For instance at, ns, tg, dt may get more
+verbose names. Once the code is stable we will also remove some tracing and
+optimize the code.</p>
+--ldx]]--
+
+xml = xml or { }
+
+--~ local xml = xml
+
+local concat, remove, insert = table.concat, table.remove, table.insert
+local type, next, setmetatable, getmetatable, tonumber = type, next, setmetatable, getmetatable, tonumber
+local format, lower, find, match, gsub = string.format, string.lower, string.find, string.match, string.gsub
+local utfchar = unicode.utf8.char
+local lpegmatch = lpeg.match
+local P, S, R, C, V, C, Cs = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.V, lpeg.C, lpeg.Cs
+
+--[[ldx--
+<p>First a hack to enable namespace resolving. A namespace is characterized by
+a <l n='url'/>. The following function associates a namespace prefix with a
+pattern. We use <l n='lpeg'/>, which in this case is more than twice as fast as a
+find based solution where we loop over an array of patterns. Less code and
+much cleaner.</p>
+--ldx]]--
+
+xml.xmlns = xml.xmlns or { }
+
+local check = P(false)
+local parse = check
+
+--[[ldx--
+<p>The next function associates a namespace prefix with an <l n='url'/>. This
+normally happens independent of parsing.</p>
+
+<typing>
+xml.registerns("mml","mathml")
+</typing>
+--ldx]]--
+
+function xml.registerns(namespace, pattern) -- pattern can be an lpeg
+    check = check + C(P(lower(pattern))) / namespace
+    parse = P { P(check) + 1 * V(1) }
+end
+
+--[[ldx--
+<p>The next function also registers a namespace, but this time we map a
+given namespace prefix onto a registered one, using the given
+<l n='url'/>. This used for attributes like <t>xmlns:m</t>.</p>
+
+<typing>
+xml.checkns("m","http://www.w3.org/mathml")
+</typing>
+--ldx]]--
+
+function xml.checkns(namespace,url)
+    local ns = lpegmatch(parse,lower(url))
+    if ns and namespace ~= ns then
+        xml.xmlns[namespace] = ns
+    end
+end
+
+--[[ldx--
+<p>Next we provide a way to turn an <l n='url'/> into a registered
+namespace. This used for the <t>xmlns</t> attribute.</p>
+
+<typing>
+resolvedns = xml.resolvens("http://www.w3.org/mathml")
+</typing>
+
+This returns <t>mml</t>.
+--ldx]]--
+
+function xml.resolvens(url)
+     return lpegmatch(parse,lower(url)) or ""
+end
+
+--[[ldx--
+<p>A namespace in an element can be remapped onto the registered
+one efficiently by using the <t>xml.xmlns</t> table.</p>
+--ldx]]--
+
+--[[ldx--
+<p>This version uses <l n='lpeg'/>. We follow the same approach as before, stack and top and
+such. This version is about twice as fast which is mostly due to the fact that
+we don't have to prepare the stream for cdata, doctype etc etc. This variant is
+is dedicated to Luigi Scarso, who challenged me with 40 megabyte <l n='xml'/> files that
+took 12.5 seconds to load (1.5 for file io and the rest for tree building). With
+the <l n='lpeg'/> implementation we got that down to less 7.3 seconds. Loading the 14
+<l n='context'/> interface definition files (2.6 meg) went down from 1.05 seconds to 0.55.</p>
+
+<p>Next comes the parser. The rather messy doctype definition comes in many
+disguises so it is no surprice that later on have to dedicate quite some
+<l n='lpeg'/> code to it.</p>
+
+<typing>
+<!DOCTYPE Something PUBLIC "... ..." "..." [ ... ] >
+<!DOCTYPE Something PUBLIC "... ..." "..." >
+<!DOCTYPE Something SYSTEM "... ..." [ ... ] >
+<!DOCTYPE Something SYSTEM "... ..." >
+<!DOCTYPE Something [ ... ] >
+<!DOCTYPE Something >
+</typing>
+
+<p>The code may look a bit complex but this is mostly due to the fact that we
+resolve namespaces and attach metatables. There is only one public function:</p>
+
+<typing>
+local x = xml.convert(somestring)
+</typing>
+
+<p>An optional second boolean argument tells this function not to create a root
+element.</p>
+
+<p>Valid entities are:</p>
+
+<typing>
+<!ENTITY xxxx SYSTEM "yyyy" NDATA zzzz>
+<!ENTITY xxxx PUBLIC "yyyy" >
+<!ENTITY xxxx "yyyy" >
+</typing>
+--ldx]]--
+
+-- not just one big nested table capture (lpeg overflow)
+
+local nsremap, resolvens = xml.xmlns, xml.resolvens
+
+local stack, top, dt, at, xmlns, errorstr, entities = { }, { }, { }, { }, { }, nil, { }
+local strip, cleanup, utfize, resolve, resolve_predefined, unify_predefined = false, false, false, false, false, false
+local dcache, hcache, acache = { }, { }, { }
+
+local mt = { }
+
+function initialize_mt(root)
+    mt = { __index = root } -- will be redefined later
+end
+
+function xml.setproperty(root,k,v)
+    getmetatable(root).__index[k] = v
+end
+
+function xml.check_error(top,toclose)
+    return ""
+end
+
+local function add_attribute(namespace,tag,value)
+    if cleanup and #value > 0 then
+        value = cleanup(value) -- new
+    end
+    if tag == "xmlns" then
+        xmlns[#xmlns+1] = resolvens(value)
+        at[tag] = value
+    elseif namespace == "" then
+        at[tag] = value
+    elseif namespace == "xmlns" then
+        xml.checkns(tag,value)
+        at["xmlns:" .. tag] = value
+    else
+        -- for the moment this way:
+        at[namespace .. ":" .. tag] = value
+    end
+end
+
+local function add_empty(spacing, namespace, tag)
+    if #spacing > 0 then
+        dt[#dt+1] = spacing
+    end
+    local resolved = (namespace == "" and xmlns[#xmlns]) or nsremap[namespace] or namespace
+    top = stack[#stack]
+    dt = top.dt
+    local t = { ns=namespace or "", rn=resolved, tg=tag, at=at, dt={}, __p__ = top }
+    dt[#dt+1] = t
+    setmetatable(t, mt)
+    if at.xmlns then
+        remove(xmlns)
+    end
+    at = { }
+end
+
+local function add_begin(spacing, namespace, tag)
+    if #spacing > 0 then
+        dt[#dt+1] = spacing
+    end
+    local resolved = (namespace == "" and xmlns[#xmlns]) or nsremap[namespace] or namespace
+    top = { ns=namespace or "", rn=resolved, tg=tag, at=at, dt={}, __p__ = stack[#stack] }
+    setmetatable(top, mt)
+    dt = top.dt
+    stack[#stack+1] = top
+    at = { }
+end
+
+local function add_end(spacing, namespace, tag)
+    if #spacing > 0 then
+        dt[#dt+1] = spacing
+    end
+    local toclose = remove(stack)
+    top = stack[#stack]
+    if #stack < 1 then
+        errorstr = format("nothing to close with %s %s", tag, xml.check_error(top,toclose) or "")
+    elseif toclose.tg ~= tag then -- no namespace check
+        errorstr = format("unable to close %s with %s %s", toclose.tg, tag, xml.check_error(top,toclose) or "")
+    end
+    dt = top.dt
+    dt[#dt+1] = toclose
+ -- dt[0] = top -- nasty circular reference when serializing table
+    if toclose.at.xmlns then
+        remove(xmlns)
+    end
+end
+
+local function add_text(text)
+    if cleanup and #text > 0 then
+        dt[#dt+1] = cleanup(text)
+    else
+        dt[#dt+1] = text
+    end
+end
+
+local function add_special(what, spacing, text)
+    if #spacing > 0 then
+        dt[#dt+1] = spacing
+    end
+    if strip and (what == "@cm@" or what == "@dt@") then
+        -- forget it
+    else
+        dt[#dt+1] = { special=true, ns="", tg=what, dt={ text } }
+    end
+end
+
+local function set_message(txt)
+    errorstr = "garbage at the end of the file: " .. gsub(txt,"([ \n\r\t]*)","")
+end
+
+local reported_attribute_errors = { }
+
+local function attribute_value_error(str)
+    if not reported_attribute_errors[str] then
+        logs.report("xml","invalid attribute value: %q",str)
+        reported_attribute_errors[str] = true
+        at._error_ = str
+    end
+    return str
+end
+local function attribute_specification_error(str)
+    if not reported_attribute_errors[str] then
+        logs.report("xml","invalid attribute specification: %q",str)
+        reported_attribute_errors[str] = true
+        at._error_ = str
+    end
+    return str
+end
+
+function xml.unknown_dec_entity_format(str) return (str == "" and "&error;") or format("&%s;",str) end
+function xml.unknown_hex_entity_format(str) return format("&#x%s;",str) end
+function xml.unknown_any_entity_format(str) return format("&#x%s;",str) end
+
+local function fromhex(s)
+    local n = tonumber(s,16)
+    if n then
+        return utfchar(n)
+    else
+        return format("h:%s",s), true
+    end
+end
+
+local function fromdec(s)
+    local n = tonumber(s)
+    if n then
+        return utfchar(n)
+    else
+        return format("d:%s",s), true
+    end
+end
+
+-- one level expansion (simple case), no checking done
+
+local rest = (1-P(";"))^0
+local many = P(1)^0
+
+local parsedentity =
+    P("&") * (P("#x")*(rest/fromhex) + P("#")*(rest/fromdec)) * P(";") * P(-1) +
+             (P("#x")*(many/fromhex) + P("#")*(many/fromdec))
+
+-- parsing in the xml file
+
+local predefined_unified = {
+    [38] = "&amp;",
+    [42] = "&quot;",
+    [47] = "&apos;",
+    [74] = "&lt;",
+    [76] = "&gr;",
+}
+
+local predefined_simplified = {
+    [38] = "&", amp  = "&",
+    [42] = '"', quot = '"',
+    [47] = "'", apos = "'",
+    [74] = "<", lt   = "<",
+    [76] = ">", gt   = ">",
+}
+
+local function handle_hex_entity(str)
+    local h = hcache[str]
+    if not h then
+        local n = tonumber(str,16)
+        h = unify_predefined and predefined_unified[n]
+        if h then
+            if trace_entities then
+                logs.report("xml","utfize, converting hex entity &#x%s; into %s",str,h)
+            end
+        elseif utfize then
+            h = (n and utfchar(n)) or xml.unknown_hex_entity_format(str) or ""
+            if not n then
+                logs.report("xml","utfize, ignoring hex entity &#x%s;",str)
+            elseif trace_entities then
+                logs.report("xml","utfize, converting hex entity &#x%s; into %s",str,h)
+            end
+        else
+            if trace_entities then
+                logs.report("xml","found entity &#x%s;",str)
+            end
+            h = "&#x" .. str .. ";"
+        end
+        hcache[str] = h
+    end
+    return h
+end
+
+local function handle_dec_entity(str)
+    local d = dcache[str]
+    if not d then
+        local n = tonumber(str)
+        d = unify_predefined and predefined_unified[n]
+        if d then
+            if trace_entities then
+                logs.report("xml","utfize, converting dec entity &#%s; into %s",str,d)
+            end
+        elseif utfize then
+            d = (n and utfchar(n)) or xml.unknown_dec_entity_format(str) or ""
+            if not n then
+                logs.report("xml","utfize, ignoring dec entity &#%s;",str)
+            elseif trace_entities then
+                logs.report("xml","utfize, converting dec entity &#%s; into %s",str,h)
+            end
+        else
+            if trace_entities then
+                logs.report("xml","found entity &#%s;",str)
+            end
+            d = "&#" .. str .. ";"
+        end
+        dcache[str] = d
+    end
+    return d
+end
+
+xml.parsedentitylpeg = parsedentity
+
+local function handle_any_entity(str)
+    if resolve then
+        local a = acache[str] -- per instance ! todo
+        if not a then
+            a = resolve_predefined and predefined_simplified[str]
+            if a then
+                -- one of the predefined
+            elseif type(resolve) == "function" then
+                a = resolve(str) or entities[str]
+            else
+                a = entities[str]
+            end
+            if a then
+                if trace_entities then
+                    logs.report("xml","resolved entity &%s; -> %s (internal)",str,a)
+                end
+                a = lpegmatch(parsedentity,a) or a
+            else
+                if xml.unknown_any_entity_format then
+                    a = xml.unknown_any_entity_format(str) or ""
+                end
+                if a then
+                    if trace_entities then
+                        logs.report("xml","resolved entity &%s; -> %s (external)",str,a)
+                    end
+                else
+                    if trace_entities then
+                        logs.report("xml","keeping entity &%s;",str)
+                    end
+                    if str == "" then
+                        a = "&error;"
+                    else
+                        a = "&" .. str .. ";"
+                    end
+                end
+            end
+            acache[str] = a
+        elseif trace_entities then
+            if not acache[str] then
+                logs.report("xml","converting entity &%s; into %s",str,a)
+                acache[str] = a
+            end
+        end
+        return a
+    else
+        local a = acache[str]
+        if not a then
+            if trace_entities then
+                logs.report("xml","found entity &%s;",str)
+            end
+            a = resolve_predefined and predefined_simplified[str]
+            if a then
+                -- one of the predefined
+                acache[str] = a
+            elseif str == "" then
+                a = "&error;"
+                acache[str] = a
+            else
+                a = "&" .. str .. ";"
+                acache[str] = a
+            end
+        end
+        return a
+    end
+end
+
+local function handle_end_entity(chr)
+    logs.report("xml","error in entity, %q found instead of ';'",chr)
+end
+
+local space            = S(' \r\n\t')
+local open             = P('<')
+local close            = P('>')
+local squote           = S("'")
+local dquote           = S('"')
+local equal            = P('=')
+local slash            = P('/')
+local colon            = P(':')
+local semicolon        = P(';')
+local ampersand        = P('&')
+local valid            = R('az', 'AZ', '09') + S('_-.')
+local name_yes         = C(valid^1) * colon * C(valid^1)
+local name_nop         = C(P(true)) * C(valid^1)
+local name             = name_yes + name_nop
+local utfbom           = lpeg.patterns.utfbom -- no capture
+local spacing          = C(space^0)
+
+----- entitycontent    = (1-open-semicolon)^0
+local anyentitycontent = (1-open-semicolon-space-close)^0
+local hexentitycontent = R("AF","af","09")^0
+local decentitycontent = R("09")^0
+local parsedentity     = P("#")/"" * (
+                                P("x")/"" * (hexentitycontent/handle_hex_entity) +
+                                            (decentitycontent/handle_dec_entity)
+                            ) +             (anyentitycontent/handle_any_entity)
+local entity           = ampersand/"" * parsedentity * ( (semicolon/"") + #(P(1)/handle_end_entity))
+
+local text_unparsed    = C((1-open)^1)
+local text_parsed      = Cs(((1-open-ampersand)^1 + entity)^1)
+
+local somespace        = space^1
+local optionalspace    = space^0
+
+----- value            = (squote * C((1 - squote)^0) * squote) + (dquote * C((1 - dquote)^0) * dquote) -- ampersand and < also invalid in value
+local value            = (squote * Cs((entity + (1 - squote))^0) * squote) + (dquote * Cs((entity + (1 - dquote))^0) * dquote) -- ampersand and < also invalid in value
+
+local endofattributes  = slash * close + close -- recovery of flacky html
+local whatever         = space * name * optionalspace * equal
+local wrongvalue       = C(P(1-whatever-close)^1 + P(1-close)^1) / attribute_value_error
+----- wrongvalue       = C(P(1-whatever-endofattributes)^1 + P(1-endofattributes)^1) / attribute_value_error
+----- wrongvalue       = C(P(1-space-endofattributes)^1) / attribute_value_error
+local wrongvalue       = Cs(P(entity + (1-space-endofattributes))^1) / attribute_value_error
+
+local attributevalue   = value + wrongvalue
+
+local attribute        = (somespace * name * optionalspace * equal * optionalspace * attributevalue) / add_attribute
+----- attributes       = (attribute)^0
+
+local attributes       = (attribute + somespace^-1 * (((1-endofattributes)^1)/attribute_specification_error))^0
+
+local parsedtext       = text_parsed   / add_text
+local unparsedtext     = text_unparsed / add_text
+local balanced         = P { "[" * ((1 - S"[]") + V(1))^0 * "]" } -- taken from lpeg manual, () example
+
+local emptyelement     = (spacing * open         * name * attributes * optionalspace * slash * close) / add_empty
+local beginelement     = (spacing * open         * name * attributes * optionalspace         * close) / add_begin
+local endelement       = (spacing * open * slash * name              * optionalspace         * close) / add_end
+
+local begincomment     = open * P("!--")
+local endcomment       = P("--") * close
+local begininstruction = open * P("?")
+local endinstruction   = P("?") * close
+local begincdata       = open * P("![CDATA[")
+local endcdata         = P("]]") * close
+
+local someinstruction  = C((1 - endinstruction)^0)
+local somecomment      = C((1 - endcomment    )^0)
+local somecdata        = C((1 - endcdata      )^0)
+
+local function normalentity(k,v  ) entities[k] = v end
+local function systementity(k,v,n) entities[k] = v end
+local function publicentity(k,v,n) entities[k] = v end
+
+local begindoctype     = open * P("!DOCTYPE")
+local enddoctype       = close
+local beginset         = P("[")
+local endset           = P("]")
+local doctypename      = C((1-somespace-close)^0)
+local elementdoctype   = optionalspace * P("<!ELEMENT") * (1-close)^0 * close
+
+local normalentitytype = (doctypename * somespace * value)/normalentity
+local publicentitytype = (doctypename * somespace * P("PUBLIC") * somespace * value)/publicentity
+local systementitytype = (doctypename * somespace * P("SYSTEM") * somespace * value * somespace * P("NDATA") * somespace * doctypename)/systementity
+local entitydoctype    = optionalspace * P("<!ENTITY") * somespace * (systementitytype + publicentitytype + normalentitytype) * optionalspace * close
+
+local doctypeset       = beginset * optionalspace * P(elementdoctype + entitydoctype + space)^0 * optionalspace * endset
+local definitiondoctype= doctypename * somespace * doctypeset
+local publicdoctype    = doctypename * somespace * P("PUBLIC") * somespace * value * somespace * value * somespace * doctypeset
+local systemdoctype    = doctypename * somespace * P("SYSTEM") * somespace * value * somespace * doctypeset
+local simpledoctype    = (1-close)^1 -- * balanced^0
+local somedoctype      = C((somespace * (publicdoctype + systemdoctype + definitiondoctype + simpledoctype) * optionalspace)^0)
+
+local instruction      = (spacing * begininstruction * someinstruction * endinstruction) / function(...) add_special("@pi@",...) end
+local comment          = (spacing * begincomment     * somecomment     * endcomment    ) / function(...) add_special("@cm@",...) end
+local cdata            = (spacing * begincdata       * somecdata       * endcdata      ) / function(...) add_special("@cd@",...) end
+local doctype          = (spacing * begindoctype     * somedoctype     * enddoctype    ) / function(...) add_special("@dt@",...) end
+
+--  nicer but slower:
+--
+--  local instruction = (Cc("@pi@") * spacing * begininstruction * someinstruction * endinstruction) / add_special
+--  local comment     = (Cc("@cm@") * spacing * begincomment     * somecomment     * endcomment    ) / add_special
+--  local cdata       = (Cc("@cd@") * spacing * begincdata       * somecdata       * endcdata      ) / add_special
+--  local doctype     = (Cc("@dt@") * spacing * begindoctype     * somedoctype     * enddoctype    ) / add_special
+
+local trailer = space^0 * (text_unparsed/set_message)^0
+
+--  comment + emptyelement + text + cdata + instruction + V("parent"), -- 6.5 seconds on 40 MB database file
+--  text + comment + emptyelement + cdata + instruction + V("parent"), -- 5.8
+--  text + V("parent") + emptyelement + comment + cdata + instruction, -- 5.5
+
+local grammar_parsed_text = P { "preamble",
+    preamble = utfbom^0 * instruction^0 * (doctype + comment + instruction)^0 * V("parent") * trailer,
+    parent   = beginelement * V("children")^0 * endelement,
+    children = parsedtext + V("parent") + emptyelement + comment + cdata + instruction,
+}
+
+local grammar_unparsed_text = P { "preamble",
+    preamble = utfbom^0 * instruction^0 * (doctype + comment + instruction)^0 * V("parent") * trailer,
+    parent   = beginelement * V("children")^0 * endelement,
+    children = unparsedtext + V("parent") + emptyelement + comment + cdata + instruction,
+}
+
+-- maybe we will add settinsg to result as well
+
+local function xmlconvert(data, settings)
+    settings = settings or { } -- no_root strip_cm_and_dt given_entities parent_root error_handler
+    strip = settings.strip_cm_and_dt
+    utfize = settings.utfize_entities
+    resolve = settings.resolve_entities
+    resolve_predefined = settings.resolve_predefined_entities -- in case we have escaped entities
+    unify_predefined = settings.unify_predefined_entities -- &#038; -> &amp;
+    cleanup = settings.text_cleanup
+    stack, top, at, xmlns, errorstr, result, entities = { }, { }, { }, { }, nil, nil, settings.entities or { }
+    acache, hcache, dcache = { }, { }, { } -- not stored
+    reported_attribute_errors = { }
+    if settings.parent_root then
+        mt = getmetatable(settings.parent_root)
+    else
+        initialize_mt(top)
+    end
+    stack[#stack+1] = top
+    top.dt = { }
+    dt = top.dt
+    if not data or data == "" then
+        errorstr = "empty xml file"
+    elseif utfize or resolve then
+        if lpegmatch(grammar_parsed_text,data) then
+            errorstr = ""
+        else
+            errorstr = "invalid xml file - parsed text"
+        end
+    elseif type(data) == "string" then
+        if lpegmatch(grammar_unparsed_text,data) then
+            errorstr = ""
+        else
+            errorstr = "invalid xml file - unparsed text"
+        end
+    else
+        errorstr = "invalid xml file - no text at all"
+    end
+    if errorstr and errorstr ~= "" then
+        result = { dt = { { ns = "", tg = "error", dt = { errorstr }, at={ }, er = true } } }
+        setmetatable(stack, mt)
+        local error_handler = settings.error_handler
+        if error_handler == false then
+            -- no error message
+        else
+            error_handler = error_handler or xml.error_handler
+            if error_handler then
+                xml.error_handler("load",errorstr)
+            end
+        end
+    else
+        result = stack[1]
+    end
+    if not settings.no_root then
+        result = { special = true, ns = "", tg = '@rt@', dt = result.dt, at={ }, entities = entities, settings = settings }
+        setmetatable(result, mt)
+        local rdt = result.dt
+        for k=1,#rdt do
+            local v = rdt[k]
+            if type(v) == "table" and not v.special then -- always table -)
+                result.ri = k -- rootindex
+v.__p__ = result  -- new, experiment, else we cannot go back to settings, we need to test this !
+                break
+            end
+        end
+    end
+    if errorstr and errorstr ~= "" then
+        result.error = true
+    end
+    return result
+end
+
+xml.convert = xmlconvert
+
+function xml.inheritedconvert(data,xmldata)
+    local settings = xmldata.settings
+    settings.parent_root = xmldata -- to be tested
+ -- settings.no_root = true
+    local xc = xmlconvert(data,settings)
+ -- xc.settings = nil
+ -- xc.entities = nil
+ -- xc.special = nil
+ -- xc.ri = nil
+ -- print(xc.tg)
+    return xc
+end
+
+--[[ldx--
+<p>Packaging data in an xml like table is done with the following
+function. Maybe it will go away (when not used).</p>
+--ldx]]--
+
+function xml.is_valid(root)
+    return root and root.dt and root.dt[1] and type(root.dt[1]) == "table" and not root.dt[1].er
+end
+
+function xml.package(tag,attributes,data)
+    local ns, tg = match(tag,"^(.-):?([^:]+)$")
+    local t = { ns = ns, tg = tg, dt = data or "", at = attributes or {} }
+    setmetatable(t, mt)
+    return t
+end
+
+function xml.is_valid(root)
+    return root and not root.error
+end
+
+xml.error_handler = (logs and logs.report) or (input and logs.report) or print
+
+--[[ldx--
+<p>We cannot load an <l n='lpeg'/> from a filehandle so we need to load
+the whole file first. The function accepts a string representing
+a filename or a file handle.</p>
+--ldx]]--
+
+function xml.load(filename,settings)
+    local data = ""
+    if type(filename) == "string" then
+     -- local data = io.loaddata(filename) - -todo: check type in io.loaddata
+        local f = io.open(filename,'r')
+        if f then
+            data = f:read("*all")
+            f:close()
+        end
+    elseif filename then -- filehandle
+        data = filename:read("*all")
+    end
+    return xmlconvert(data,settings)
+end
+
+--[[ldx--
+<p>When we inject new elements, we need to convert strings to
+valid trees, which is what the next function does.</p>
+--ldx]]--
+
+local no_root = { no_root = true }
+
+function xml.toxml(data)
+    if type(data) == "string" then
+        local root = { xmlconvert(data,no_root) }
+        return (#root > 1 and root) or root[1]
+    else
+        return data
+    end
+end
+
+--[[ldx--
+<p>For copying a tree we use a dedicated function instead of the
+generic table copier. Since we know what we're dealing with we
+can speed up things a bit. The second argument is not to be used!</p>
+--ldx]]--
+
+local function copy(old,tables)
+    if old then
+        tables = tables or { }
+        local new = { }
+        if not tables[old] then
+            tables[old] = new
+        end
+        for k,v in next, old do
+            new[k] = (type(v) == "table" and (tables[v] or copy(v, tables))) or v
+        end
+        local mt = getmetatable(old)
+        if mt then
+            setmetatable(new,mt)
+        end
+        return new
+    else
+        return { }
+    end
+end
+
+xml.copy = copy
+
+--[[ldx--
+<p>In <l n='context'/> serializing the tree or parts of the tree is a major
+actitivity which is why the following function is pretty optimized resulting
+in a few more lines of code than needed. The variant that uses the formatting
+function for all components is about 15% slower than the concatinating
+alternative.</p>
+--ldx]]--
+
+-- todo: add <?xml version='1.0' standalone='yes'?> when not present
+
+function xml.checkbom(root) -- can be made faster
+    if root.ri then
+        local dt, found = root.dt, false
+        for k=1,#dt do
+            local v = dt[k]
+            if type(v) == "table" and v.special and v.tg == "@pi@" and find(v.dt[1],"xml.*version=") then
+                found = true
+                break
+            end
+        end
+        if not found then
+            insert(dt, 1, { special=true, ns="", tg="@pi@", dt = { "xml version='1.0' standalone='yes'"} } )
+            insert(dt, 2, "\n" )
+        end
+    end
+end
+
+--[[ldx--
+<p>At the cost of some 25% runtime overhead you can first convert the tree to a string
+and then handle the lot.</p>
+--ldx]]--
+
+-- new experimental reorganized serialize
+
+local function verbose_element(e,handlers)
+    local handle = handlers.handle
+    local serialize = handlers.serialize
+    local ens, etg, eat, edt, ern = e.ns, e.tg, e.at, e.dt, e.rn
+    local ats = eat and next(eat) and { }
+    if ats then
+        for k,v in next, eat do
+            ats[#ats+1] = format('%s=%q',k,v)
+        end
+    end
+    if ern and trace_remap and ern ~= ens then
+        ens = ern
+    end
+    if ens ~= "" then
+        if edt and #edt > 0 then
+            if ats then
+                handle("<",ens,":",etg," ",concat(ats," "),">")
+            else
+                handle("<",ens,":",etg,">")
+            end
+            for i=1,#edt do
+                local e = edt[i]
+                if type(e) == "string" then
+                    handle(e)
+                else
+                    serialize(e,handlers)
+                end
+            end
+            handle("</",ens,":",etg,">")
+        else
+            if ats then
+                handle("<",ens,":",etg," ",concat(ats," "),"/>")
+            else
+                handle("<",ens,":",etg,"/>")
+            end
+        end
+    else
+        if edt and #edt > 0 then
+            if ats then
+                handle("<",etg," ",concat(ats," "),">")
+            else
+                handle("<",etg,">")
+            end
+            for i=1,#edt do
+                local ei = edt[i]
+                if type(ei) == "string" then
+                    handle(ei)
+                else
+                    serialize(ei,handlers)
+                end
+            end
+            handle("</",etg,">")
+        else
+            if ats then
+                handle("<",etg," ",concat(ats," "),"/>")
+            else
+                handle("<",etg,"/>")
+            end
+        end
+    end
+end
+
+local function verbose_pi(e,handlers)
+    handlers.handle("<?",e.dt[1],"?>")
+end
+
+local function verbose_comment(e,handlers)
+    handlers.handle("<!--",e.dt[1],"-->")
+end
+
+local function verbose_cdata(e,handlers)
+    handlers.handle("<![CDATA[", e.dt[1],"]]>")
+end
+
+local function verbose_doctype(e,handlers)
+    handlers.handle("<!DOCTYPE ",e.dt[1],">")
+end
+
+local function verbose_root(e,handlers)
+    handlers.serialize(e.dt,handlers)
+end
+
+local function verbose_text(e,handlers)
+    handlers.handle(e)
+end
+
+local function verbose_document(e,handlers)
+    local serialize = handlers.serialize
+    local functions = handlers.functions
+    for i=1,#e do
+        local ei = e[i]
+        if type(ei) == "string" then
+            functions["@tx@"](ei,handlers)
+        else
+            serialize(ei,handlers)
+        end
+    end
+end
+
+local function serialize(e,handlers,...)
+    local initialize = handlers.initialize
+    local finalize   = handlers.finalize
+    local functions  = handlers.functions
+    if initialize then
+        local state = initialize(...)
+        if not state == true then
+            return state
+        end
+    end
+    local etg = e.tg
+    if etg then
+        (functions[etg] or functions["@el@"])(e,handlers)
+ -- elseif type(e) == "string" then
+ --     functions["@tx@"](e,handlers)
+    else
+        functions["@dc@"](e,handlers)
+    end
+    if finalize then
+        return finalize()
+    end
+end
+
+local function xserialize(e,handlers)
+    local functions = handlers.functions
+    local etg = e.tg
+    if etg then
+        (functions[etg] or functions["@el@"])(e,handlers)
+ -- elseif type(e) == "string" then
+ --     functions["@tx@"](e,handlers)
+    else
+        functions["@dc@"](e,handlers)
+    end
+end
+
+local handlers = { }
+
+local function newhandlers(settings)
+    local t = table.copy(handlers.verbose or { }) -- merge
+    if settings then
+        for k,v in next, settings do
+            if type(v) == "table" then
+                tk = t[k] if not tk then tk = { } t[k] = tk end
+                for kk,vv in next, v do
+                    tk[kk] = vv
+                end
+            else
+                t[k] = v
+            end
+        end
+        if settings.name then
+            handlers[settings.name] = t
+        end
+    end
+    return t
+end
+
+local nofunction = function() end
+
+function xml.sethandlersfunction(handler,name,fnc)
+    handler.functions[name] = fnc or nofunction
+end
+
+function xml.gethandlersfunction(handler,name)
+    return handler.functions[name]
+end
+
+function xml.gethandlers(name)
+    return handlers[name]
+end
+
+newhandlers {
+    name       = "verbose",
+    initialize = false, -- faster than nil and mt lookup
+    finalize   = false, -- faster than nil and mt lookup
+    serialize  = xserialize,
+    handle     = print,
+    functions  = {
+        ["@dc@"]   = verbose_document,
+        ["@dt@"]   = verbose_doctype,
+        ["@rt@"]   = verbose_root,
+        ["@el@"]   = verbose_element,
+        ["@pi@"]   = verbose_pi,
+        ["@cm@"]   = verbose_comment,
+        ["@cd@"]   = verbose_cdata,
+        ["@tx@"]   = verbose_text,
+    }
+}
+
+--[[ldx--
+<p>How you deal with saving data depends on your preferences. For a 40 MB database
+file the timing on a 2.3 Core Duo are as follows (time in seconds):</p>
+
+<lines>
+1.3 : load data from file to string
+6.1 : convert string into tree
+5.3 : saving in file using xmlsave
+6.8 : converting to string using xml.tostring
+3.6 : saving converted string in file
+</lines>
+
+<p>Beware, these were timing with the old routine but measurements will not be that
+much different I guess.</p>
+--ldx]]--
+
+-- maybe this will move to lxml-xml
+
+local result
+
+local xmlfilehandler = newhandlers {
+    name       = "file",
+    initialize = function(name) result = io.open(name,"wb") return result end,
+    finalize   = function() result:close() return true end,
+    handle     = function(...) result:write(...) end,
+}
+
+-- no checking on writeability here but not faster either
+--
+-- local xmlfilehandler = newhandlers {
+--     initialize = function(name) io.output(name,"wb") return true end,
+--     finalize   = function() io.close() return true end,
+--     handle     = io.write,
+-- }
+
+
+function xml.save(root,name)
+    serialize(root,xmlfilehandler,name)
+end
+
+local result
+
+local xmlstringhandler = newhandlers {
+    name       = "string",
+    initialize = function() result = { } return result end,
+    finalize   = function() return concat(result) end,
+    handle     = function(...) result[#result+1] = concat { ... } end
+}
+
+local function xmltostring(root) -- 25% overhead due to collecting
+    if root then
+        if type(root) == 'string' then
+            return root
+        else -- if next(root) then -- next is faster than type (and >0 test)
+            return serialize(root,xmlstringhandler) or ""
+        end
+    end
+    return ""
+end
+
+local function xmltext(root) -- inline
+    return (root and xmltostring(root)) or ""
+end
+
+function initialize_mt(root)
+    mt = { __tostring = xmltext, __index = root }
+end
+
+xml.defaulthandlers = handlers
+xml.newhandlers     = newhandlers
+xml.serialize       = serialize
+xml.tostring        = xmltostring
+
+--[[ldx--
+<p>The next function operated on the content only and needs a handle function
+that accepts a string.</p>
+--ldx]]--
+
+local function xmlstring(e,handle)
+    if not handle or (e.special and e.tg ~= "@rt@") then
+        -- nothing
+    elseif e.tg then
+        local edt = e.dt
+        if edt then
+            for i=1,#edt do
+                xmlstring(edt[i],handle)
+            end
+        end
+    else
+        handle(e)
+    end
+end
+
+xml.string = xmlstring
+
+--[[ldx--
+<p>A few helpers:</p>
+--ldx]]--
+
+--~ xmlsetproperty(root,"settings",settings)
+
+function xml.settings(e)
+    while e do
+        local s = e.settings
+        if s then
+            return s
+        else
+            e = e.__p__
+        end
+    end
+    return nil
+end
+
+function xml.root(e)
+    local r = e
+    while e do
+        e = e.__p__
+        if e then
+            r = e
+        end
+    end
+    return r
+end
+
+function xml.parent(root)
+    return root.__p__
+end
+
+function xml.body(root)
+    return (root.ri and root.dt[root.ri]) or root -- not ok yet
+end
+
+function xml.name(root)
+    if not root then
+        return ""
+    elseif root.ns == "" then
+        return root.tg
+    else
+        return root.ns .. ":" .. root.tg
+    end
+end
+
+--[[ldx--
+<p>The next helper erases an element but keeps the table as it is,
+and since empty strings are not serialized (effectively) it does
+not harm. Copying the table would take more time. Usage:</p>
+--ldx]]--
+
+function xml.erase(dt,k)
+    if dt then
+        if k then
+            dt[k] = ""
+        else for k=1,#dt do
+            dt[1] = { "" }
+        end end
+    end
+end
+
+--[[ldx--
+<p>The next helper assigns a tree (or string). Usage:</p>
+
+<typing>
+dt[k] = xml.assign(root) or xml.assign(dt,k,root)
+</typing>
+--ldx]]--
+
+function xml.assign(dt,k,root)
+    if dt and k then
+        dt[k] = (type(root) == "table" and xml.body(root)) or root
+        return dt[k]
+    else
+        return xml.body(root)
+    end
+end
+
+-- the following helpers may move
+
+--[[ldx--
+<p>The next helper assigns a tree (or string). Usage:</p>
+<typing>
+xml.tocdata(e)
+xml.tocdata(e,"error")
+</typing>
+--ldx]]--
+
+function xml.tocdata(e,wrapper)
+    local whatever = xmltostring(e.dt)
+    if wrapper then
+        whatever = format("<%s>%s</%s>",wrapper,whatever,wrapper)
+    end
+    local t = { special = true, ns = "", tg = "@cd@", at = {}, rn = "", dt = { whatever }, __p__ = e }
+    setmetatable(t,getmetatable(e))
+    e.dt = { t }
+end
+
+function xml.makestandalone(root)
+    if root.ri then
+        local dt = root.dt
+        for k=1,#dt do
+            local v = dt[k]
+            if type(v) == "table" and v.special and v.tg == "@pi@" then
+                local txt = v.dt[1]
+                if find(txt,"xml.*version=") then
+                    v.dt[1] = txt .. " standalone='yes'"
+                    break
+                end
+            end
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['lxml-pth'] = {
+    version   = 1.001,
+    comment   = "this module is the basis for the lxml-* ones",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- e.ni is only valid after a filter run
+
+local concat, remove, insert = table.concat, table.remove, table.insert
+local type, next, tonumber, tostring, setmetatable, loadstring = type, next, tonumber, tostring, setmetatable, loadstring
+local format, upper, lower, gmatch, gsub, find, rep = string.format, string.upper, string.lower, string.gmatch, string.gsub, string.find, string.rep
+local lpegmatch = lpeg.match
+
+-- beware, this is not xpath ... e.g. position is different (currently) and
+-- we have reverse-sibling as reversed preceding sibling
+
+--[[ldx--
+<p>This module can be used stand alone but also inside <l n='mkiv'/> in
+which case it hooks into the tracker code. Therefore we provide a few
+functions that set the tracers. Here we overload a previously defined
+function.</p>
+<p>If I can get in the mood I will make a variant that is XSLT compliant
+but I wonder if it makes sense.</P>
+--ldx]]--
+
+--[[ldx--
+<p>Expecially the lpath code is experimental, we will support some of xpath, but
+only things that make sense for us; as compensation it is possible to hook in your
+own functions. Apart from preprocessing content for <l n='context'/> we also need
+this module for process management, like handling <l n='ctx'/> and <l n='rlx'/>
+files.</p>
+
+<typing>
+a/b/c /*/c
+a/b/c/first() a/b/c/last() a/b/c/index(n) a/b/c/index(-n)
+a/b/c/text() a/b/c/text(1) a/b/c/text(-1) a/b/c/text(n)
+</typing>
+--ldx]]--
+
+local trace_lpath    = false  if trackers then trackers.register("xml.path",    function(v) trace_lpath  = v end) end
+local trace_lparse   = false  if trackers then trackers.register("xml.parse",   function(v) trace_lparse = v end) end
+local trace_lprofile = false  if trackers then trackers.register("xml.profile", function(v) trace_lpath  = v trace_lparse = v trace_lprofile = v end) end
+
+--[[ldx--
+<p>We've now arrived at an interesting part: accessing the tree using a subset
+of <l n='xpath'/> and since we're not compatible we call it <l n='lpath'/>. We
+will explain more about its usage in other documents.</p>
+--ldx]]--
+
+local lpathcalls  = 0  function xml.lpathcalls () return lpathcalls  end
+local lpathcached = 0  function xml.lpathcached() return lpathcached end
+
+xml.functions      = xml.functions      or { } -- internal
+xml.expressions    = xml.expressions    or { } -- in expressions
+xml.finalizers     = xml.finalizers     or { } -- fast do-with ... (with return value other than collection)
+xml.specialhandler = xml.specialhandler or { }
+
+local functions   = xml.functions
+local expressions = xml.expressions
+local finalizers  = xml.finalizers
+
+finalizers.xml = finalizers.xml or { }
+finalizers.tex = finalizers.tex or { }
+
+local function fallback (t, name)
+    local fn = finalizers[name]
+    if fn then
+        t[name] = fn
+    else
+        logs.report("xml","unknown sub finalizer '%s'",tostring(name))
+        fn = function() end
+    end
+    return fn
+end
+
+setmetatable(finalizers.xml, { __index = fallback })
+setmetatable(finalizers.tex, { __index = fallback })
+
+xml.defaultprotocol = "xml"
+
+-- as xsl does not follow xpath completely here we will also
+-- be more liberal especially with regards to the use of | and
+-- the rootpath:
+--
+-- test    : all 'test' under current
+-- /test   : 'test' relative to current
+-- a|b|c   : set of names
+-- (a|b|c) : idem
+-- !       : not
+--
+-- after all, we're not doing transformations but filtering. in
+-- addition we provide filter functions (last bit)
+--
+-- todo: optimizer
+--
+-- .. : parent
+-- *  : all kids
+-- /  : anchor here
+-- // : /**/
+-- ** : all in between
+--
+-- so far we had (more practical as we don't transform)
+--
+-- {/test}   : kids 'test' under current node
+-- {test}    : any kid with tag 'test'
+-- {//test}  : same as above
+
+-- evaluator (needs to be redone, for the moment copied)
+
+-- todo: apply_axis(list,notable) and collection vs single
+
+local apply_axis = { }
+
+apply_axis['root'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        local rt = ll
+        while ll do
+            ll = ll.__p__
+            if ll then
+                rt = ll
+            end
+        end
+        collected[#collected+1] = rt
+    end
+    return collected
+end
+
+apply_axis['self'] = function(list)
+--~     local collected = { }
+--~     for l=1,#list do
+--~         collected[#collected+1] = list[l]
+--~     end
+--~     return collected
+    return list
+end
+
+apply_axis['child'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        local dt = ll.dt
+        local en = 0
+        for k=1,#dt do
+            local dk = dt[k]
+            if dk.tg then
+                collected[#collected+1] = dk
+                dk.ni = k -- refresh
+            en = en + 1
+            dk.ei = en
+            end
+        end
+        ll.en = en
+    end
+    return collected
+end
+
+local function collect(list,collected)
+    local dt = list.dt
+    if dt then
+        local en = 0
+        for k=1,#dt do
+            local dk = dt[k]
+            if dk.tg then
+                collected[#collected+1] = dk
+                dk.ni = k -- refresh
+                en = en + 1
+                dk.ei = en
+                collect(dk,collected)
+            end
+        end
+        list.en = en
+    end
+end
+apply_axis['descendant'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        collect(list[l],collected)
+    end
+    return collected
+end
+
+local function collect(list,collected)
+    local dt = list.dt
+    if dt then
+        local en = 0
+        for k=1,#dt do
+            local dk = dt[k]
+            if dk.tg then
+                collected[#collected+1] = dk
+                dk.ni = k -- refresh
+                en = en + 1
+                dk.ei = en
+                collect(dk,collected)
+            end
+        end
+        list.en = en
+    end
+end
+apply_axis['descendant-or-self'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        if ll.special ~= true then -- catch double root
+            collected[#collected+1] = ll
+        end
+        collect(ll,collected)
+    end
+    return collected
+end
+
+apply_axis['ancestor'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        while ll do
+            ll = ll.__p__
+            if ll then
+                collected[#collected+1] = ll
+            end
+        end
+    end
+    return collected
+end
+
+apply_axis['ancestor-or-self'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        collected[#collected+1] = ll
+        while ll do
+            ll = ll.__p__
+            if ll then
+                collected[#collected+1] = ll
+            end
+        end
+    end
+    return collected
+end
+
+apply_axis['parent'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local pl = list[l].__p__
+        if pl then
+            collected[#collected+1] = pl
+        end
+    end
+    return collected
+end
+
+apply_axis['attribute'] = function(list)
+    return { }
+end
+
+apply_axis['namespace'] = function(list)
+    return { }
+end
+
+apply_axis['following'] = function(list) -- incomplete
+--~     local collected = { }
+--~     for l=1,#list do
+--~         local ll = list[l]
+--~         local p = ll.__p__
+--~         local d = p.dt
+--~         for i=ll.ni+1,#d do
+--~             local di = d[i]
+--~             if type(di) == "table" then
+--~                 collected[#collected+1] = di
+--~                 break
+--~             end
+--~         end
+--~     end
+--~     return collected
+    return { }
+end
+
+apply_axis['preceding'] = function(list) -- incomplete
+--~     local collected = { }
+--~     for l=1,#list do
+--~         local ll = list[l]
+--~         local p = ll.__p__
+--~         local d = p.dt
+--~         for i=ll.ni-1,1,-1 do
+--~             local di = d[i]
+--~             if type(di) == "table" then
+--~                 collected[#collected+1] = di
+--~                 break
+--~             end
+--~         end
+--~     end
+--~     return collected
+    return { }
+end
+
+apply_axis['following-sibling'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        local p = ll.__p__
+        local d = p.dt
+        for i=ll.ni+1,#d do
+            local di = d[i]
+            if type(di) == "table" then
+                collected[#collected+1] = di
+            end
+        end
+    end
+    return collected
+end
+
+apply_axis['preceding-sibling'] = function(list)
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        local p = ll.__p__
+        local d = p.dt
+        for i=1,ll.ni-1 do
+            local di = d[i]
+            if type(di) == "table" then
+                collected[#collected+1] = di
+            end
+        end
+    end
+    return collected
+end
+
+apply_axis['reverse-sibling'] = function(list) -- reverse preceding
+    local collected = { }
+    for l=1,#list do
+        local ll = list[l]
+        local p = ll.__p__
+        local d = p.dt
+        for i=ll.ni-1,1,-1 do
+            local di = d[i]
+            if type(di) == "table" then
+                collected[#collected+1] = di
+            end
+        end
+    end
+    return collected
+end
+
+apply_axis['auto-descendant-or-self'] = apply_axis['descendant-or-self']
+apply_axis['auto-descendant']         = apply_axis['descendant']
+apply_axis['auto-child']              = apply_axis['child']
+apply_axis['auto-self']               = apply_axis['self']
+apply_axis['initial-child']           = apply_axis['child']
+
+local function apply_nodes(list,directive,nodes)
+    -- todo: nodes[1] etc ... negated node name in set ... when needed
+    -- ... currently ignored
+    local maxn = #nodes
+    if maxn == 3 then --optimized loop
+        local nns, ntg = nodes[2], nodes[3]
+        if not nns and not ntg then -- wildcard
+            if directive then
+                return list
+            else
+                return { }
+            end
+        else
+            local collected, m, p = { }, 0, nil
+            if not nns then -- only check tag
+                for l=1,#list do
+                    local ll = list[l]
+                    local ltg = ll.tg
+                    if ltg then
+                        if directive then
+                            if ntg == ltg then
+                                local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                                collected[#collected+1], ll.mi = ll, m
+                            end
+                        elseif ntg ~= ltg then
+                            local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                            collected[#collected+1], ll.mi = ll, m
+                        end
+                    end
+                end
+            elseif not ntg then -- only check namespace
+                for l=1,#list do
+                    local ll = list[l]
+                    local lns = ll.rn or ll.ns
+                    if lns then
+                        if directive then
+                            if lns == nns then
+                                local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                                collected[#collected+1], ll.mi = ll, m
+                            end
+                        elseif lns ~= nns then
+                            local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                            collected[#collected+1], ll.mi = ll, m
+                        end
+                    end
+                end
+            else -- check both
+                for l=1,#list do
+                    local ll = list[l]
+                    local ltg = ll.tg
+                    if ltg then
+                        local lns = ll.rn or ll.ns
+                        local ok = ltg == ntg and lns == nns
+                        if directive then
+                            if ok then
+                                local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                                collected[#collected+1], ll.mi = ll, m
+                            end
+                        elseif not ok then
+                            local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                            collected[#collected+1], ll.mi = ll, m
+                        end
+                    end
+                end
+            end
+            return collected
+        end
+    else
+        local collected, m, p = { }, 0, nil
+        for l=1,#list do
+            local ll = list[l]
+            local ltg = ll.tg
+            if ltg then
+                local lns = ll.rn or ll.ns
+                local ok = false
+                for n=1,maxn,3 do
+                    local nns, ntg = nodes[n+1], nodes[n+2]
+                    ok = (not ntg or ltg == ntg) and (not nns or lns == nns)
+                    if ok then
+                        break
+                    end
+                end
+                if directive then
+                    if ok then
+                        local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                        collected[#collected+1], ll.mi = ll, m
+                    end
+                elseif not ok then
+                    local llp = ll.__p__ ; if llp ~= p then p, m = llp, 1 else m = m + 1 end
+                    collected[#collected+1], ll.mi = ll, m
+                end
+            end
+        end
+        return collected
+    end
+end
+
+local quit_expression = false
+
+local function apply_expression(list,expression,order)
+    local collected = { }
+    quit_expression = false
+    for l=1,#list do
+        local ll = list[l]
+        if expression(list,ll,l,order) then -- nasty, order alleen valid als n=1
+            collected[#collected+1] = ll
+        end
+        if quit_expression then
+            break
+        end
+    end
+    return collected
+end
+
+local P, V, C, Cs, Cc, Ct, R, S, Cg, Cb = lpeg.P, lpeg.V, lpeg.C, lpeg.Cs, lpeg.Cc, lpeg.Ct, lpeg.R, lpeg.S, lpeg.Cg, lpeg.Cb
+
+local spaces     = S(" \n\r\t\f")^0
+local lp_space   = S(" \n\r\t\f")
+local lp_any     = P(1)
+local lp_noequal = P("!=") / "~=" + P("<=") + P(">=") + P("==")
+local lp_doequal = P("=")  / "=="
+local lp_or      = P("|")  / " or "
+local lp_and     = P("&")  / " and "
+
+local lp_builtin = P (
+        P("firstindex")   / "1" +
+        P("lastindex")    / "(#ll.__p__.dt or 1)" +
+        P("firstelement") / "1" +
+        P("lastelement")  / "(ll.__p__.en or 1)" +
+        P("first")        / "1" +
+        P("last")         / "#list" +
+        P("rootposition") / "order" +
+        P("position")     / "l" + -- is element in finalizer
+        P("order")        / "order" +
+        P("element")      / "(ll.ei or 1)" +
+        P("index")        / "(ll.ni or 1)" +
+        P("match")        / "(ll.mi or 1)" +
+        P("text")         / "(ll.dt[1] or '')" +
+    --  P("name")         / "(ll.ns~='' and ll.ns..':'..ll.tg)" +
+        P("name")         / "((ll.ns~='' and ll.ns..':'..ll.tg) or ll.tg)" +
+        P("tag")          / "ll.tg" +
+        P("ns")           / "ll.ns"
+    ) * ((spaces * P("(") * spaces * P(")"))/"")
+
+local lp_attribute = (P("@") + P("attribute::")) / "" * Cc("(ll.at and ll.at['") * R("az","AZ","--","__")^1 * Cc("'])")
+local lp_fastpos_p = ((P("+")^0 * R("09")^1 * P(-1)) / function(s) return "l==" .. s end)
+local lp_fastpos_n = ((P("-")   * R("09")^1 * P(-1)) / function(s) return "(" .. s .. "<0 and (#list+".. s .. "==l))" end)
+local lp_fastpos   = lp_fastpos_n + lp_fastpos_p
+local lp_reserved  = C("and") + C("or") + C("not") + C("div") + C("mod") + C("true") + C("false")
+
+local lp_lua_function  = C(R("az","AZ","__")^1 * (P(".") * R("az","AZ","__")^1)^1) * ("(") / function(t) -- todo: better . handling
+    return t .. "("
+end
+
+local lp_function  = C(R("az","AZ","__")^1) * P("(") / function(t) -- todo: better . handling
+    if expressions[t] then
+        return "expr." .. t .. "("
+    else
+        return "expr.error("
+    end
+end
+
+local lparent  = lpeg.P("(")
+local rparent  = lpeg.P(")")
+local noparent = 1 - (lparent+rparent)
+local nested   = lpeg.P{lparent * (noparent + lpeg.V(1))^0 * rparent}
+local value    = lpeg.P(lparent * lpeg.C((noparent + nested)^0) * rparent) -- lpeg.P{"("*C(((1-S("()"))+V(1))^0)*")"}
+
+local lp_child   = Cc("expr.child(ll,'") * R("az","AZ","--","__")^1 * Cc("')")
+local lp_number  = S("+-") * R("09")^1
+local lp_string  = Cc("'") * R("az","AZ","--","__")^1 * Cc("'")
+local lp_content = (P("'") * (1-P("'"))^0 * P("'") + P('"') * (1-P('"'))^0 * P('"'))
+
+local cleaner
+
+local lp_special = (C(P("name")+P("text")+P("tag")+P("count")+P("child"))) * value / function(t,s)
+    if expressions[t] then
+        s = s and s ~= "" and lpegmatch(cleaner,s)
+        if s and s ~= "" then
+            return "expr." .. t .. "(ll," .. s ..")"
+        else
+            return "expr." .. t .. "(ll)"
+        end
+    else
+        return "expr.error(" .. t .. ")"
+    end
+end
+
+local content =
+    lp_builtin +
+    lp_attribute +
+    lp_special +
+    lp_noequal + lp_doequal +
+    lp_or + lp_and +
+    lp_reserved +
+    lp_lua_function + lp_function +
+    lp_content + -- too fragile
+    lp_child +
+    lp_any
+
+local converter = Cs (
+    lp_fastpos + (P { lparent * (V(1))^0 * rparent + content } )^0
+)
+
+cleaner = Cs ( (
+--~     lp_fastpos +
+    lp_reserved +
+    lp_number +
+    lp_string +
+1 )^1 )
+
+
+--~ expr
+
+local template_e = [[
+    local expr = xml.expressions
+    return function(list,ll,l,order)
+        return %s
+    end
+]]
+
+local template_f_y = [[
+    local finalizer = xml.finalizers['%s']['%s']
+    return function(collection)
+        return finalizer(collection,%s)
+    end
+]]
+
+local template_f_n = [[
+    return xml.finalizers['%s']['%s']
+]]
+
+--
+
+local register_self                    = { kind = "axis", axis = "self"                    } -- , apply = apply_axis["self"]               }
+local register_parent                  = { kind = "axis", axis = "parent"                  } -- , apply = apply_axis["parent"]             }
+local register_descendant              = { kind = "axis", axis = "descendant"              } -- , apply = apply_axis["descendant"]         }
+local register_child                   = { kind = "axis", axis = "child"                   } -- , apply = apply_axis["child"]              }
+local register_descendant_or_self      = { kind = "axis", axis = "descendant-or-self"      } -- , apply = apply_axis["descendant-or-self"] }
+local register_root                    = { kind = "axis", axis = "root"                    } -- , apply = apply_axis["root"]               }
+local register_ancestor                = { kind = "axis", axis = "ancestor"                } -- , apply = apply_axis["ancestor"]           }
+local register_ancestor_or_self        = { kind = "axis", axis = "ancestor-or-self"        } -- , apply = apply_axis["ancestor-or-self"]   }
+local register_attribute               = { kind = "axis", axis = "attribute"               } -- , apply = apply_axis["attribute"]          }
+local register_namespace               = { kind = "axis", axis = "namespace"               } -- , apply = apply_axis["namespace"]          }
+local register_following               = { kind = "axis", axis = "following"               } -- , apply = apply_axis["following"]          }
+local register_following_sibling       = { kind = "axis", axis = "following-sibling"       } -- , apply = apply_axis["following-sibling"]  }
+local register_preceding               = { kind = "axis", axis = "preceding"               } -- , apply = apply_axis["preceding"]          }
+local register_preceding_sibling       = { kind = "axis", axis = "preceding-sibling"       } -- , apply = apply_axis["preceding-sibling"]  }
+local register_reverse_sibling         = { kind = "axis", axis = "reverse-sibling"         } -- , apply = apply_axis["reverse-sibling"]    }
+
+local register_auto_descendant_or_self = { kind = "axis", axis = "auto-descendant-or-self" } -- , apply = apply_axis["auto-descendant-or-self"] }
+local register_auto_descendant         = { kind = "axis", axis = "auto-descendant"         } -- , apply = apply_axis["auto-descendant"] }
+local register_auto_self               = { kind = "axis", axis = "auto-self"               } -- , apply = apply_axis["auto-self"] }
+local register_auto_child              = { kind = "axis", axis = "auto-child"              } -- , apply = apply_axis["auto-child"] }
+
+local register_initial_child           = { kind = "axis", axis = "initial-child"           } -- , apply = apply_axis["initial-child"] }
+
+local register_all_nodes               = { kind = "nodes", nodetest = true, nodes = { true, false, false } }
+
+local skip = { }
+
+local function errorrunner_e(str,cnv)
+    if not skip[str] then
+        logs.report("lpath","error in expression: %s => %s",str,cnv)
+        skip[str] = cnv or str
+    end
+    return false
+end
+local function errorrunner_f(str,arg)
+    logs.report("lpath","error in finalizer: %s(%s)",str,arg or "")
+    return false
+end
+
+local function register_nodes(nodetest,nodes)
+    return { kind = "nodes", nodetest = nodetest, nodes = nodes }
+end
+
+local function register_expression(expression)
+    local converted = lpegmatch(converter,expression)
+    local runner = loadstring(format(template_e,converted))
+    runner = (runner and runner()) or function() errorrunner_e(expression,converted) end
+    return { kind = "expression", expression = expression, converted = converted, evaluator = runner }
+end
+
+local function register_finalizer(protocol,name,arguments)
+    local runner
+    if arguments and arguments ~= "" then
+        runner = loadstring(format(template_f_y,protocol or xml.defaultprotocol,name,arguments))
+    else
+        runner = loadstring(format(template_f_n,protocol or xml.defaultprotocol,name))
+    end
+    runner = (runner and runner()) or function() errorrunner_f(name,arguments) end
+    return { kind = "finalizer", name = name, arguments = arguments, finalizer = runner }
+end
+
+local expression = P { "ex",
+    ex = "[" * C((V("sq") + V("dq") + (1 - S("[]")) + V("ex"))^0) * "]",
+    sq = "'" * (1 - S("'"))^0 * "'",
+    dq = '"' * (1 - S('"'))^0 * '"',
+}
+
+local arguments = P { "ar",
+    ar = "(" * Cs((V("sq") + V("dq") + V("nq") + P(1-P(")")))^0) * ")",
+    nq = ((1 - S("),'\""))^1) / function(s) return format("%q",s) end,
+    sq = P("'") * (1 - P("'"))^0 * P("'"),
+    dq = P('"') * (1 - P('"'))^0 * P('"'),
+}
+
+-- todo: better arg parser
+
+local function register_error(str)
+    return { kind = "error", error = format("unparsed: %s",str) }
+end
+
+-- there is a difference in * and /*/ and so we need to catch a few special cases
+
+local special_1 = P("*")  * Cc(register_auto_descendant) * Cc(register_all_nodes) -- last one not needed
+local special_2 = P("/")  * Cc(register_auto_self)
+local special_3 = P("")   * Cc(register_auto_self)
+
+local parser = Ct { "patterns", -- can be made a bit faster by moving pattern outside
+
+    patterns             = spaces * V("protocol") * spaces * (
+                              ( V("special") * spaces * P(-1)                                                         ) +
+                              ( V("initial") * spaces * V("step") * spaces * (P("/") * spaces * V("step") * spaces)^0 )
+                           ),
+
+    protocol             = Cg(V("letters"),"protocol") * P("://") + Cg(Cc(nil),"protocol"),
+
+ -- the / is needed for // as descendant or self is somewhat special
+ -- step                 = (V("shortcuts") + V("axis") * spaces * V("nodes")^0 + V("error")) * spaces * V("expressions")^0 * spaces * V("finalizer")^0,
+    step                 = ((V("shortcuts") + P("/") + V("axis")) * spaces * V("nodes")^0 + V("error")) * spaces * V("expressions")^0 * spaces * V("finalizer")^0,
+
+    axis                 = V("descendant") + V("child") + V("parent") + V("self") + V("root") + V("ancestor") +
+                           V("descendant_or_self") + V("following_sibling") + V("following") +
+                           V("reverse_sibling") + V("preceding_sibling") + V("preceding") + V("ancestor_or_self") +
+                           #(1-P(-1)) * Cc(register_auto_child),
+
+    special              = special_1 + special_2 + special_3,
+
+    initial              = (P("/") * spaces * Cc(register_initial_child))^-1,
+
+    error                = (P(1)^1) / register_error,
+
+    shortcuts_a          = V("s_descendant_or_self") + V("s_descendant") + V("s_child") + V("s_parent") + V("s_self") + V("s_root") + V("s_ancestor"),
+
+    shortcuts            = V("shortcuts_a") * (spaces * "/" * spaces * V("shortcuts_a"))^0,
+
+    s_descendant_or_self = (P("***/") + P("/"))  * Cc(register_descendant_or_self), --- *** is a bonus
+ -- s_descendant_or_self = P("/")                * Cc(register_descendant_or_self),
+    s_descendant         = P("**")               * Cc(register_descendant),
+    s_child              = P("*") * #(1-P(":"))  * Cc(register_child     ),
+--  s_child              = P("*") * #(P("/")+P(-1)) * Cc(register_child     ),
+    s_parent             = P("..")               * Cc(register_parent    ),
+    s_self               = P("." )               * Cc(register_self      ),
+    s_root               = P("^^")               * Cc(register_root      ),
+    s_ancestor           = P("^")                * Cc(register_ancestor  ),
+
+    descendant           = P("descendant::")         * Cc(register_descendant         ),
+    child                = P("child::")              * Cc(register_child              ),
+    parent               = P("parent::")             * Cc(register_parent             ),
+    self                 = P("self::")               * Cc(register_self               ),
+    root                 = P('root::')               * Cc(register_root               ),
+    ancestor             = P('ancestor::')           * Cc(register_ancestor           ),
+    descendant_or_self   = P('descendant-or-self::') * Cc(register_descendant_or_self ),
+    ancestor_or_self     = P('ancestor-or-self::')   * Cc(register_ancestor_or_self   ),
+ -- attribute            = P('attribute::')          * Cc(register_attribute          ),
+ -- namespace            = P('namespace::')          * Cc(register_namespace          ),
+    following            = P('following::')          * Cc(register_following          ),
+    following_sibling    = P('following-sibling::')  * Cc(register_following_sibling  ),
+    preceding            = P('preceding::')          * Cc(register_preceding          ),
+    preceding_sibling    = P('preceding-sibling::')  * Cc(register_preceding_sibling  ),
+    reverse_sibling      = P('reverse-sibling::')    * Cc(register_reverse_sibling    ),
+
+    nodes                = (V("nodefunction") * spaces * P("(") * V("nodeset") * P(")") + V("nodetest") * V("nodeset")) / register_nodes,
+
+    expressions          = expression / register_expression,
+
+    letters              = R("az")^1,
+    name                 = (1-lpeg.S("/[]()|:*!"))^1,
+    negate               = P("!") * Cc(false),
+
+    nodefunction         = V("negate") + P("not") * Cc(false) + Cc(true),
+    nodetest             = V("negate") + Cc(true),
+    nodename             = (V("negate") + Cc(true)) * spaces * ((V("wildnodename") * P(":") * V("wildnodename")) + (Cc(false) * V("wildnodename"))),
+    wildnodename         = (C(V("name")) + P("*") * Cc(false)) * #(1-P("(")),
+    nodeset              = spaces * Ct(V("nodename") * (spaces * P("|") * spaces * V("nodename"))^0) * spaces,
+
+    finalizer            = (Cb("protocol") * P("/")^-1 * C(V("name")) * arguments * P(-1)) / register_finalizer,
+
+}
+
+local cache = { }
+
+local function nodesettostring(set,nodetest)
+    local t = { }
+    for i=1,#set,3 do
+        local directive, ns, tg = set[i], set[i+1], set[i+2]
+        if not ns or ns == "" then ns = "*" end
+        if not tg or tg == "" then tg = "*" end
+        tg = (tg == "@rt@" and "[root]") or format("%s:%s",ns,tg)
+        t[#t+1] = (directive and tg) or format("not(%s)",tg)
+    end
+    if nodetest == false then
+        return format("not(%s)",concat(t,"|"))
+    else
+        return concat(t,"|")
+    end
+end
+
+local function tagstostring(list)
+    if #list == 0 then
+        return "no elements"
+    else
+        local t = { }
+        for i=1, #list do
+            local li = list[i]
+            local ns, tg = li.ns, li.tg
+            if not ns or ns == "" then ns = "*" end
+            if not tg or tg == "" then tg = "*" end
+            t[#t+1] = (tg == "@rt@" and "[root]") or format("%s:%s",ns,tg)
+        end
+        return concat(t," ")
+    end
+end
+
+xml.nodesettostring = nodesettostring
+
+local parse_pattern -- we have a harmless kind of circular reference
+
+local function lshow(parsed)
+    if type(parsed) == "string" then
+        parsed = parse_pattern(parsed)
+    end
+    local s = table.serialize_functions -- ugly
+    table.serialize_functions = false -- ugly
+    logs.report("lpath","%s://%s => %s",parsed.protocol or xml.defaultprotocol,parsed.pattern,table.serialize(parsed,false))
+    table.serialize_functions = s -- ugly
+end
+
+xml.lshow = lshow
+
+local function add_comment(p,str)
+    local pc = p.comment
+    if not pc then
+        p.comment = { str }
+    else
+        pc[#pc+1] = str
+    end
+end
+
+parse_pattern = function (pattern) -- the gain of caching is rather minimal
+    lpathcalls = lpathcalls + 1
+    if type(pattern) == "table" then
+        return pattern
+    else
+        local parsed = cache[pattern]
+        if parsed then
+            lpathcached = lpathcached + 1
+        else
+            parsed = lpegmatch(parser,pattern)
+            if parsed then
+                parsed.pattern = pattern
+                local np = #parsed
+                if np == 0 then
+                    parsed = { pattern = pattern, register_self, state = "parsing error" }
+                    logs.report("lpath","parsing error in '%s'",pattern)
+                    lshow(parsed)
+                else
+                    -- we could have done this with a more complex parser but this
+                    -- is cleaner
+                    local pi = parsed[1]
+                    if pi.axis == "auto-child" then
+                        if false then
+                            add_comment(parsed, "auto-child replaced by auto-descendant-or-self")
+                            parsed[1] = register_auto_descendant_or_self
+                        else
+                            add_comment(parsed, "auto-child replaced by auto-descendant")
+                            parsed[1] = register_auto_descendant
+                        end
+                    elseif pi.axis == "initial-child" and np > 1 and parsed[2].axis then
+                        add_comment(parsed, "initial-child removed") -- we could also make it a auto-self
+                        remove(parsed,1)
+                    end
+                    local np = #parsed -- can have changed
+                    if np > 1 then
+                        local pnp = parsed[np]
+                        if pnp.kind == "nodes" and pnp.nodetest == true then
+                            local nodes = pnp.nodes
+                            if nodes[1] == true and nodes[2] == false and nodes[3] == false then
+                                add_comment(parsed, "redundant final wildcard filter removed")
+                                remove(parsed,np)
+                            end
+                        end
+                    end
+                end
+            else
+                parsed = { pattern = pattern }
+            end
+            cache[pattern] = parsed
+            if trace_lparse and not trace_lprofile then
+                lshow(parsed)
+            end
+        end
+        return parsed
+    end
+end
+
+-- we can move all calls inline and then merge the trace back
+-- technically we can combine axis and the next nodes which is
+-- what we did before but this a bit cleaner (but slower too)
+-- but interesting is that it's not that much faster when we
+-- go inline
+--
+-- beware: we need to return a collection even when we filter
+-- else the (simple) cache gets messed up
+
+-- caching found lookups saves not that much (max .1 sec on a 8 sec run)
+-- and it also messes up finalizers
+
+-- watch out: when there is a finalizer, it's always called as there
+-- can be cases that a finalizer returns (or does) something in case
+-- there is no match; an example of this is count()
+
+local profiled = { }  xml.profiled = profiled
+
+local function profiled_apply(list,parsed,nofparsed,order)
+    local p = profiled[parsed.pattern]
+    if p then
+        p.tested = p.tested + 1
+    else
+        p = { tested = 1, matched = 0, finalized = 0 }
+        profiled[parsed.pattern] = p
+    end
+    local collected = list
+    for i=1,nofparsed do
+        local pi = parsed[i]
+        local kind = pi.kind
+        if kind == "axis" then
+            collected = apply_axis[pi.axis](collected)
+        elseif kind == "nodes" then
+            collected = apply_nodes(collected,pi.nodetest,pi.nodes)
+        elseif kind == "expression" then
+            collected = apply_expression(collected,pi.evaluator,order)
+        elseif kind == "finalizer" then
+            collected = pi.finalizer(collected)
+            p.matched = p.matched + 1
+            p.finalized = p.finalized + 1
+            return collected
+        end
+        if not collected or #collected == 0 then
+            local pn = i < nofparsed and parsed[nofparsed]
+            if pn and pn.kind == "finalizer" then
+                collected = pn.finalizer(collected)
+                p.finalized = p.finalized + 1
+                return collected
+            end
+            return nil
+        end
+    end
+    if collected then
+        p.matched = p.matched + 1
+    end
+    return collected
+end
+
+local function traced_apply(list,parsed,nofparsed,order)
+    if trace_lparse then
+        lshow(parsed)
+    end
+    logs.report("lpath", "collecting : %s",parsed.pattern)
+    logs.report("lpath", " root tags : %s",tagstostring(list))
+    logs.report("lpath", "     order : %s",order or "unset")
+    local collected = list
+    for i=1,nofparsed do
+        local pi = parsed[i]
+        local kind = pi.kind
+        if kind == "axis" then
+            collected = apply_axis[pi.axis](collected)
+            logs.report("lpath", "% 10i : ax : %s",(collected and #collected) or 0,pi.axis)
+        elseif kind == "nodes" then
+            collected = apply_nodes(collected,pi.nodetest,pi.nodes)
+            logs.report("lpath", "% 10i : ns : %s",(collected and #collected) or 0,nodesettostring(pi.nodes,pi.nodetest))
+        elseif kind == "expression" then
+            collected = apply_expression(collected,pi.evaluator,order)
+            logs.report("lpath", "% 10i : ex : %s -> %s",(collected and #collected) or 0,pi.expression,pi.converted)
+        elseif kind == "finalizer" then
+            collected = pi.finalizer(collected)
+            logs.report("lpath", "% 10i : fi : %s : %s(%s)",(type(collected) == "table" and #collected) or 0,parsed.protocol or xml.defaultprotocol,pi.name,pi.arguments or "")
+            return collected
+        end
+        if not collected or #collected == 0 then
+            local pn = i < nofparsed and parsed[nofparsed]
+            if pn and pn.kind == "finalizer" then
+                collected = pn.finalizer(collected)
+                logs.report("lpath", "% 10i : fi : %s : %s(%s)",(type(collected) == "table" and #collected) or 0,parsed.protocol or xml.defaultprotocol,pn.name,pn.arguments or "")
+                return collected
+            end
+            return nil
+        end
+    end
+    return collected
+end
+
+local function normal_apply(list,parsed,nofparsed,order)
+    local collected = list
+    for i=1,nofparsed do
+        local pi = parsed[i]
+        local kind = pi.kind
+        if kind == "axis" then
+            local axis = pi.axis
+            if axis ~= "self" then
+                collected = apply_axis[axis](collected)
+            end
+        elseif kind == "nodes" then
+            collected = apply_nodes(collected,pi.nodetest,pi.nodes)
+        elseif kind == "expression" then
+            collected = apply_expression(collected,pi.evaluator,order)
+        elseif kind == "finalizer" then
+            return pi.finalizer(collected)
+        end
+        if not collected or #collected == 0 then
+            local pf = i < nofparsed and parsed[nofparsed].finalizer
+            if pf then
+                return pf(collected) -- can be anything
+            end
+            return nil
+        end
+    end
+    return collected
+end
+
+local function parse_apply(list,pattern)
+    -- we avoid an extra call
+    local parsed = cache[pattern]
+    if parsed then
+        lpathcalls = lpathcalls + 1
+        lpathcached = lpathcached + 1
+    elseif type(pattern) == "table" then
+        lpathcalls = lpathcalls + 1
+        parsed = pattern
+    else
+        parsed = parse_pattern(pattern) or pattern
+    end
+    if not parsed then
+        return
+    end
+    local nofparsed = #parsed
+    if nofparsed == 0 then
+        return -- something is wrong
+    end
+    local one = list[1]
+    if not one then
+        return -- something is wrong
+    elseif not trace_lpath then
+        return normal_apply(list,parsed,nofparsed,one.mi)
+    elseif trace_lprofile then
+        return profiled_apply(list,parsed,nofparsed,one.mi)
+    else
+        return traced_apply(list,parsed,nofparsed,one.mi)
+    end
+end
+
+-- internal (parsed)
+
+expressions.child = function(e,pattern)
+    return parse_apply({ e },pattern) -- todo: cache
+end
+expressions.count = function(e,pattern)
+    local collected = parse_apply({ e },pattern) -- todo: cache
+    return (collected and #collected) or 0
+end
+
+-- external
+
+expressions.oneof = function(s,...) -- slow
+    local t = {...} for i=1,#t do if s == t[i] then return true end end return false
+end
+expressions.error = function(str)
+    xml.error_handler("unknown function in lpath expression",tostring(str or "?"))
+    return false
+end
+expressions.undefined = function(s)
+    return s == nil
+end
+
+expressions.quit = function(s)
+    if s or s == nil then
+        quit_expression = true
+    end
+    return true
+end
+
+expressions.print = function(...)
+    print(...)
+    return true
+end
+
+expressions.contains  = find
+expressions.find      = find
+expressions.upper     = upper
+expressions.lower     = lower
+expressions.number    = tonumber
+expressions.boolean   = toboolean
+
+-- user interface
+
+local function traverse(root,pattern,handle)
+    logs.report("xml","use 'xml.selection' instead for '%s'",pattern)
+    local collected = parse_apply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            local r = e.__p__
+            handle(r,r.dt,e.ni)
+        end
+    end
+end
+
+local function selection(root,pattern,handle)
+    local collected = parse_apply({ root },pattern)
+    if collected then
+        if handle then
+            for c=1,#collected do
+                handle(collected[c])
+            end
+        else
+            return collected
+        end
+    end
+end
+
+xml.parse_parser  = parser
+xml.parse_pattern = parse_pattern
+xml.parse_apply   = parse_apply
+xml.traverse      = traverse           -- old method, r, d, k
+xml.selection     = selection          -- new method, simple handle
+
+local lpath = parse_pattern
+
+xml.lpath = lpath
+
+function xml.cached_patterns()
+    return cache
+end
+
+-- generic function finalizer (independant namespace)
+
+local function dofunction(collected,fnc)
+    if collected then
+        local f = functions[fnc]
+        if f then
+            for c=1,#collected do
+                f(collected[c])
+            end
+        else
+            logs.report("xml","unknown function '%s'",fnc)
+        end
+    end
+end
+
+xml.finalizers.xml["function"] = dofunction
+xml.finalizers.tex["function"] = dofunction
+
+-- functions
+
+expressions.text = function(e,n)
+    local rdt = e.__p__.dt
+    return (rdt and rdt[n]) or ""
+end
+
+expressions.name = function(e,n) -- ns + tg
+    local found = false
+    n = tonumber(n) or 0
+    if n == 0 then
+        found = type(e) == "table" and e
+    elseif n < 0 then
+        local d, k = e.__p__.dt, e.ni
+        for i=k-1,1,-1 do
+            local di = d[i]
+            if type(di) == "table" then
+                if n == -1 then
+                    found = di
+                    break
+                else
+                    n = n + 1
+                end
+            end
+        end
+    else
+        local d, k = e.__p__.dt, e.ni
+        for i=k+1,#d,1 do
+            local di = d[i]
+            if type(di) == "table" then
+                if n == 1 then
+                    found = di
+                    break
+                else
+                    n = n - 1
+                end
+            end
+        end
+    end
+    if found then
+        local ns, tg = found.rn or found.ns or "", found.tg
+        if ns ~= "" then
+            return ns .. ":" .. tg
+        else
+            return tg
+        end
+    else
+        return ""
+    end
+end
+
+expressions.tag = function(e,n) -- only tg
+    if not e then
+        return ""
+    else
+        local found = false
+        n = tonumber(n) or 0
+        if n == 0 then
+            found = (type(e) == "table") and e -- seems to fail
+        elseif n < 0 then
+            local d, k = e.__p__.dt, e.ni
+            for i=k-1,1,-1 do
+                local di = d[i]
+                if type(di) == "table" then
+                    if n == -1 then
+                        found = di
+                        break
+                    else
+                        n = n + 1
+                    end
+                end
+            end
+        else
+            local d, k = e.__p__.dt, e.ni
+            for i=k+1,#d,1 do
+                local di = d[i]
+                if type(di) == "table" then
+                    if n == 1 then
+                        found = di
+                        break
+                    else
+                        n = n - 1
+                    end
+                end
+            end
+        end
+        return (found and found.tg) or ""
+    end
+end
+
+--[[ldx--
+<p>This is the main filter function. It returns whatever is asked for.</p>
+--ldx]]--
+
+function xml.filter(root,pattern) -- no longer funny attribute handling here
+    return parse_apply({ root },pattern)
+end
+
+--[[ldx--
+<p>Often using an iterators looks nicer in the code than passing handler
+functions. The <l n='lua'/> book describes how to use coroutines for that
+purpose (<url href='http://www.lua.org/pil/9.3.html'/>). This permits
+code like:</p>
+
+<typing>
+for r, d, k in xml.elements(xml.load('text.xml'),"title") do
+    print(d[k]) -- old method
+end
+for e in xml.collected(xml.load('text.xml'),"title") do
+    print(e) -- new one
+end
+</typing>
+--ldx]]--
+
+local wrap, yield = coroutine.wrap, coroutine.yield
+
+function xml.elements(root,pattern,reverse) -- r, d, k
+    local collected = parse_apply({ root },pattern)
+    if collected then
+        if reverse then
+            return wrap(function() for c=#collected,1,-1 do
+                local e = collected[c] local r = e.__p__ yield(r,r.dt,e.ni)
+            end end)
+        else
+            return wrap(function() for c=1,#collected    do
+                local e = collected[c] local r = e.__p__ yield(r,r.dt,e.ni)
+            end end)
+        end
+    end
+    return wrap(function() end)
+end
+
+function xml.collected(root,pattern,reverse) -- e
+    local collected = parse_apply({ root },pattern)
+    if collected then
+        if reverse then
+            return wrap(function() for c=#collected,1,-1 do yield(collected[c]) end end)
+        else
+            return wrap(function() for c=1,#collected    do yield(collected[c]) end end)
+        end
+    end
+    return wrap(function() end)
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['lxml-mis'] = {
+    version   = 1.001,
+    comment   = "this module is the basis for the lxml-* ones",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local concat = table.concat
+local type, next, tonumber, tostring, setmetatable, loadstring = type, next, tonumber, tostring, setmetatable, loadstring
+local format, gsub, match = string.format, string.gsub, string.match
+local lpegmatch = lpeg.match
+
+--[[ldx--
+<p>The following helper functions best belong to the <t>lxml-ini</t>
+module. Some are here because we need then in the <t>mk</t>
+document and other manuals, others came up when playing with
+this module. Since this module is also used in <l n='mtxrun'/> we've
+put them here instead of loading mode modules there then needed.</p>
+--ldx]]--
+
+local function xmlgsub(t,old,new) -- will be replaced
+    local dt = t.dt
+    if dt then
+        for k=1,#dt do
+            local v = dt[k]
+            if type(v) == "string" then
+                dt[k] = gsub(v,old,new)
+            else
+                xmlgsub(v,old,new)
+            end
+        end
+    end
+end
+
+--~ xml.gsub = xmlgsub
+
+function xml.strip_leading_spaces(dk,d,k) -- cosmetic, for manual
+    if d and k then
+        local dkm = d[k-1]
+        if dkm and type(dkm) == "string" then
+            local s = match(dkm,"\n(%s+)")
+            xmlgsub(dk,"\n"..rep(" ",#s),"\n")
+        end
+    end
+end
+
+--~ xml.escapes   = { ['&'] = '&amp;', ['<'] = '&lt;', ['>'] = '&gt;', ['"'] = '&quot;' }
+--~ xml.unescapes = { } for k,v in next, xml.escapes do xml.unescapes[v] = k end
+
+--~ function xml.escaped  (str) return (gsub(str,"(.)"   , xml.escapes  )) end
+--~ function xml.unescaped(str) return (gsub(str,"(&.-;)", xml.unescapes)) end
+--~ function xml.cleansed (str) return (gsub(str,"<.->"  , ''           )) end -- "%b<>"
+
+local P, S, R, C, V, Cc, Cs = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.V, lpeg.Cc, lpeg.Cs
+
+-- 100 * 2500 * "oeps< oeps> oeps&" : gsub:lpeg|lpeg|lpeg
+--
+-- 1021:0335:0287:0247
+
+-- 10 * 1000 * "oeps< oeps> oeps& asfjhalskfjh alskfjh alskfjh alskfjh ;al J;LSFDJ"
+--
+-- 1559:0257:0288:0190 (last one suggested by roberto)
+
+--    escaped = Cs((S("<&>") / xml.escapes + 1)^0)
+--    escaped = Cs((S("<")/"&lt;" + S(">")/"&gt;" + S("&")/"&amp;" + 1)^0)
+local normal  = (1 - S("<&>"))^0
+local special = P("<")/"&lt;" + P(">")/"&gt;" + P("&")/"&amp;"
+local escaped = Cs(normal * (special * normal)^0)
+
+-- 100 * 1000 * "oeps&lt; oeps&gt; oeps&amp;" : gsub:lpeg == 0153:0280:0151:0080 (last one by roberto)
+
+local normal    = (1 - S"&")^0
+local special   = P("&lt;")/"<" + P("&gt;")/">" + P("&amp;")/"&"
+local unescaped = Cs(normal * (special * normal)^0)
+
+-- 100 * 5000 * "oeps <oeps bla='oeps' foo='bar'> oeps </oeps> oeps " : gsub:lpeg == 623:501 msec (short tags, less difference)
+
+local cleansed = Cs(((P("<") * (1-P(">"))^0 * P(">"))/"" + 1)^0)
+
+xml.escaped_pattern   = escaped
+xml.unescaped_pattern = unescaped
+xml.cleansed_pattern  = cleansed
+
+function xml.escaped  (str) return lpegmatch(escaped,str)   end
+function xml.unescaped(str) return lpegmatch(unescaped,str) end
+function xml.cleansed (str) return lpegmatch(cleansed,str)  end
+
+-- this might move
+
+function xml.fillin(root,pattern,str,check)
+    local e = xml.first(root,pattern)
+    if e then
+        local n = #e.dt
+        if not check or n == 0 or (n == 1 and e.dt[1] == "") then
+            e.dt = { str }
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['lxml-aux'] = {
+    version   = 1.001,
+    comment   = "this module is the basis for the lxml-* ones",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- not all functions here make sense anymore vbut we keep them for
+-- compatibility reasons
+
+local trace_manipulations = false  trackers.register("lxml.manipulations", function(v) trace_manipulations = v end)
+
+local xmlparseapply, xmlconvert, xmlcopy, xmlname = xml.parse_apply, xml.convert, xml.copy, xml.name
+local xmlinheritedconvert = xml.inheritedconvert
+
+local type = type
+local insert, remove = table.insert, table.remove
+local gmatch, gsub = string.gmatch, string.gsub
+
+local function report(what,pattern,c,e)
+    logs.report("xml","%s element '%s' (root: '%s', position: %s, index: %s, pattern: %s)",what,xmlname(e),xmlname(e.__p__),c,e.ni,pattern)
+end
+
+local function withelements(e,handle,depth)
+    if e and handle then
+        local edt = e.dt
+        if edt then
+            depth = depth or 0
+            for i=1,#edt do
+                local e = edt[i]
+                if type(e) == "table" then
+                    handle(e,depth)
+                    withelements(e,handle,depth+1)
+                end
+            end
+        end
+    end
+end
+
+xml.withelements = withelements
+
+function xml.withelement(e,n,handle) -- slow
+    if e and n ~= 0 and handle then
+        local edt = e.dt
+        if edt then
+            if n > 0 then
+                for i=1,#edt do
+                    local ei = edt[i]
+                    if type(ei) == "table" then
+                        if n == 1 then
+                            handle(ei)
+                            return
+                        else
+                            n = n - 1
+                        end
+                    end
+                end
+            elseif n < 0 then
+                for i=#edt,1,-1 do
+                    local ei = edt[i]
+                    if type(ei) == "table" then
+                        if n == -1 then
+                            handle(ei)
+                            return
+                        else
+                            n = n + 1
+                        end
+                    end
+                end
+            end
+        end
+    end
+end
+
+xml.elements_only = xml.collected
+
+function xml.each_element(root,pattern,handle,reverse)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        if reverse then
+            for c=#collected,1,-1 do
+                handle(collected[c])
+            end
+        else
+            for c=1,#collected do
+                handle(collected[c])
+            end
+        end
+        return collected
+    end
+end
+
+xml.process_elements = xml.each_element
+
+function xml.process_attributes(root,pattern,handle)
+    local collected = xmlparseapply({ root },pattern)
+    if collected and handle then
+        for c=1,#collected do
+            handle(collected[c].at)
+        end
+    end
+    return collected
+end
+
+--[[ldx--
+<p>The following functions collect elements and texts.</p>
+--ldx]]--
+
+-- are these still needed -> lxml-cmp.lua
+
+function xml.collect_elements(root, pattern)
+    return xmlparseapply({ root },pattern)
+end
+
+function xml.collect_texts(root, pattern, flatten) -- todo: variant with handle
+    local collected = xmlparseapply({ root },pattern)
+    if collected and flatten then
+        local xmltostring = xml.tostring
+        for c=1,#collected do
+            collected[c] = xmltostring(collected[c].dt)
+        end
+    end
+    return collected or { }
+end
+
+function xml.collect_tags(root, pattern, nonamespace)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        local t = { }
+        for c=1,#collected do
+            local e = collected[c]
+            local ns, tg = e.ns, e.tg
+            if nonamespace then
+                t[#t+1] = tg
+            elseif ns == "" then
+                t[#t+1] = tg
+            else
+                t[#t+1] = ns .. ":" .. tg
+            end
+        end
+        return t
+    end
+end
+
+--[[ldx--
+<p>We've now arrived at the functions that manipulate the tree.</p>
+--ldx]]--
+
+local no_root = { no_root = true }
+
+function xml.redo_ni(d)
+    for k=1,#d do
+        local dk = d[k]
+        if type(dk) == "table" then
+            dk.ni = k
+        end
+    end
+end
+
+local function xmltoelement(whatever,root)
+    if not whatever then
+        return nil
+    end
+    local element
+    if type(whatever) == "string" then
+        element = xmlinheritedconvert(whatever,root)
+    else
+        element = whatever -- we assume a table
+    end
+    if element.error then
+        return whatever -- string
+    end
+    if element then
+    --~ if element.ri then
+    --~     element = element.dt[element.ri].dt
+    --~ else
+    --~     element = element.dt
+    --~ end
+    end
+    return element
+end
+
+xml.toelement = xmltoelement
+
+local function copiedelement(element,newparent)
+    if type(element) == "string" then
+        return element
+    else
+        element = xmlcopy(element).dt
+        if newparent and type(element) == "table" then
+            element.__p__ = newparent
+        end
+        return element
+    end
+end
+
+function xml.delete_element(root,pattern)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            local p = e.__p__
+            if p then
+                if trace_manipulations then
+                    report('deleting',pattern,c,e)
+                end
+                local d = p.dt
+                remove(d,e.ni)
+                xml.redo_ni(d) -- can be made faster and inlined
+            end
+        end
+    end
+end
+
+function xml.replace_element(root,pattern,whatever)
+    local element = root and xmltoelement(whatever,root)
+    local collected = element and xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            local p = e.__p__
+            if p then
+                if trace_manipulations then
+                    report('replacing',pattern,c,e)
+                end
+                local d = p.dt
+                d[e.ni] = copiedelement(element,p)
+                xml.redo_ni(d) -- probably not needed
+            end
+        end
+    end
+end
+
+local function inject_element(root,pattern,whatever,prepend)
+    local element = root and xmltoelement(whatever,root)
+    local collected = element and xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            local r = e.__p__
+            local d, k, rri = r.dt, e.ni, r.ri
+            local edt = (rri and d[rri].dt) or (d and d[k] and d[k].dt)
+            if edt then
+                local be, af
+                local cp = copiedelement(element,e)
+                if prepend then
+                    be, af = cp, edt
+                else
+                    be, af = edt, cp
+                end
+                for i=1,#af do
+                    be[#be+1] = af[i]
+                end
+                if rri then
+                    r.dt[rri].dt = be
+                else
+                    d[k].dt = be
+                end
+                xml.redo_ni(d)
+            end
+        end
+    end
+end
+
+local function insert_element(root,pattern,whatever,before) -- todo: element als functie
+    local element = root and xmltoelement(whatever,root)
+    local collected = element and xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            local r = e.__p__
+            local d, k = r.dt, e.ni
+            if not before then
+                k = k + 1
+            end
+            insert(d,k,copiedelement(element,r))
+            xml.redo_ni(d)
+        end
+    end
+end
+
+xml.insert_element        =                 insert_element
+xml.insert_element_after  =                 insert_element
+xml.insert_element_before = function(r,p,e) insert_element(r,p,e,true) end
+xml.inject_element        =                 inject_element
+xml.inject_element_after  =                 inject_element
+xml.inject_element_before = function(r,p,e) inject_element(r,p,e,true) end
+
+local function include(xmldata,pattern,attribute,recursive,loaddata)
+    -- parse="text" (default: xml), encoding="" (todo)
+    -- attribute = attribute or 'href'
+    pattern = pattern or 'include'
+    loaddata = loaddata or io.loaddata
+    local collected = xmlparseapply({ xmldata },pattern)
+    if collected then
+        for c=1,#collected do
+            local ek = collected[c]
+            local name = nil
+            local ekdt = ek.dt
+            local ekat = ek.at
+            local epdt = ek.__p__.dt
+            if not attribute or attribute == "" then
+                name = (type(ekdt) == "table" and ekdt[1]) or ekdt -- ckeck, probably always tab or str
+            end
+            if not name then
+                for a in gmatch(attribute or "href","([^|]+)") do
+                    name = ekat[a]
+                    if name then break end
+                end
+            end
+            local data = (name and name ~= "" and loaddata(name)) or ""
+            if data == "" then
+                epdt[ek.ni] = "" -- xml.empty(d,k)
+            elseif ekat["parse"] == "text" then
+                -- for the moment hard coded
+                epdt[ek.ni] = xml.escaped(data) -- d[k] = xml.escaped(data)
+            else
+--~                 local settings = xmldata.settings
+--~                 settings.parent_root = xmldata -- to be tested
+--~                 local xi = xmlconvert(data,settings)
+                local xi = xmlinheritedconvert(data,xmldata)
+                if not xi then
+                    epdt[ek.ni] = "" -- xml.empty(d,k)
+                else
+                    if recursive then
+                        include(xi,pattern,attribute,recursive,loaddata)
+                    end
+                    epdt[ek.ni] = xml.body(xi) -- xml.assign(d,k,xi)
+                end
+            end
+        end
+    end
+end
+
+xml.include = include
+
+--~ local function manipulate(xmldata,pattern,manipulator) -- untested and might go away
+--~     local collected = xmlparseapply({ xmldata },pattern)
+--~     if collected then
+--~         local xmltostring = xml.tostring
+--~         for c=1,#collected do
+--~             local e = collected[c]
+--~             local data = manipulator(xmltostring(e))
+--~             if data == "" then
+--~                 epdt[e.ni] = ""
+--~             else
+--~                 local xi = xmlinheritedconvert(data,xmldata)
+--~                 if not xi then
+--~                     epdt[e.ni] = ""
+--~                 else
+--~                     epdt[e.ni] = xml.body(xi) -- xml.assign(d,k,xi)
+--~                 end
+--~             end
+--~         end
+--~     end
+--~ end
+
+--~ xml.manipulate = manipulate
+
+function xml.strip_whitespace(root, pattern, nolines) -- strips all leading and trailing space !
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for i=1,#collected do
+            local e = collected[i]
+            local edt = e.dt
+            if edt then
+                local t = { }
+                for i=1,#edt do
+                    local str = edt[i]
+                    if type(str) == "string" then
+                        if str == "" then
+                            -- stripped
+                        else
+                            if nolines then
+                                str = gsub(str,"[ \n\r\t]+"," ")
+                            end
+                            if str == "" then
+                                -- stripped
+                            else
+                                t[#t+1] = str
+                            end
+                        end
+                    else
+        --~                         str.ni = i
+                        t[#t+1] = str
+                    end
+                end
+                e.dt = t
+            end
+        end
+    end
+end
+
+function xml.strip_whitespace(root, pattern, nolines, anywhere) -- strips all leading and trailing spacing
+    local collected = xmlparseapply({ root },pattern) -- beware, indices no longer are valid now
+    if collected then
+        for i=1,#collected do
+            local e = collected[i]
+            local edt = e.dt
+            if edt then
+                if anywhere then
+                    local t = { }
+                    for e=1,#edt do
+                        local str = edt[e]
+                        if type(str) ~= "string" then
+                            t[#t+1] = str
+                        elseif str ~= "" then
+                            -- todo: lpeg for each case
+                            if nolines then
+                                str = gsub(str,"%s+"," ")
+                            end
+                            str = gsub(str,"^%s*(.-)%s*$","%1")
+                            if str ~= "" then
+                                t[#t+1] = str
+                            end
+                        end
+                    end
+                    e.dt = t
+                else
+                    -- we can assume a regular sparse xml table with no successive strings
+                    -- otherwise we should use a while loop
+                    if #edt > 0 then
+                        -- strip front
+                        local str = edt[1]
+                        if type(str) ~= "string" then
+                            -- nothing
+                        elseif str == "" then
+                            remove(edt,1)
+                        else
+                            if nolines then
+                                str = gsub(str,"%s+"," ")
+                            end
+                            str = gsub(str,"^%s+","")
+                            if str == "" then
+                                remove(edt,1)
+                            else
+                                edt[1] = str
+                            end
+                        end
+                    end
+                    if #edt > 1 then
+                        -- strip end
+                        local str = edt[#edt]
+                        if type(str) ~= "string" then
+                            -- nothing
+                        elseif str == "" then
+                            remove(edt)
+                        else
+                            if nolines then
+                                str = gsub(str,"%s+"," ")
+                            end
+                            str = gsub(str,"%s+$","")
+                            if str == "" then
+                                remove(edt)
+                            else
+                                edt[#edt] = str
+                            end
+                        end
+                    end
+                end
+            end
+        end
+    end
+end
+
+local function rename_space(root, oldspace, newspace) -- fast variant
+    local ndt = #root.dt
+    for i=1,ndt or 0 do
+        local e = root[i]
+        if type(e) == "table" then
+            if e.ns == oldspace then
+                e.ns = newspace
+                if e.rn then
+                    e.rn = newspace
+                end
+            end
+            local edt = e.dt
+            if edt then
+                rename_space(edt, oldspace, newspace)
+            end
+        end
+    end
+end
+
+xml.rename_space = rename_space
+
+function xml.remap_tag(root, pattern, newtg)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            collected[c].tg = newtg
+        end
+    end
+end
+
+function xml.remap_namespace(root, pattern, newns)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            collected[c].ns = newns
+        end
+    end
+end
+
+function xml.check_namespace(root, pattern, newns)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            if (not e.rn or e.rn == "") and e.ns == "" then
+                e.rn = newns
+            end
+        end
+    end
+end
+
+function xml.remap_name(root, pattern, newtg, newns, newrn)
+    local collected = xmlparseapply({ root },pattern)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            e.tg, e.ns, e.rn = newtg, newns, newrn
+        end
+    end
+end
+
+--[[ldx--
+<p>Here are a few synonyms.</p>
+--ldx]]--
+
+xml.each     = xml.each_element
+xml.process  = xml.process_element
+xml.strip    = xml.strip_whitespace
+xml.collect  = xml.collect_elements
+xml.all      = xml.collect_elements
+
+xml.insert   = xml.insert_element_after
+xml.inject   = xml.inject_element_after
+xml.after    = xml.insert_element_after
+xml.before   = xml.insert_element_before
+xml.delete   = xml.delete_element
+xml.replace  = xml.replace_element
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['lxml-xml'] = {
+    version   = 1.001,
+    comment   = "this module is the basis for the lxml-* ones",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local finalizers   = xml.finalizers.xml
+local xmlfilter    = xml.filter -- we could inline this one for speed
+local xmltostring  = xml.tostring
+local xmlserialize = xml.serialize
+
+local function first(collected) -- wrong ?
+    return collected and collected[1]
+end
+
+local function last(collected)
+    return collected and collected[#collected]
+end
+
+local function all(collected)
+    return collected
+end
+
+local function reverse(collected)
+    if collected then
+        local reversed = { }
+        for c=#collected,1,-1 do
+            reversed[#reversed+1] = collected[c]
+        end
+        return reversed
+    end
+end
+
+local function attribute(collected,name)
+    if collected and #collected > 0 then
+        local at = collected[1].at
+        return at and at[name]
+    end
+end
+
+local function att(id,name)
+    local at = id.at
+    return at and at[name]
+end
+
+local function count(collected)
+    return (collected and #collected) or 0
+end
+
+local function position(collected,n)
+    if collected then
+        n = tonumber(n) or 0
+        if n < 0 then
+            return collected[#collected + n + 1]
+        elseif n > 0 then
+            return collected[n]
+        else
+            return collected[1].mi or 0
+        end
+    end
+end
+
+local function match(collected)
+    return (collected and collected[1].mi) or 0 -- match
+end
+
+local function index(collected)
+    if collected then
+        return collected[1].ni
+    end
+end
+
+local function attributes(collected,arguments)
+    if collected then
+        local at = collected[1].at
+        if arguments then
+            return at[arguments]
+        elseif next(at) then
+            return at -- all of them
+        end
+    end
+end
+
+local function chainattribute(collected,arguments) -- todo: optional levels
+    if collected then
+        local e = collected[1]
+        while e do
+            local at = e.at
+            if at then
+                local a = at[arguments]
+                if a then
+                    return a
+                end
+            else
+                break -- error
+            end
+            e = e.__p__
+        end
+    end
+    return ""
+end
+
+local function raw(collected) -- hybrid
+    if collected then
+        local e = collected[1] or collected
+        return (e and xmlserialize(e)) or "" -- only first as we cannot concat function
+    else
+        return ""
+    end
+end
+
+local function text(collected) -- hybrid
+    if collected then
+        local e = collected[1] or collected
+        return (e and xmltostring(e.dt)) or ""
+    else
+        return ""
+    end
+end
+
+local function texts(collected)
+    if collected then
+        local t = { }
+        for c=1,#collected do
+            local e = collection[c]
+            if e and e.dt then
+                t[#t+1] = e.dt
+            end
+        end
+        return t
+    end
+end
+
+local function tag(collected,n)
+    if collected then
+        local c
+        if n == 0 or not n then
+            c = collected[1]
+        elseif n > 1 then
+            c = collected[n]
+        else
+            c = collected[#collected-n+1]
+        end
+        return c and c.tg
+    end
+end
+
+local function name(collected,n)
+    if collected then
+        local c
+        if n == 0 or not n then
+            c = collected[1]
+        elseif n > 1 then
+            c = collected[n]
+        else
+            c = collected[#collected-n+1]
+        end
+        if c then
+            if c.ns == "" then
+                return c.tg
+            else
+                return c.ns .. ":" .. c.tg
+            end
+        end
+    end
+end
+
+local function tags(collected,nonamespace)
+    if collected then
+        local t = { }
+        for c=1,#collected do
+            local e = collected[c]
+            local ns, tg = e.ns, e.tg
+            if nonamespace or ns == "" then
+                t[#t+1] = tg
+            else
+                t[#t+1] = ns .. ":" .. tg
+            end
+        end
+        return t
+    end
+end
+
+local function empty(collected)
+    if collected then
+        for c=1,#collected do
+            local e = collected[c]
+            if e then
+                local edt = e.dt
+                if edt then
+                    local n = #edt
+                    if n == 1 then
+                        local edk = edt[1]
+                        local typ = type(edk)
+                        if typ == "table" then
+                            return false
+                        elseif edk ~= "" then -- maybe an extra tester for spacing only
+                            return false
+                        end
+                    elseif n > 1 then
+                        return false
+                    end
+                end
+            end
+        end
+    end
+    return true
+end
+
+finalizers.first          = first
+finalizers.last           = last
+finalizers.all            = all
+finalizers.reverse        = reverse
+finalizers.elements       = all
+finalizers.default        = all
+finalizers.attribute      = attribute
+finalizers.att            = att
+finalizers.count          = count
+finalizers.position       = position
+finalizers.match          = match
+finalizers.index          = index
+finalizers.attributes     = attributes
+finalizers.chainattribute = chainattribute
+finalizers.text           = text
+finalizers.texts          = texts
+finalizers.tag            = tag
+finalizers.name           = name
+finalizers.tags           = tags
+finalizers.empty          = empty
+
+-- shortcuts -- we could support xmlfilter(id,pattern,first)
+
+function xml.first(id,pattern)
+    return first(xmlfilter(id,pattern))
+end
+
+function xml.last(id,pattern)
+    return last(xmlfilter(id,pattern))
+end
+
+function xml.count(id,pattern)
+    return count(xmlfilter(id,pattern))
+end
+
+function xml.attribute(id,pattern,a,default)
+    return attribute(xmlfilter(id,pattern),a,default)
+end
+
+function xml.raw(id,pattern)
+    if pattern then
+        return raw(xmlfilter(id,pattern))
+    else
+        return raw(id)
+    end
+end
+
+function xml.text(id,pattern)
+    if pattern then
+     -- return text(xmlfilter(id,pattern))
+        local collected = xmlfilter(id,pattern)
+        return (collected and xmltostring(collected[1].dt)) or ""
+    elseif id then
+     -- return text(id)
+        return xmltostring(id.dt) or ""
+    else
+        return ""
+    end
+end
+
+xml.content = text
+
+function xml.position(id,pattern,n) -- element
+    return position(xmlfilter(id,pattern),n)
+end
+
+function xml.match(id,pattern) -- number
+    return match(xmlfilter(id,pattern))
+end
+
+function xml.empty(id,pattern)
+    return empty(xmlfilter(id,pattern))
+end
+
+xml.all    = xml.filter
+xml.index  = xml.position
+xml.found  = xml.filter
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['luat-env'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- A former version provided functionality for non embeded core
+-- scripts i.e. runtime library loading. Given the amount of
+-- Lua code we use now, this no longer makes sense. Much of this
+-- evolved before bytecode arrays were available and so a lot of
+-- code has disappeared already.
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+local format, sub, match, gsub, find = string.format, string.sub, string.match, string.gsub, string.find
+local unquote, quote = string.unquote, string.quote
+
+-- precautions
+
+os.setlocale(nil,nil) -- useless feature and even dangerous in luatex
+
+function os.setlocale()
+    -- no way you can mess with it
+end
+
+-- dirty tricks
+
+if arg and (arg[0] == 'luatex' or arg[0] == 'luatex.exe') and arg[1] == "--luaonly" then
+    arg[-1]=arg[0] arg[0]=arg[2] for k=3,#arg do arg[k-2]=arg[k] end arg[#arg]=nil arg[#arg]=nil
+end
+
+if profiler and os.env["MTX_PROFILE_RUN"] == "YES" then
+    profiler.start("luatex-profile.log")
+end
+
+-- environment
+
+environment             = environment or { }
+environment.arguments   = { }
+environment.files       = { }
+environment.sortedflags = nil
+
+if not environment.jobname or environment.jobname == "" then if tex then environment.jobname = tex.jobname end end
+if not environment.version or environment.version == "" then             environment.version = "unknown"   end
+if not environment.jobname                              then             environment.jobname = "unknown"   end
+
+function environment.initialize_arguments(arg)
+    local arguments, files = { }, { }
+    environment.arguments, environment.files, environment.sortedflags = arguments, files, nil
+    for index=1,#arg do
+        local argument = arg[index]
+        if index > 0 then
+            local flag, value = match(argument,"^%-+(.-)=(.-)$")
+            if flag then
+                arguments[flag] = unquote(value or "")
+            else
+                flag = match(argument,"^%-+(.+)")
+                if flag then
+                    arguments[flag] = true
+                else
+                    files[#files+1] = argument
+                end
+            end
+        end
+    end
+    environment.ownname = environment.ownname or arg[0] or 'unknown.lua'
+end
+
+function environment.setargument(name,value)
+    environment.arguments[name] = value
+end
+
+-- todo: defaults, better checks e.g on type (boolean versus string)
+--
+-- tricky: too many hits when we support partials unless we add
+-- a registration of arguments so from now on we have 'partial'
+
+function environment.argument(name,partial)
+    local arguments, sortedflags = environment.arguments, environment.sortedflags
+    if arguments[name] then
+        return arguments[name]
+    elseif partial then
+        if not sortedflags then
+            sortedflags = table.sortedkeys(arguments)
+            for k=1,#sortedflags do
+                sortedflags[k] = "^" .. sortedflags[k]
+            end
+            environment.sortedflags = sortedflags
+        end
+        -- example of potential clash: ^mode ^modefile
+        for k=1,#sortedflags do
+            local v = sortedflags[k]
+            if find(name,v) then
+                return arguments[sub(v,2,#v)]
+            end
+        end
+    end
+    return nil
+end
+
+environment.argument("x",true)
+
+function environment.split_arguments(separator) -- rather special, cut-off before separator
+    local done, before, after = false, { }, { }
+    local original_arguments = environment.original_arguments
+    for k=1,#original_arguments do
+        local v = original_arguments[k]
+        if not done and v == separator then
+            done = true
+        elseif done then
+            after[#after+1] = v
+        else
+            before[#before+1] = v
+        end
+    end
+    return before, after
+end
+
+function environment.reconstruct_commandline(arg,noquote)
+    arg = arg or environment.original_arguments
+    if noquote and #arg == 1 then
+        local a = arg[1]
+        a = resolvers.resolve(a)
+        a = unquote(a)
+        return a
+    elseif #arg > 0 then
+        local result = { }
+        for i=1,#arg do
+            local a = arg[i]
+            a = resolvers.resolve(a)
+            a = unquote(a)
+            a = gsub(a,'"','\\"') -- tricky
+            if find(a," ") then
+                result[#result+1] = quote(a)
+            else
+                result[#result+1] = a
+            end
+        end
+        return table.join(result," ")
+    else
+        return ""
+    end
+end
+
+if arg then
+
+    -- new, reconstruct quoted snippets (maybe better just remove the " then and add them later)
+    local newarg, instring = { }, false
+
+    for index=1,#arg do
+        local argument = arg[index]
+        if find(argument,"^\"") then
+            newarg[#newarg+1] = gsub(argument,"^\"","")
+            if not find(argument,"\"$") then
+                instring = true
+            end
+        elseif find(argument,"\"$") then
+            newarg[#newarg] = newarg[#newarg] .. " " .. gsub(argument,"\"$","")
+            instring = false
+        elseif instring then
+            newarg[#newarg] = newarg[#newarg] .. " " .. argument
+        else
+            newarg[#newarg+1] = argument
+        end
+    end
+    for i=1,-5,-1 do
+        newarg[i] = arg[i]
+    end
+
+    environment.initialize_arguments(newarg)
+    environment.original_arguments = newarg
+    environment.raw_arguments = arg
+
+    arg = { } -- prevent duplicate handling
+
+end
+
+-- weird place ... depends on a not yet loaded module
+
+function environment.texfile(filename)
+    return resolvers.find_file(filename,'tex')
+end
+
+function environment.luafile(filename)
+    local resolved = resolvers.find_file(filename,'tex') or ""
+    if resolved ~= "" then
+        return resolved
+    end
+    resolved = resolvers.find_file(filename,'texmfscripts') or ""
+    if resolved ~= "" then
+        return resolved
+    end
+    return resolvers.find_file(filename,'luatexlibs') or ""
+end
+
+environment.loadedluacode = loadfile -- can be overloaded
+
+--~ function environment.loadedluacode(name)
+--~     if os.spawn("texluac -s -o texluac.luc " .. name) == 0 then
+--~         local chunk = loadstring(io.loaddata("texluac.luc"))
+--~         os.remove("texluac.luc")
+--~         return chunk
+--~     else
+--~         environment.loadedluacode = loadfile -- can be overloaded
+--~         return loadfile(name)
+--~     end
+--~ end
+
+function environment.luafilechunk(filename) -- used for loading lua bytecode in the format
+    filename = file.replacesuffix(filename, "lua")
+    local fullname = environment.luafile(filename)
+    if fullname and fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading file %s", fullname)
+        end
+        return environment.loadedluacode(fullname)
+    else
+        if trace_locating then
+            logs.report("fileio","unknown file %s", filename)
+        end
+        return nil
+    end
+end
+
+-- the next ones can use the previous ones / combine
+
+function environment.loadluafile(filename, version)
+    local lucname, luaname, chunk
+    local basename = file.removesuffix(filename)
+    if basename == filename then
+        lucname, luaname = basename .. ".luc",  basename .. ".lua"
+    else
+        lucname, luaname = nil, basename -- forced suffix
+    end
+    -- when not overloaded by explicit suffix we look for a luc file first
+    local fullname = (lucname and environment.luafile(lucname)) or ""
+    if fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading %s", fullname)
+        end
+        chunk = loadfile(fullname) -- this way we don't need a file exists check
+    end
+    if chunk then
+        assert(chunk)()
+        if version then
+            -- we check of the version number of this chunk matches
+            local v = version -- can be nil
+            if modules and modules[filename] then
+                v = modules[filename].version -- new method
+            elseif versions and versions[filename] then
+                v = versions[filename]        -- old method
+            end
+            if v == version then
+                return true
+            else
+                if trace_locating then
+                    logs.report("fileio","version mismatch for %s: lua=%s, luc=%s", filename, v, version)
+                end
+                environment.loadluafile(filename)
+            end
+        else
+            return true
+        end
+    end
+    fullname = (luaname and environment.luafile(luaname)) or ""
+    if fullname ~= "" then
+        if trace_locating then
+            logs.report("fileio","loading %s", fullname)
+        end
+        chunk = loadfile(fullname) -- this way we don't need a file exists check
+        if not chunk then
+            if trace_locating then
+                logs.report("fileio","unknown file %s", filename)
+            end
+        else
+            assert(chunk)()
+            return true
+        end
+    end
+    return false
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-inf'] = {
+    version   = 1.001,
+    comment   = "companion to trac-inf.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format = string.format
+
+local statusinfo, n, registered = { }, 0, { }
+
+statistics = statistics or { }
+
+statistics.enable    = true
+statistics.threshold = 0.05
+
+-- timing functions
+
+local clock = os.gettimeofday or os.clock
+
+local notimer
+
+function statistics.hastimer(instance)
+    return instance and instance.starttime
+end
+
+function statistics.resettiming(instance)
+    if not instance then
+        notimer = { timing = 0, loadtime = 0 }
+    else
+        instance.timing, instance.loadtime = 0, 0
+    end
+end
+
+function statistics.starttiming(instance)
+    if not instance then
+        notimer = { }
+        instance = notimer
+    end
+    local it = instance.timing
+    if not it then
+        it = 0
+    end
+    if it == 0 then
+        instance.starttime = clock()
+        if not instance.loadtime then
+            instance.loadtime = 0
+        end
+    else
+--~         logs.report("system","nested timing (%s)",tostring(instance))
+    end
+    instance.timing = it + 1
+end
+
+function statistics.stoptiming(instance, report)
+    if not instance then
+        instance = notimer
+    end
+    if instance then
+        local it = instance.timing
+        if it > 1 then
+            instance.timing = it - 1
+        else
+            local starttime = instance.starttime
+            if starttime then
+                local stoptime = clock()
+                local loadtime = stoptime - starttime
+                instance.stoptime = stoptime
+                instance.loadtime = instance.loadtime + loadtime
+                if report then
+                    statistics.report("load time %0.3f",loadtime)
+                end
+                instance.timing = 0
+                return loadtime
+            end
+        end
+    end
+    return 0
+end
+
+function statistics.elapsedtime(instance)
+    if not instance then
+        instance = notimer
+    end
+    return format("%0.3f",(instance and instance.loadtime) or 0)
+end
+
+function statistics.elapsedindeed(instance)
+    if not instance then
+        instance = notimer
+    end
+    local t = (instance and instance.loadtime) or 0
+    return t > statistics.threshold
+end
+
+function statistics.elapsedseconds(instance,rest) -- returns nil if 0 seconds
+    if statistics.elapsedindeed(instance) then
+        return format("%s seconds %s", statistics.elapsedtime(instance),rest or "")
+    end
+end
+
+-- general function
+
+function statistics.register(tag,fnc)
+    if statistics.enable and type(fnc) == "function" then
+        local rt = registered[tag] or (#statusinfo + 1)
+        statusinfo[rt] = { tag, fnc }
+        registered[tag] = rt
+        if #tag > n then n = #tag end
+    end
+end
+
+function statistics.show(reporter)
+    if statistics.enable then
+        if not reporter then reporter = function(tag,data,n) texio.write_nl(tag .. " " .. data) end end
+        -- this code will move
+        local register = statistics.register
+        register("luatex banner", function()
+            return string.lower(status.banner)
+        end)
+        register("control sequences", function()
+            return format("%s of %s", status.cs_count, status.hash_size+status.hash_extra)
+        end)
+        register("callbacks", function()
+            local total, indirect = status.callbacks or 0, status.indirect_callbacks or 0
+            return format("direct: %s, indirect: %s, total: %s", total-indirect, indirect, total)
+        end)
+        register("current memory usage", statistics.memused)
+        register("runtime",statistics.runtime)
+--         --
+        for i=1,#statusinfo do
+            local s = statusinfo[i]
+            local r = s[2]()
+            if r then
+                reporter(s[1],r,n)
+            end
+        end
+        texio.write_nl("") -- final newline
+        statistics.enable = false
+    end
+end
+
+function statistics.show_job_stat(tag,data,n)
+    texio.write_nl(format("%-15s: %s - %s","mkiv lua stats",tag:rpadd(n," "),data))
+end
+
+function statistics.memused() -- no math.round yet -)
+    local round = math.round or math.floor
+    return format("%s MB (ctx: %s MB)",round(collectgarbage("count")/1000), round(status.luastate_bytes/1000000))
+end
+
+if statistics.runtime then
+    -- already loaded and set
+elseif luatex and luatex.starttime then
+    statistics.starttime = luatex.starttime
+    statistics.loadtime = 0
+    statistics.timing = 0
+else
+    statistics.starttiming(statistics)
+end
+
+function statistics.runtime()
+    statistics.stoptiming(statistics)
+    return statistics.formatruntime(statistics.elapsedtime(statistics))
+end
+
+function statistics.formatruntime(runtime)
+    return format("%s seconds", statistics.elapsedtime(statistics))
+end
+
+function statistics.timed(action,report)
+    local timer = { }
+    report = report or logs.simple
+    statistics.starttiming(timer)
+    action()
+    statistics.stoptiming(timer)
+    report("total runtime: %s",statistics.elapsedtime(timer))
+end
+
+-- where, not really the best spot for this:
+
+commands = commands or { }
+
+local timer
+
+function commands.resettimer()
+    statistics.resettiming(timer)
+    statistics.starttiming(timer)
+end
+
+function commands.elapsedtime()
+    statistics.stoptiming(timer)
+    tex.sprint(statistics.elapsedtime(timer))
+end
+
+commands.resettimer()
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['trac-log'] = {
+    version   = 1.001,
+    comment   = "companion to trac-log.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- this is old code that needs an overhaul
+
+--~ io.stdout:setvbuf("no")
+--~ io.stderr:setvbuf("no")
+
+local write_nl, write = texio.write_nl or print, texio.write or io.write
+local format, gmatch = string.format, string.gmatch
+local texcount = tex and tex.count
+
+if texlua then
+    write_nl = print
+    write    = io.write
+end
+
+--[[ldx--
+<p>This is a prelude to a more extensive logging module. For the sake
+of parsing log files, in addition to the standard logging we will
+provide an <l n='xml'/> structured file. Actually, any logging that
+is hooked into callbacks will be \XML\ by default.</p>
+--ldx]]--
+
+logs     = logs     or { }
+logs.xml = logs.xml or { }
+logs.tex = logs.tex or { }
+
+--[[ldx--
+<p>This looks pretty ugly but we need to speed things up a bit.</p>
+--ldx]]--
+
+logs.moreinfo = [[
+more information about ConTeXt and the tools that come with it can be found at:
+
+maillist : ntg-context@ntg.nl / http://www.ntg.nl/mailman/listinfo/ntg-context
+webpage  : http://www.pragma-ade.nl / http://tex.aanhet.net
+wiki     : http://contextgarden.net
+]]
+
+logs.levels = {
+    ['error']   = 1,
+    ['warning'] = 2,
+    ['info']    = 3,
+    ['debug']   = 4,
+}
+
+logs.functions = {
+    'report', 'start', 'stop', 'push', 'pop', 'line', 'direct',
+    'start_run', 'stop_run',
+    'start_page_number', 'stop_page_number',
+    'report_output_pages', 'report_output_log',
+    'report_tex_stat', 'report_job_stat',
+    'show_open', 'show_close', 'show_load',
+}
+
+logs.tracers = {
+}
+
+logs.level = 0
+logs.mode  = string.lower((os.getenv("MTX.LOG.MODE") or os.getenv("MTX_LOG_MODE") or "tex"))
+
+function logs.set_level(level)
+    logs.level = logs.levels[level] or level
+end
+
+function logs.set_method(method)
+    for _, v in next, logs.functions do
+        logs[v] = logs[method][v] or function() end
+    end
+end
+
+-- tex logging
+
+function logs.tex.report(category,fmt,...) -- new
+    if fmt then
+        write_nl(category .. " | " .. format(fmt,...))
+    else
+        write_nl(category .. " |")
+    end
+end
+
+function logs.tex.line(fmt,...) -- new
+    if fmt then
+        write_nl(format(fmt,...))
+    else
+        write_nl("")
+    end
+end
+
+--~ function logs.tex.start_page_number()
+--~     local real, user, sub = texcount.realpageno, texcount.userpageno, texcount.subpageno
+--~     if real > 0 then
+--~         if user > 0 then
+--~             if sub > 0 then
+--~                 write(format("[%s.%s.%s",real,user,sub))
+--~             else
+--~                 write(format("[%s.%s",real,user))
+--~             end
+--~         else
+--~             write(format("[%s",real))
+--~         end
+--~     else
+--~         write("[-")
+--~     end
+--~ end
+
+--~ function logs.tex.stop_page_number()
+--~     write("]")
+--~ end
+
+local real, user, sub
+
+function logs.tex.start_page_number()
+    real, user, sub = texcount.realpageno, texcount.userpageno, texcount.subpageno
+end
+
+function logs.tex.stop_page_number()
+    if real > 0 then
+        if user > 0 then
+            if sub > 0 then
+                logs.report("pages", "flushing realpage %s, userpage %s, subpage %s",real,user,sub)
+            else
+                logs.report("pages", "flushing realpage %s, userpage %s",real,user)
+            end
+        else
+            logs.report("pages", "flushing realpage %s",real)
+        end
+    else
+        logs.report("pages", "flushing page")
+    end
+    io.flush()
+end
+
+logs.tex.report_job_stat = statistics.show_job_stat
+
+-- xml logging
+
+function logs.xml.report(category,fmt,...) -- new
+    if fmt then
+        write_nl(format("<r category='%s'>%s</r>",category,format(fmt,...)))
+    else
+        write_nl(format("<r category='%s'/>",category))
+    end
+end
+function logs.xml.line(fmt,...) -- new
+    if fmt then
+        write_nl(format("<r>%s</r>",format(fmt,...)))
+    else
+        write_nl("<r/>")
+    end
+end
+
+function logs.xml.start() if logs.level > 0 then tw("<%s>" ) end end
+function logs.xml.stop () if logs.level > 0 then tw("</%s>") end end
+function logs.xml.push () if logs.level > 0 then tw("<!-- ") end end
+function logs.xml.pop  () if logs.level > 0 then tw(" -->" ) end end
+
+function logs.xml.start_run()
+    write_nl("<?xml version='1.0' standalone='yes'?>")
+    write_nl("<job>") --  xmlns='www.pragma-ade.com/luatex/schemas/context-job.rng'
+    write_nl("")
+end
+
+function logs.xml.stop_run()
+    write_nl("</job>")
+end
+
+function logs.xml.start_page_number()
+    write_nl(format("<p real='%s' page='%s' sub='%s'", texcount.realpageno, texcount.userpageno, texcount.subpageno))
+end
+
+function logs.xml.stop_page_number()
+    write("/>")
+    write_nl("")
+end
+
+function logs.xml.report_output_pages(p,b)
+    write_nl(format("<v k='pages' v='%s'/>", p))
+    write_nl(format("<v k='bytes' v='%s'/>", b))
+    write_nl("")
+end
+
+function logs.xml.report_output_log()
+end
+
+function logs.xml.report_tex_stat(k,v)
+    texiowrite_nl("log","<v k='"..k.."'>"..tostring(v).."</v>")
+end
+
+local level = 0
+
+function logs.xml.show_open(name)
+    level = level + 1
+    texiowrite_nl(format("<f l='%s' n='%s'>",level,name))
+end
+
+function logs.xml.show_close(name)
+    texiowrite("</f> ")
+    level = level - 1
+end
+
+function logs.xml.show_load(name)
+    texiowrite_nl(format("<f l='%s' n='%s'/>",level+1,name))
+end
+
+--
+
+local name, banner = 'report', 'context'
+
+local function report(category,fmt,...)
+    if fmt then
+        write_nl(format("%s | %s: %s",name,category,format(fmt,...)))
+    elseif category then
+        write_nl(format("%s | %s",name,category))
+    else
+        write_nl(format("%s |",name))
+    end
+end
+
+local function simple(fmt,...)
+    if fmt then
+        write_nl(format("%s | %s",name,format(fmt,...)))
+    else
+        write_nl(format("%s |",name))
+    end
+end
+
+function logs.setprogram(_name_,_banner_,_verbose_)
+    name, banner = _name_, _banner_
+    if _verbose_ then
+        trackers.enable("resolvers.locating")
+    end
+    logs.set_method("tex")
+    logs.report = report -- also used in libraries
+    logs.simple = simple -- only used in scripts !
+    if utils then
+        utils.report = simple
+    end
+    logs.verbose = _verbose_
+end
+
+function logs.setverbose(what)
+    if what then
+        trackers.enable("resolvers.locating")
+    else
+        trackers.disable("resolvers.locating")
+    end
+    logs.verbose = what or false
+end
+
+function logs.extendbanner(_banner_,_verbose_)
+    banner = banner .. " | ".. _banner_
+    if _verbose_ ~= nil then
+        logs.setverbose(what)
+    end
+end
+
+logs.verbose = false
+logs.report  = logs.tex.report
+logs.simple  = logs.tex.report
+
+function logs.reportlines(str) -- todo: <lines></lines>
+    for line in gmatch(str,"(.-)[\n\r]") do
+        logs.report(line)
+    end
+end
+
+function logs.reportline() -- for scripts too
+    logs.report()
+end
+
+logs.simpleline = logs.reportline
+
+function logs.reportbanner() -- for scripts too
+    logs.report(banner)
+end
+
+function logs.help(message,option)
+    logs.reportbanner()
+    logs.reportline()
+    logs.reportlines(message)
+    local moreinfo = logs.moreinfo or ""
+    if moreinfo ~= "" and option ~= "nomoreinfo" then
+        logs.reportline()
+        logs.reportlines(moreinfo)
+    end
+end
+
+logs.set_level('error')
+logs.set_method('tex')
+
+function logs.system(whereto,process,jobname,category,...)
+    for i=1,10 do
+        local f = io.open(whereto,"a")
+        if f then
+            f:write(format("%s %s => %s => %s => %s\r",os.date("%d/%m/%y %H:%m:%S"),process,jobname,category,format(...)))
+            f:close()
+            break
+        else
+            sleep(0.1)
+        end
+    end
+end
+
+--~ local syslogname = "oeps.xxx"
+--~
+--~ for i=1,10 do
+--~     logs.system(syslogname,"context","test","fonts","font %s recached due to newer version (%s)","blabla","123")
+--~ end
+
+function logs.fatal(where,...)
+    logs.report(where,"fatal error: %s, aborting now",format(...))
+    os.exit()
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-inp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files",
+}
+
+-- After a few years using the code the large luat-inp.lua file
+-- has been split up a bit. In the process some functionality was
+-- dropped:
+--
+-- * support for reading lsr files
+-- * selective scanning (subtrees)
+-- * some public auxiliary functions were made private
+--
+-- TODO: os.getenv -> os.env[]
+-- TODO: instances.[hashes,cnffiles,configurations,522]
+-- TODO: check escaping in find etc, too much, too slow
+
+-- This lib is multi-purpose and can be loaded again later on so that
+-- additional functionality becomes available. We will split thislogs.report("fileio",
+-- module in components once we're done with prototyping. This is the
+-- first code I wrote for LuaTeX, so it needs some cleanup. Before changing
+-- something in this module one can best check with Taco or Hans first; there
+-- is some nasty trickery going on that relates to traditional kpse support.
+
+-- To be considered: hash key lowercase, first entry in table filename
+-- (any case), rest paths (so no need for optimization). Or maybe a
+-- separate table that matches lowercase names to mixed case when
+-- present. In that case the lower() cases can go away. I will do that
+-- only when we run into problems with names ... well ... Iwona-Regular.
+
+-- Beware, loading and saving is overloaded in luat-tmp!
+
+local format, gsub, find, lower, upper, match, gmatch = string.format, string.gsub, string.find, string.lower, string.upper, string.match, string.gmatch
+local concat, insert, sortedkeys = table.concat, table.insert, table.sortedkeys
+local next, type = next, type
+local lpegmatch = lpeg.match
+
+local trace_locating, trace_detail, trace_expansions = false, false, false
+
+trackers.register("resolvers.locating",   function(v) trace_locating   = v end)
+trackers.register("resolvers.details",    function(v) trace_detail     = v end)
+trackers.register("resolvers.expansions", function(v) trace_expansions = v end) -- todo
+
+if not resolvers then
+    resolvers = {
+        suffixes     = { },
+        formats      = { },
+        dangerous    = { },
+        suffixmap    = { },
+        alternatives = { },
+        locators     = { },  -- locate databases
+        hashers      = { },  -- load databases
+        generators   = { },  -- generate databases
+    }
+end
+
+local resolvers = resolvers
+
+resolvers.locators  .notfound = { nil }
+resolvers.hashers   .notfound = { nil }
+resolvers.generators.notfound = { nil }
+
+resolvers.cacheversion = '1.0.1'
+resolvers.cnfname      = 'texmf.cnf'
+resolvers.luaname      = 'texmfcnf.lua'
+resolvers.homedir      = os.env[os.type == "windows" and 'USERPROFILE'] or os.env['HOME'] or '~'
+resolvers.cnfdefault   = '{$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,.local,}/web2c}'
+
+local dummy_path_expr = "^!*unset/*$"
+
+local formats      = resolvers.formats
+local suffixes     = resolvers.suffixes
+local dangerous    = resolvers.dangerous
+local suffixmap    = resolvers.suffixmap
+local alternatives = resolvers.alternatives
+
+formats['afm'] = 'AFMFONTS'       suffixes['afm'] = { 'afm' }
+formats['enc'] = 'ENCFONTS'       suffixes['enc'] = { 'enc' }
+formats['fmt'] = 'TEXFORMATS'     suffixes['fmt'] = { 'fmt' }
+formats['map'] = 'TEXFONTMAPS'    suffixes['map'] = { 'map' }
+formats['mp']  = 'MPINPUTS'       suffixes['mp']  = { 'mp' }
+formats['ocp'] = 'OCPINPUTS'      suffixes['ocp'] = { 'ocp' }
+formats['ofm'] = 'OFMFONTS'       suffixes['ofm'] = { 'ofm', 'tfm' }
+formats['otf'] = 'OPENTYPEFONTS'  suffixes['otf'] = { 'otf' } -- 'ttf'
+formats['opl'] = 'OPLFONTS'       suffixes['opl'] = { 'opl' }
+formats['otp'] = 'OTPINPUTS'      suffixes['otp'] = { 'otp' }
+formats['ovf'] = 'OVFFONTS'       suffixes['ovf'] = { 'ovf', 'vf' }
+formats['ovp'] = 'OVPFONTS'       suffixes['ovp'] = { 'ovp' }
+formats['tex'] = 'TEXINPUTS'      suffixes['tex'] = { 'tex' }
+formats['tfm'] = 'TFMFONTS'       suffixes['tfm'] = { 'tfm' }
+formats['ttf'] = 'TTFONTS'        suffixes['ttf'] = { 'ttf', 'ttc', 'dfont' }
+formats['pfb'] = 'T1FONTS'        suffixes['pfb'] = { 'pfb', 'pfa' }
+formats['vf']  = 'VFFONTS'        suffixes['vf']  = { 'vf' }
+
+formats['fea'] = 'FONTFEATURES'   suffixes['fea'] = { 'fea' }
+formats['cid'] = 'FONTCIDMAPS'    suffixes['cid'] = { 'cid', 'cidmap' }
+
+formats ['texmfscripts'] = 'TEXMFSCRIPTS' -- new
+suffixes['texmfscripts'] = { 'rb', 'pl', 'py' } -- 'lua'
+
+formats ['lua'] = 'LUAINPUTS' -- new
+suffixes['lua'] = { 'lua', 'luc', 'tma', 'tmc' }
+
+-- backward compatible ones
+
+alternatives['map files']            = 'map'
+alternatives['enc files']            = 'enc'
+alternatives['cid maps']             = 'cid' -- great, why no cid files
+alternatives['font feature files']   = 'fea' -- and fea files here
+alternatives['opentype fonts']       = 'otf'
+alternatives['truetype fonts']       = 'ttf'
+alternatives['truetype collections'] = 'ttc'
+alternatives['truetype dictionary']  = 'dfont'
+alternatives['type1 fonts']          = 'pfb'
+
+-- obscure ones
+
+formats ['misc fonts'] = ''
+suffixes['misc fonts'] = { }
+
+formats     ['sfd']                      = 'SFDFONTS'
+suffixes    ['sfd']                      = { 'sfd' }
+alternatives['subfont definition files'] = 'sfd'
+
+-- lib paths
+
+formats ['lib'] = 'CLUAINPUTS' -- new (needs checking)
+suffixes['lib'] = (os.libsuffix and { os.libsuffix }) or { 'dll', 'so' }
+
+-- In practice we will work within one tds tree, but i want to keep
+-- the option open to build tools that look at multiple trees, which is
+-- why we keep the tree specific data in a table. We used to pass the
+-- instance but for practical pusposes we now avoid this and use a
+-- instance variable.
+
+-- here we catch a few new thingies (todo: add these paths to context.tmf)
+--
+-- FONTFEATURES  = .;$TEXMF/fonts/fea//
+-- FONTCIDMAPS   = .;$TEXMF/fonts/cid//
+
+-- we always have one instance active
+
+resolvers.instance = resolvers.instance or nil -- the current one (slow access)
+local instance = resolvers.instance or nil -- the current one (fast access)
+
+function resolvers.newinstance()
+
+    -- store once, freeze and faster (once reset we can best use
+    -- instance.environment) maybe better have a register suffix
+    -- function
+
+    for k, v in next, suffixes do
+        for i=1,#v do
+            local vi = v[i]
+            if vi then
+                suffixmap[vi] = k
+            end
+        end
+    end
+
+    -- because vf searching is somewhat dangerous, we want to prevent
+    -- too liberal searching esp because we do a lookup on the current
+    -- path anyway; only tex (or any) is safe
+
+    for k, v in next, formats do
+        dangerous[k] = true
+    end
+    dangerous.tex = nil
+
+    -- the instance
+
+    local newinstance = {
+        rootpath        = '',
+        treepath        = '',
+        progname        = 'context',
+        engine          = 'luatex',
+        format          = '',
+        environment     = { },
+        variables       = { },
+        expansions      = { },
+        files           = { },
+        remap           = { },
+        configuration   = { },
+        setup           = { },
+        order           = { },
+        found           = { },
+        foundintrees    = { },
+        kpsevars        = { },
+        hashes          = { },
+        cnffiles        = { },
+        luafiles        = { },
+        lists           = { },
+        remember        = true,
+        diskcache       = true,
+        renewcache      = false,
+        scandisk        = true,
+        cachepath       = nil,
+        loaderror       = false,
+        sortdata        = false,
+        savelists       = true,
+        cleanuppaths    = true,
+        allresults      = false,
+        pattern         = nil, -- lists
+        data            = { }, -- only for loading
+        force_suffixes  = true,
+        fakepaths       = { },
+    }
+
+    local ne = newinstance.environment
+
+    for k,v in next, os.env do
+        ne[k] = resolvers.bare_variable(v)
+    end
+
+    return newinstance
+
+end
+
+function resolvers.setinstance(someinstance)
+    instance = someinstance
+    resolvers.instance = someinstance
+    return someinstance
+end
+
+function resolvers.reset()
+    return resolvers.setinstance(resolvers.newinstance())
+end
+
+local function reset_hashes()
+    instance.lists = { }
+    instance.found = { }
+end
+
+local function check_configuration() -- not yet ok, no time for debugging now
+    local ie, iv = instance.environment, instance.variables
+    local function fix(varname,default)
+        local proname = varname .. "." .. instance.progname or "crap"
+        local p, v = ie[proname], ie[varname] or iv[varname]
+        if not ((p and p ~= "") or (v and v ~= "")) then
+            iv[varname] = default -- or environment?
+        end
+    end
+    local name = os.name
+    if name == "windows" then
+        fix("OSFONTDIR", "c:/windows/fonts//")
+    elseif name == "macosx" then
+        fix("OSFONTDIR", "$HOME/Library/Fonts//;/Library/Fonts//;/System/Library/Fonts//")
+    else
+        -- bad luck
+    end
+    fix("LUAINPUTS"   , ".;$TEXINPUTS;$TEXMFSCRIPTS") -- no progname, hm
+    -- this will go away some day
+    fix("FONTFEATURES", ".;$TEXMF/fonts/{data,fea}//;$OPENTYPEFONTS;$TTFONTS;$T1FONTS;$AFMFONTS")
+    fix("FONTCIDMAPS" , ".;$TEXMF/fonts/{data,cid}//;$OPENTYPEFONTS;$TTFONTS;$T1FONTS;$AFMFONTS")
+    --
+    fix("LUATEXLIBS"  , ".;$TEXMF/luatex/lua//")
+end
+
+function resolvers.bare_variable(str) -- assumes str is a string
+    return (gsub(str,"\s*([\"\']?)(.+)%1\s*", "%2"))
+end
+
+function resolvers.settrace(n) -- no longer number but: 'locating' or 'detail'
+    if n then
+        trackers.disable("resolvers.*")
+        trackers.enable("resolvers."..n)
+    end
+end
+
+resolvers.settrace(os.getenv("MTX_INPUT_TRACE"))
+
+function resolvers.osenv(key)
+    local ie = instance.environment
+    local value = ie[key]
+    if value == nil then
+     -- local e = os.getenv(key)
+        local e = os.env[key]
+        if e == nil then
+         -- value = "" -- false
+        else
+            value = resolvers.bare_variable(e)
+        end
+        ie[key] = value
+    end
+    return value or ""
+end
+
+function resolvers.env(key)
+    return instance.environment[key] or resolvers.osenv(key)
+end
+
+--
+
+local function expand_vars(lst) -- simple vars
+    local variables, env = instance.variables, resolvers.env
+    local function resolve(a)
+        return variables[a] or env(a)
+    end
+    for k=1,#lst do
+        lst[k] = gsub(lst[k],"%$([%a%d%_%-]+)",resolve)
+    end
+end
+
+local function expanded_var(var) -- simple vars
+    local function resolve(a)
+        return instance.variables[a] or resolvers.env(a)
+    end
+    return (gsub(var,"%$([%a%d%_%-]+)",resolve))
+end
+
+local function entry(entries,name)
+    if name and (name ~= "") then
+        name = gsub(name,'%$','')
+        local result = entries[name..'.'..instance.progname] or entries[name]
+        if result then
+            return result
+        else
+            result = resolvers.env(name)
+            if result then
+                instance.variables[name] = result
+                resolvers.expand_variables()
+                return instance.expansions[name] or ""
+            end
+        end
+    end
+    return ""
+end
+
+local function is_entry(entries,name)
+    if name and name ~= "" then
+        name = gsub(name,'%$','')
+        return (entries[name..'.'..instance.progname] or entries[name]) ~= nil
+    else
+        return false
+    end
+end
+
+-- {a,b,c,d}
+-- a,b,c/{p,q,r},d
+-- a,b,c/{p,q,r}/d/{x,y,z}//
+-- a,b,c/{p,q/{x,y,z},r},d/{p,q,r}
+-- a,b,c/{p,q/{x,y,z},r},d/{p,q,r}
+-- a{b,c}{d,e}f
+-- {a,b,c,d}
+-- {a,b,c/{p,q,r},d}
+-- {a,b,c/{p,q,r}/d/{x,y,z}//}
+-- {a,b,c/{p,q/{x,y,z}},d/{p,q,r}}
+-- {a,b,c/{p,q/{x,y,z},w}v,d/{p,q,r}}
+-- {$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,.local,}/web2c}
+
+-- this one is better and faster, but it took me a while to realize
+-- that this kind of replacement is cleaner than messy parsing and
+-- fuzzy concatenating we can probably gain a bit with selectively
+-- applying lpeg, but experiments with lpeg parsing this proved not to
+-- work that well; the parsing is ok, but dealing with the resulting
+-- table is a pain because we need to work inside-out recursively
+
+local function do_first(a,b)
+    local t = { }
+    for s in gmatch(b,"[^,]+") do t[#t+1] = a .. s end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_second(a,b)
+    local t = { }
+    for s in gmatch(a,"[^,]+") do t[#t+1] = s .. b end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_both(a,b)
+    local t = { }
+    for sa in gmatch(a,"[^,]+") do
+        for sb in gmatch(b,"[^,]+") do
+            t[#t+1] = sa .. sb
+        end
+    end
+    return "{" .. concat(t,",") .. "}"
+end
+
+local function do_three(a,b,c)
+    return a .. b.. c
+end
+
+local function splitpathexpr(str, t, validate)
+    -- no need for further optimization as it is only called a
+    -- few times, we can use lpeg for the sub
+    if trace_expansions then
+        logs.report("fileio","expanding variable '%s'",str)
+    end
+    t = t or { }
+    str = gsub(str,",}",",@}")
+    str = gsub(str,"{,","{@,")
+ -- str = "@" .. str .. "@"
+    local ok, done
+    while true do
+        done = false
+        while true do
+            str, ok = gsub(str,"([^{},]+){([^{}]+)}",do_first)
+            if ok > 0 then done = true else break end
+        end
+        while true do
+            str, ok = gsub(str,"{([^{}]+)}([^{},]+)",do_second)
+            if ok > 0 then done = true else break end
+        end
+        while true do
+            str, ok = gsub(str,"{([^{}]+)}{([^{}]+)}",do_both)
+            if ok > 0 then done = true else break end
+        end
+        str, ok = gsub(str,"({[^{}]*){([^{}]+)}([^{}]*})",do_three)
+        if ok > 0 then done = true end
+        if not done then break end
+    end
+    str = gsub(str,"[{}]", "")
+    str = gsub(str,"@","")
+    if validate then
+        for s in gmatch(str,"[^,]+") do
+            s = validate(s)
+            if s then t[#t+1] = s end
+        end
+    else
+        for s in gmatch(str,"[^,]+") do
+            t[#t+1] = s
+        end
+    end
+    if trace_expansions then
+        for k=1,#t do
+            logs.report("fileio","% 4i: %s",k,t[k])
+        end
+    end
+    return t
+end
+
+local function expanded_path_from_list(pathlist) -- maybe not a list, just a path
+    -- a previous version fed back into pathlist
+    local newlist, ok = { }, false
+    for k=1,#pathlist do
+        if find(pathlist[k],"[{}]") then
+            ok = true
+            break
+        end
+    end
+    if ok then
+        local function validate(s)
+            s = file.collapse_path(s)
+            return s ~= "" and not find(s,dummy_path_expr) and s
+        end
+        for k=1,#pathlist do
+            splitpathexpr(pathlist[k],newlist,validate)
+        end
+    else
+        for k=1,#pathlist do
+            for p in gmatch(pathlist[k],"([^,]+)") do
+                p = file.collapse_path(p)
+                if p ~= "" then newlist[#newlist+1] = p end
+            end
+        end
+    end
+    return newlist
+end
+
+-- we follow a rather traditional approach:
+--
+-- (1) texmf.cnf given in TEXMFCNF
+-- (2) texmf.cnf searched in default variable
+--
+-- also we now follow the stupid route: if not set then just assume *one*
+-- cnf file under texmf (i.e. distribution)
+
+local args = environment and environment.original_arguments or arg -- this needs a cleanup
+
+resolvers.ownbin = resolvers.ownbin or args[-2] or arg[-2] or args[-1] or arg[-1] or arg[0] or "luatex"
+resolvers.ownbin = gsub(resolvers.ownbin,"\\","/")
+
+function resolvers.getownpath()
+    local ownpath = resolvers.ownpath or os.selfdir
+    if not ownpath or ownpath == "" or ownpath == "unset" then
+        ownpath = args[-1] or arg[-1]
+        ownpath = ownpath and file.dirname(gsub(ownpath,"\\","/"))
+        if not ownpath or ownpath == "" then
+            ownpath = args[-0] or arg[-0]
+            ownpath = ownpath and file.dirname(gsub(ownpath,"\\","/"))
+        end
+        local binary = resolvers.ownbin
+        if not ownpath or ownpath == "" then
+            ownpath = ownpath and file.dirname(binary)
+        end
+        if not ownpath or ownpath == "" then
+            if os.binsuffix ~= "" then
+                binary = file.replacesuffix(binary,os.binsuffix)
+            end
+            for p in gmatch(os.getenv("PATH"),"[^"..io.pathseparator.."]+") do
+                local b = file.join(p,binary)
+                if lfs.isfile(b) then
+                    -- we assume that after changing to the path the currentdir function
+                    -- resolves to the real location and use this side effect here; this
+                    -- trick is needed because on the mac installations use symlinks in the
+                    -- path instead of real locations
+                    local olddir = lfs.currentdir()
+                    if lfs.chdir(p) then
+                        local pp = lfs.currentdir()
+                        if trace_locating and p ~= pp then
+                            logs.report("fileio","following symlink '%s' to '%s'",p,pp)
+                        end
+                        ownpath = pp
+                        lfs.chdir(olddir)
+                    else
+                        if trace_locating then
+                            logs.report("fileio","unable to check path '%s'",p)
+                        end
+                        ownpath =  p
+                    end
+                    break
+                end
+            end
+        end
+        if not ownpath or ownpath == "" then
+            ownpath = "."
+            logs.report("fileio","forcing fallback ownpath .")
+        elseif trace_locating then
+            logs.report("fileio","using ownpath '%s'",ownpath)
+        end
+    end
+    resolvers.ownpath = ownpath
+    function resolvers.getownpath()
+        return resolvers.ownpath
+    end
+    return ownpath
+end
+
+local own_places = { "SELFAUTOLOC", "SELFAUTODIR", "SELFAUTOPARENT", "TEXMFCNF" }
+
+local function identify_own()
+    local ownpath = resolvers.getownpath() or dir.current()
+    local ie = instance.environment
+    if ownpath then
+        if resolvers.env('SELFAUTOLOC')    == "" then os.env['SELFAUTOLOC']    = file.collapse_path(ownpath) end
+        if resolvers.env('SELFAUTODIR')    == "" then os.env['SELFAUTODIR']    = file.collapse_path(ownpath .. "/..") end
+        if resolvers.env('SELFAUTOPARENT') == "" then os.env['SELFAUTOPARENT'] = file.collapse_path(ownpath .. "/../..") end
+    else
+        logs.report("fileio","error: unable to locate ownpath")
+        os.exit()
+    end
+    if resolvers.env('TEXMFCNF') == "" then os.env['TEXMFCNF'] = resolvers.cnfdefault end
+    if resolvers.env('TEXOS')    == "" then os.env['TEXOS']    = resolvers.env('SELFAUTODIR') end
+    if resolvers.env('TEXROOT')  == "" then os.env['TEXROOT']  = resolvers.env('SELFAUTOPARENT') end
+    if trace_locating then
+        for i=1,#own_places do
+            local v = own_places[i]
+            logs.report("fileio","variable '%s' set to '%s'",v,resolvers.env(v) or "unknown")
+        end
+    end
+    identify_own = function() end
+end
+
+function resolvers.identify_cnf()
+    if #instance.cnffiles == 0 then
+        -- fallback
+        identify_own()
+        -- the real search
+        resolvers.expand_variables()
+        local t = resolvers.split_path(resolvers.env('TEXMFCNF'))
+        t = expanded_path_from_list(t)
+        expand_vars(t) -- redundant
+        local function locate(filename,list)
+            for i=1,#t do
+                local ti = t[i]
+                local texmfcnf = file.collapse_path(file.join(ti,filename))
+                if lfs.isfile(texmfcnf) then
+                    list[#list+1] = texmfcnf
+                end
+            end
+        end
+        locate(resolvers.luaname,instance.luafiles)
+        locate(resolvers.cnfname,instance.cnffiles)
+    end
+end
+
+local function load_cnf_file(fname)
+    fname = resolvers.clean_path(fname)
+    local lname = file.replacesuffix(fname,'lua')
+    if lfs.isfile(lname) then
+        local dname = file.dirname(fname) -- fname ?
+        if not instance.configuration[dname] then
+            resolvers.load_data(dname,'configuration',lname and file.basename(lname))
+            instance.order[#instance.order+1] = instance.configuration[dname]
+        end
+    else
+        f = io.open(fname)
+        if f then
+            if trace_locating then
+                logs.report("fileio","loading configuration file %s", fname)
+            end
+            local line, data, n, k, v
+            local dname = file.dirname(fname)
+            if not instance.configuration[dname] then
+                instance.configuration[dname] = { }
+                instance.order[#instance.order+1] = instance.configuration[dname]
+            end
+            local data = instance.configuration[dname]
+            while true do
+                local line, n = f:read(), 0
+                if line then
+                    while true do -- join lines
+                        line, n = gsub(line,"\\%s*$", "")
+                        if n > 0 then
+                            line = line .. f:read()
+                        else
+                            break
+                        end
+                    end
+                    if not find(line,"^[%%#]") then
+                        local l = gsub(line,"%s*%%.*$","")
+                        local k, v = match(l,"%s*(.-)%s*=%s*(.-)%s*$")
+                        if k and v and not data[k] then
+                            v = gsub(v,"[%%#].*",'')
+                            data[k] = gsub(v,"~","$HOME")
+                            instance.kpsevars[k] = true
+                        end
+                    end
+                else
+                    break
+                end
+            end
+            f:close()
+        elseif trace_locating then
+            logs.report("fileio","skipping configuration file '%s'", fname)
+        end
+    end
+end
+
+local function collapse_cnf_data() -- potential optimization: pass start index (setup and configuration are shared)
+    local order = instance.order
+    for i=1,#order do
+        local c = order[i]
+        for k,v in next, c do
+            if not instance.variables[k] then
+                if instance.environment[k] then
+                    instance.variables[k] = instance.environment[k]
+                else
+                    instance.kpsevars[k] = true
+                    instance.variables[k] = resolvers.bare_variable(v)
+                end
+            end
+        end
+    end
+end
+
+function resolvers.load_cnf()
+    local function loadoldconfigdata()
+        local cnffiles = instance.cnffiles
+        for i=1,#cnffiles do
+            load_cnf_file(cnffiles[i])
+        end
+    end
+    -- instance.cnffiles contain complete names now !
+    -- we still use a funny mix of cnf and new but soon
+    -- we will switch to lua exclusively as we only use
+    -- the file to collect the tree roots
+    if #instance.cnffiles == 0 then
+        if trace_locating then
+            logs.report("fileio","no cnf files found (TEXMFCNF may not be set/known)")
+        end
+    else
+        local cnffiles = instance.cnffiles
+        instance.rootpath = cnffiles[1]
+        for k=1,#cnffiles do
+            instance.cnffiles[k] = file.collapse_path(cnffiles[k])
+        end
+        for i=1,3 do
+            instance.rootpath = file.dirname(instance.rootpath)
+        end
+        instance.rootpath = file.collapse_path(instance.rootpath)
+        if instance.diskcache and not instance.renewcache then
+            resolvers.loadoldconfig(instance.cnffiles)
+            if instance.loaderror then
+                loadoldconfigdata()
+                resolvers.saveoldconfig()
+            end
+        else
+            loadoldconfigdata()
+            if instance.renewcache then
+                resolvers.saveoldconfig()
+            end
+        end
+        collapse_cnf_data()
+    end
+    check_configuration()
+end
+
+function resolvers.load_lua()
+    if #instance.luafiles == 0 then
+        -- yet harmless
+    else
+        instance.rootpath = instance.luafiles[1]
+        local luafiles = instance.luafiles
+        for k=1,#luafiles do
+            instance.luafiles[k] = file.collapse_path(luafiles[k])
+        end
+        for i=1,3 do
+            instance.rootpath = file.dirname(instance.rootpath)
+        end
+        instance.rootpath = file.collapse_path(instance.rootpath)
+        resolvers.loadnewconfig()
+        collapse_cnf_data()
+    end
+    check_configuration()
+end
+
+-- database loading
+
+function resolvers.load_hash()
+    resolvers.locatelists()
+    if instance.diskcache and not instance.renewcache then
+        resolvers.loadfiles()
+        if instance.loaderror then
+            resolvers.loadlists()
+            resolvers.savefiles()
+        end
+    else
+        resolvers.loadlists()
+        if instance.renewcache then
+            resolvers.savefiles()
+        end
+    end
+end
+
+function resolvers.append_hash(type,tag,name)
+    if trace_locating then
+        logs.report("fileio","hash '%s' appended",tag)
+    end
+    insert(instance.hashes, { ['type']=type, ['tag']=tag, ['name']=name } )
+end
+
+function resolvers.prepend_hash(type,tag,name)
+    if trace_locating then
+        logs.report("fileio","hash '%s' prepended",tag)
+    end
+    insert(instance.hashes, 1, { ['type']=type, ['tag']=tag, ['name']=name } )
+end
+
+function resolvers.extend_texmf_var(specification) -- crap, we could better prepend the hash
+--  local t = resolvers.expanded_path_list('TEXMF') -- full expansion
+    local t = resolvers.split_path(resolvers.env('TEXMF'))
+    insert(t,1,specification)
+    local newspec = concat(t,";")
+    if instance.environment["TEXMF"] then
+        instance.environment["TEXMF"] = newspec
+    elseif instance.variables["TEXMF"] then
+        instance.variables["TEXMF"] = newspec
+    else
+        -- weird
+    end
+    resolvers.expand_variables()
+    reset_hashes()
+end
+
+-- locators
+
+function resolvers.locatelists()
+    local texmfpaths = resolvers.clean_path_list('TEXMF')
+    for i=1,#texmfpaths do
+        local path = texmfpaths[i]
+        if trace_locating then
+            logs.report("fileio","locating list of '%s'",path)
+        end
+        resolvers.locatedatabase(file.collapse_path(path))
+    end
+end
+
+function resolvers.locatedatabase(specification)
+    return resolvers.methodhandler('locators', specification)
+end
+
+function resolvers.locators.tex(specification)
+    if specification and specification ~= '' and lfs.isdir(specification) then
+        if trace_locating then
+            logs.report("fileio","tex locator '%s' found",specification)
+        end
+        resolvers.append_hash('file',specification,filename)
+    elseif trace_locating then
+        logs.report("fileio","tex locator '%s' not found",specification)
+    end
+end
+
+-- hashers
+
+function resolvers.hashdatabase(tag,name)
+    return resolvers.methodhandler('hashers',tag,name)
+end
+
+function resolvers.loadfiles()
+    instance.loaderror = false
+    instance.files = { }
+    if not instance.renewcache then
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            resolvers.hashdatabase(hash.tag,hash.name)
+            if instance.loaderror then break end
+        end
+    end
+end
+
+function resolvers.hashers.tex(tag,name)
+    resolvers.load_data(tag,'files')
+end
+
+-- generators:
+
+function resolvers.loadlists()
+    local hashes = instance.hashes
+    for i=1,#hashes do
+        resolvers.generatedatabase(hashes[i].tag)
+    end
+end
+
+function resolvers.generatedatabase(specification)
+    return resolvers.methodhandler('generators', specification)
+end
+
+-- starting with . or .. etc or funny char
+
+local weird = lpeg.P(".")^1 + lpeg.anywhere(lpeg.S("~`!#$%^&*()={}[]:;\"\'||<>,?\n\r\t"))
+
+--~ local l_forbidden = lpeg.S("~`!#$%^&*()={}[]:;\"\'||\\/<>,?\n\r\t")
+--~ local l_confusing = lpeg.P(" ")
+--~ local l_character = lpeg.patterns.utf8
+--~ local l_dangerous = lpeg.P(".")
+
+--~ local l_normal = (l_character - l_forbidden - l_confusing - l_dangerous) * (l_character - l_forbidden - l_confusing^2)^0 * lpeg.P(-1)
+--~ ----- l_normal = l_normal * lpeg.Cc(true) + lpeg.Cc(false)
+
+--~ local function test(str)
+--~     print(str,lpeg.match(l_normal,str))
+--~ end
+--~ test("ヒラギノ明朝 Pro W3")
+--~ test("..ヒラギノ明朝 Pro W3")
+--~ test(":ヒラギノ明朝 Pro W3;")
+--~ test("ヒラギノ明朝 /Pro W3;")
+--~ test("ヒラギノ明朝 Pro  W3")
+
+function resolvers.generators.tex(specification)
+    local tag = specification
+    if trace_locating then
+        logs.report("fileio","scanning path '%s'",specification)
+    end
+    instance.files[tag] = { }
+    local files = instance.files[tag]
+    local n, m, r = 0, 0, 0
+    local spec = specification .. '/'
+    local attributes = lfs.attributes
+    local directory = lfs.dir
+    local function action(path)
+        local full
+        if path then
+            full = spec .. path .. '/'
+        else
+            full = spec
+        end
+        for name in directory(full) do
+            if not lpegmatch(weird,name) then
+         -- if lpegmatch(l_normal,name) then
+                local mode = attributes(full..name,'mode')
+                if mode == 'file' then
+                    if path then
+                        n = n + 1
+                        local f = files[name]
+                        if f then
+                            if type(f) == 'string' then
+                                files[name] = { f, path }
+                            else
+                                f[#f+1] = path
+                            end
+                        else -- probably unique anyway
+                            files[name] = path
+                            local lower = lower(name)
+                            if name ~= lower then
+                                files["remap:"..lower] = name
+                                r = r + 1
+                            end
+                        end
+                    end
+                elseif mode == 'directory' then
+                    m = m + 1
+                    if path then
+                        action(path..'/'..name)
+                    else
+                        action(name)
+                    end
+                end
+            end
+        end
+    end
+    action()
+    if trace_locating then
+        logs.report("fileio","%s files found on %s directories with %s uppercase remappings",n,m,r)
+    end
+end
+
+-- savers, todo
+
+function resolvers.savefiles()
+    resolvers.save_data('files')
+end
+
+-- A config (optionally) has the paths split in tables. Internally
+-- we join them and split them after the expansion has taken place. This
+-- is more convenient.
+
+--~ local checkedsplit = string.checkedsplit
+
+local cache = { }
+
+local splitter = lpeg.Ct(lpeg.splitat(lpeg.S(os.type == "windows" and ";" or ":;")))
+
+local function split_kpse_path(str) -- beware, this can be either a path or a {specification}
+    local found = cache[str]
+    if not found then
+        if str == "" then
+            found = { }
+        else
+            str = gsub(str,"\\","/")
+--~             local split = (find(str,";") and checkedsplit(str,";")) or checkedsplit(str,io.pathseparator)
+local split = lpegmatch(splitter,str)
+            found = { }
+            for i=1,#split do
+                local s = split[i]
+                if not find(s,"^{*unset}*") then
+                    found[#found+1] = s
+                end
+            end
+            if trace_expansions then
+                logs.report("fileio","splitting path specification '%s'",str)
+                for k=1,#found do
+                    logs.report("fileio","% 4i: %s",k,found[k])
+                end
+            end
+            cache[str] = found
+        end
+    end
+    return found
+end
+
+resolvers.split_kpse_path = split_kpse_path
+
+function resolvers.splitconfig()
+    for i=1,#instance do
+        local c = instance[i]
+        for k,v in next, c do
+            if type(v) == 'string' then
+                local t = split_kpse_path(v)
+                if #t > 1 then
+                    c[k] = t
+                end
+            end
+        end
+    end
+end
+
+function resolvers.joinconfig()
+    local order = instance.order
+    for i=1,#order do
+        local c = order[i]
+        for k,v in next, c do -- indexed?
+            if type(v) == 'table' then
+                c[k] = file.join_path(v)
+            end
+        end
+    end
+end
+
+function resolvers.split_path(str)
+    if type(str) == 'table' then
+        return str
+    else
+        return split_kpse_path(str)
+    end
+end
+
+function resolvers.join_path(str)
+    if type(str) == 'table' then
+        return file.join_path(str)
+    else
+        return str
+    end
+end
+
+function resolvers.splitexpansions()
+    local ie = instance.expansions
+    for k,v in next, ie do
+        local t, h, p = { }, { }, split_kpse_path(v)
+        for kk=1,#p do
+            local vv = p[kk]
+            if vv ~= "" and not h[vv] then
+                t[#t+1] = vv
+                h[vv] = true
+            end
+        end
+        if #t > 1 then
+            ie[k] = t
+        else
+            ie[k] = t[1]
+        end
+    end
+end
+
+-- end of split/join code
+
+function resolvers.saveoldconfig()
+    resolvers.splitconfig()
+    resolvers.save_data('configuration')
+    resolvers.joinconfig()
+end
+
+resolvers.configbanner = [[
+-- This is a Luatex configuration file created by 'luatools.lua' or
+-- 'luatex.exe' directly. For comment, suggestions and questions you can
+-- contact the ConTeXt Development Team. This configuration file is
+-- not copyrighted. [HH & TH]
+]]
+
+function resolvers.serialize(files)
+    -- This version is somewhat optimized for the kind of
+    -- tables that we deal with, so it's much faster than
+    -- the generic serializer. This makes sense because
+    -- luatools and mtxtools are called frequently. Okay,
+    -- we pay a small price for properly tabbed tables.
+    local t = { }
+    local function dump(k,v,m) -- could be moved inline
+        if type(v) == 'string' then
+            return m .. "['" .. k .. "']='" .. v .. "',"
+        elseif #v == 1 then
+            return m .. "['" .. k .. "']='" .. v[1] .. "',"
+        else
+            return m .. "['" .. k .. "']={'" .. concat(v,"','").. "'},"
+        end
+    end
+    t[#t+1] = "return {"
+    if instance.sortdata then
+	local sortedfiles = sortedkeys(files)
+	for i=1,#sortedfiles do
+	    local k = sortedfiles[i]
+            local fk  = files[k]
+            if type(fk) == 'table' then
+                t[#t+1] = "\t['" .. k .. "']={"
+		local sortedfk = sortedkeys(fk)
+        	for j=1,#sortedfk do
+                    local kk = sortedfk[j]
+                    t[#t+1] = dump(kk,fk[kk],"\t\t")
+                end
+                t[#t+1] = "\t},"
+            else
+                t[#t+1] = dump(k,fk,"\t")
+            end
+        end
+    else
+        for k, v in next, files do
+            if type(v) == 'table' then
+                t[#t+1] = "\t['" .. k .. "']={"
+                for kk,vv in next, v do
+                    t[#t+1] = dump(kk,vv,"\t\t")
+                end
+                t[#t+1] = "\t},"
+            else
+                t[#t+1] = dump(k,v,"\t")
+            end
+        end
+    end
+    t[#t+1] = "}"
+    return concat(t,"\n")
+end
+
+local data_state = { }
+
+function resolvers.data_state()
+    return data_state or { }
+end
+
+function resolvers.save_data(dataname, makename) -- untested without cache overload
+    for cachename, files in next, instance[dataname] do
+        local name = (makename or file.join)(cachename,dataname)
+        local luaname, lucname = name .. ".lua", name .. ".luc"
+        if trace_locating then
+            logs.report("fileio","preparing '%s' for '%s'",dataname,cachename)
+        end
+        for k, v in next, files do
+            if type(v) == "table" and #v == 1 then
+                files[k] = v[1]
+            end
+        end
+        local data = {
+            type    = dataname,
+            root    = cachename,
+            version = resolvers.cacheversion,
+            date    = os.date("%Y-%m-%d"),
+            time    = os.date("%H:%M:%S"),
+            content = files,
+            uuid    = os.uuid(),
+        }
+        local ok = io.savedata(luaname,resolvers.serialize(data))
+        if ok then
+            if trace_locating then
+                logs.report("fileio","'%s' saved in '%s'",dataname,luaname)
+            end
+            if utils.lua.compile(luaname,lucname,false,true) then -- no cleanup but strip
+                if trace_locating then
+                    logs.report("fileio","'%s' compiled to '%s'",dataname,lucname)
+                end
+            else
+                if trace_locating then
+                    logs.report("fileio","compiling failed for '%s', deleting file '%s'",dataname,lucname)
+                end
+                os.remove(lucname)
+            end
+        elseif trace_locating then
+            logs.report("fileio","unable to save '%s' in '%s' (access error)",dataname,luaname)
+        end
+    end
+end
+
+function resolvers.load_data(pathname,dataname,filename,makename) -- untested without cache overload
+    filename = ((not filename or (filename == "")) and dataname) or filename
+    filename = (makename and makename(dataname,filename)) or file.join(pathname,filename)
+    local blob = loadfile(filename .. ".luc") or loadfile(filename .. ".lua")
+    if blob then
+        local data = blob()
+        if data and data.content and data.type == dataname and data.version == resolvers.cacheversion then
+            data_state[#data_state+1] = data.uuid
+            if trace_locating then
+                logs.report("fileio","loading '%s' for '%s' from '%s'",dataname,pathname,filename)
+            end
+            instance[dataname][pathname] = data.content
+        else
+            if trace_locating then
+                logs.report("fileio","skipping '%s' for '%s' from '%s'",dataname,pathname,filename)
+            end
+            instance[dataname][pathname] = { }
+            instance.loaderror = true
+        end
+    elseif trace_locating then
+        logs.report("fileio","skipping '%s' for '%s' from '%s'",dataname,pathname,filename)
+    end
+end
+
+-- some day i'll use the nested approach, but not yet (actually we even drop
+-- engine/progname support since we have only luatex now)
+--
+-- first texmfcnf.lua files are located, next the cached texmf.cnf files
+--
+-- return {
+--     TEXMFBOGUS = 'effe checken of dit werkt',
+-- }
+
+function resolvers.resetconfig()
+    identify_own()
+    instance.configuration, instance.setup, instance.order, instance.loaderror = { }, { }, { }, false
+end
+
+function resolvers.loadnewconfig()
+    local luafiles = instance.luafiles
+    for i=1,#luafiles do
+        local cnf = luafiles[i]
+        local pathname = file.dirname(cnf)
+        local filename = file.join(pathname,resolvers.luaname)
+        local blob = loadfile(filename)
+        if blob then
+            local data = blob()
+            if data then
+                if trace_locating then
+                    logs.report("fileio","loading configuration file '%s'",filename)
+                end
+                if true then
+                    -- flatten to variable.progname
+                    local t = { }
+                    for k, v in next, data do -- v = progname
+                        if type(v) == "string" then
+                            t[k] = v
+                        else
+                            for kk, vv in next, v do -- vv = variable
+                                if type(vv) == "string" then
+                                    t[vv.."."..v] = kk
+                                end
+                            end
+                        end
+                    end
+                    instance['setup'][pathname] = t
+                else
+                    instance['setup'][pathname] = data
+                end
+            else
+                if trace_locating then
+                    logs.report("fileio","skipping configuration file '%s'",filename)
+                end
+                instance['setup'][pathname] = { }
+                instance.loaderror = true
+            end
+        elseif trace_locating then
+            logs.report("fileio","skipping configuration file '%s'",filename)
+        end
+        instance.order[#instance.order+1] = instance.setup[pathname]
+        if instance.loaderror then break end
+    end
+end
+
+function resolvers.loadoldconfig()
+    if not instance.renewcache then
+        local cnffiles = instance.cnffiles
+        for i=1,#cnffiles do
+            local cnf = cnffiles[i]
+            local dname = file.dirname(cnf)
+            resolvers.load_data(dname,'configuration')
+            instance.order[#instance.order+1] = instance.configuration[dname]
+            if instance.loaderror then break end
+        end
+    end
+    resolvers.joinconfig()
+end
+
+function resolvers.expand_variables()
+    local expansions, environment, variables = { }, instance.environment, instance.variables
+    local env = resolvers.env
+    instance.expansions = expansions
+    if instance.engine   ~= "" then environment['engine']   = instance.engine   end
+    if instance.progname ~= "" then environment['progname'] = instance.progname end
+    for k,v in next, environment do
+        local a, b = match(k,"^(%a+)%_(.*)%s*$")
+        if a and b then
+            expansions[a..'.'..b] = v
+        else
+            expansions[k] = v
+        end
+    end
+    for k,v in next, environment do -- move environment to expansions
+        if not expansions[k] then expansions[k] = v end
+    end
+    for k,v in next, variables do -- move variables to expansions
+        if not expansions[k] then expansions[k] = v end
+    end
+    local busy = false
+    local function resolve(a)
+        busy = true
+        return expansions[a] or env(a)
+    end
+    while true do
+        busy = false
+        for k,v in next, expansions do
+            local s, n = gsub(v,"%$([%a%d%_%-]+)",resolve)
+            local s, m = gsub(s,"%$%{([%a%d%_%-]+)%}",resolve)
+            if n > 0 or m > 0 then
+                expansions[k]= s
+            end
+        end
+        if not busy then break end
+    end
+    for k,v in next, expansions do
+        expansions[k] = gsub(v,"\\", '/')
+    end
+end
+
+function resolvers.variable(name)
+    return entry(instance.variables,name)
+end
+
+function resolvers.expansion(name)
+    return entry(instance.expansions,name)
+end
+
+function resolvers.is_variable(name)
+    return is_entry(instance.variables,name)
+end
+
+function resolvers.is_expansion(name)
+    return is_entry(instance.expansions,name)
+end
+
+function resolvers.unexpanded_path_list(str)
+    local pth = resolvers.variable(str)
+    local lst = resolvers.split_path(pth)
+    return expanded_path_from_list(lst)
+end
+
+function resolvers.unexpanded_path(str)
+    return file.join_path(resolvers.unexpanded_path_list(str))
+end
+
+do -- no longer needed
+
+    local done = { }
+
+    function resolvers.reset_extra_path()
+        local ep = instance.extra_paths
+        if not ep then
+            ep, done = { }, { }
+            instance.extra_paths = ep
+        elseif #ep > 0 then
+            instance.lists, done = { }, { }
+        end
+    end
+
+    function resolvers.register_extra_path(paths,subpaths)
+        local ep = instance.extra_paths or { }
+        local n = #ep
+        if paths and paths ~= "" then
+            if subpaths and subpaths ~= "" then
+                for p in gmatch(paths,"[^,]+") do
+                    -- we gmatch each step again, not that fast, but used seldom
+                    for s in gmatch(subpaths,"[^,]+") do
+                        local ps = p .. "/" .. s
+                        if not done[ps] then
+                            ep[#ep+1] = resolvers.clean_path(ps)
+                            done[ps] = true
+                        end
+                    end
+                end
+            else
+                for p in gmatch(paths,"[^,]+") do
+                    if not done[p] then
+                        ep[#ep+1] = resolvers.clean_path(p)
+                        done[p] = true
+                    end
+                end
+            end
+        elseif subpaths and subpaths ~= "" then
+            for i=1,n do
+                -- we gmatch each step again, not that fast, but used seldom
+                for s in gmatch(subpaths,"[^,]+") do
+                    local ps = ep[i] .. "/" .. s
+                    if not done[ps] then
+                        ep[#ep+1] = resolvers.clean_path(ps)
+                        done[ps] = true
+                    end
+                end
+            end
+        end
+        if #ep > 0 then
+            instance.extra_paths = ep -- register paths
+        end
+        if #ep > n then
+            instance.lists = { } -- erase the cache
+        end
+    end
+
+end
+
+local function made_list(instance,list)
+    local ep = instance.extra_paths
+    if not ep or #ep == 0 then
+        return list
+    else
+        local done, new = { }, { }
+        -- honour . .. ../.. but only when at the start
+        for k=1,#list do
+            local v = list[k]
+            if not done[v] then
+                if find(v,"^[%.%/]$") then
+                    done[v] = true
+                    new[#new+1] = v
+                else
+                    break
+                end
+            end
+        end
+        -- first the extra paths
+        for k=1,#ep do
+            local v = ep[k]
+            if not done[v] then
+                done[v] = true
+                new[#new+1] = v
+            end
+        end
+        -- next the formal paths
+        for k=1,#list do
+            local v = list[k]
+            if not done[v] then
+                done[v] = true
+                new[#new+1] = v
+            end
+        end
+        return new
+    end
+end
+
+function resolvers.clean_path_list(str)
+    local t = resolvers.expanded_path_list(str)
+    if t then
+        for i=1,#t do
+            t[i] = file.collapse_path(resolvers.clean_path(t[i]))
+        end
+    end
+    return t
+end
+
+function resolvers.expand_path(str)
+    return file.join_path(resolvers.expanded_path_list(str))
+end
+
+function resolvers.expanded_path_list(str)
+    if not str then
+        return ep or { } -- ep ?
+    elseif instance.savelists then
+        -- engine+progname hash
+        str = gsub(str,"%$","")
+        if not instance.lists[str] then -- cached
+            local lst = made_list(instance,resolvers.split_path(resolvers.expansion(str)))
+            instance.lists[str] = expanded_path_from_list(lst)
+        end
+        return instance.lists[str]
+    else
+        local lst = resolvers.split_path(resolvers.expansion(str))
+        return made_list(instance,expanded_path_from_list(lst))
+    end
+end
+
+function resolvers.expanded_path_list_from_var(str) -- brrr
+    local tmp = resolvers.var_of_format_or_suffix(gsub(str,"%$",""))
+    if tmp ~= "" then
+        return resolvers.expanded_path_list(tmp)
+    else
+        return resolvers.expanded_path_list(str)
+    end
+end
+
+function resolvers.expand_path_from_var(str)
+    return file.join_path(resolvers.expanded_path_list_from_var(str))
+end
+
+function resolvers.format_of_var(str)
+    return formats[str] or formats[alternatives[str]] or ''
+end
+function resolvers.format_of_suffix(str)
+    return suffixmap[file.extname(str)] or 'tex'
+end
+
+function resolvers.variable_of_format(str)
+    return formats[str] or formats[alternatives[str]] or ''
+end
+
+function resolvers.var_of_format_or_suffix(str)
+    local v = formats[str]
+    if v then
+        return v
+    end
+    v = formats[alternatives[str]]
+    if v then
+        return v
+    end
+    v = suffixmap[file.extname(str)]
+    if v then
+        return formats[isf]
+    end
+    return ''
+end
+
+function resolvers.expand_braces(str) -- output variable and brace expansion of STRING
+    local ori = resolvers.variable(str)
+    local pth = expanded_path_from_list(resolvers.split_path(ori))
+    return file.join_path(pth)
+end
+
+resolvers.isreadable = { }
+
+function resolvers.isreadable.file(name)
+    local readable = lfs.isfile(name) -- brrr
+    if trace_detail then
+        if readable then
+            logs.report("fileio","file '%s' is readable",name)
+        else
+            logs.report("fileio","file '%s' is not readable", name)
+        end
+    end
+    return readable
+end
+
+resolvers.isreadable.tex = resolvers.isreadable.file
+
+-- name
+-- name/name
+
+local function collect_files(names)
+    local filelist = { }
+    for k=1,#names do
+        local fname = names[k]
+        if trace_detail then
+            logs.report("fileio","checking name '%s'",fname)
+        end
+        local bname = file.basename(fname)
+        local dname = file.dirname(fname)
+        if dname == "" or find(dname,"^%.") then
+            dname = false
+        else
+            dname = "/" .. dname .. "$"
+        end
+        local hashes = instance.hashes
+        for h=1,#hashes do
+            local hash = hashes[h]
+            local blobpath = hash.tag
+            local files = blobpath and instance.files[blobpath]
+            if files then
+                if trace_detail then
+                    logs.report("fileio","deep checking '%s' (%s)",blobpath,bname)
+                end
+                local blobfile = files[bname]
+                if not blobfile then
+                    local rname = "remap:"..bname
+                    blobfile = files[rname]
+                    if blobfile then
+                        bname = files[rname]
+                        blobfile = files[bname]
+                    end
+                end
+                if blobfile then
+                    if type(blobfile) == 'string' then
+                        if not dname or find(blobfile,dname) then
+                            filelist[#filelist+1] = {
+                                hash.type,
+                                file.join(blobpath,blobfile,bname), -- search
+                                resolvers.concatinators[hash.type](blobpath,blobfile,bname) -- result
+                            }
+                        end
+                    else
+                        for kk=1,#blobfile do
+                            local vv = blobfile[kk]
+                            if not dname or find(vv,dname) then
+                                filelist[#filelist+1] = {
+                                    hash.type,
+                                    file.join(blobpath,vv,bname), -- search
+                                    resolvers.concatinators[hash.type](blobpath,vv,bname) -- result
+                                }
+                            end
+                        end
+                    end
+                end
+            elseif trace_locating then
+                logs.report("fileio","no match in '%s' (%s)",blobpath,bname)
+            end
+        end
+    end
+    if #filelist > 0 then
+        return filelist
+    else
+        return nil
+    end
+end
+
+function resolvers.suffix_of_format(str)
+    if suffixes[str] then
+        return suffixes[str][1]
+    else
+        return ""
+    end
+end
+
+function resolvers.suffixes_of_format(str)
+    if suffixes[str] then
+        return suffixes[str]
+    else
+        return {}
+    end
+end
+
+function resolvers.register_in_trees(name)
+    if not find(name,"^%.") then
+        instance.foundintrees[name] = (instance.foundintrees[name] or 0) + 1 -- maybe only one
+    end
+end
+
+-- split the next one up for readability (bu this module needs a cleanup anyway)
+
+local function can_be_dir(name) -- can become local
+    local fakepaths = instance.fakepaths
+    if not fakepaths[name] then
+        if lfs.isdir(name) then
+            fakepaths[name] = 1 -- directory
+        else
+            fakepaths[name] = 2 -- no directory
+        end
+    end
+    return (fakepaths[name] == 1)
+end
+
+local function collect_instance_files(filename,collected) -- todo : plugin (scanners, checkers etc)
+    local result = collected or { }
+    local stamp  = nil
+    filename = file.collapse_path(filename)
+    -- speed up / beware: format problem
+    if instance.remember then
+        stamp = filename .. "--" .. instance.engine .. "--" .. instance.progname .. "--" .. instance.format
+        if instance.found[stamp] then
+            if trace_locating then
+                logs.report("fileio","remembering file '%s'",filename)
+            end
+            return instance.found[stamp]
+        end
+    end
+    if not dangerous[instance.format or "?"] then
+        if resolvers.isreadable.file(filename) then
+            if trace_detail then
+                logs.report("fileio","file '%s' found directly",filename)
+            end
+            instance.found[stamp] = { filename }
+            return { filename }
+        end
+    end
+    if find(filename,'%*') then
+        if trace_locating then
+            logs.report("fileio","checking wildcard '%s'", filename)
+        end
+        result = resolvers.find_wildcard_files(filename)
+    elseif file.is_qualified_path(filename) then
+        if resolvers.isreadable.file(filename) then
+            if trace_locating then
+                logs.report("fileio","qualified name '%s'", filename)
+            end
+            result = { filename }
+        else
+            local forcedname, ok, suffix = "", false, file.extname(filename)
+            if suffix == "" then -- why
+                if instance.format == "" then
+                    forcedname = filename .. ".tex"
+                    if resolvers.isreadable.file(forcedname) then
+                        if trace_locating then
+                            logs.report("fileio","no suffix, forcing standard filetype 'tex'")
+                        end
+                        result, ok = { forcedname }, true
+                    end
+                else
+                    local suffixes = resolvers.suffixes_of_format(instance.format)
+                    for _, s in next, suffixes do
+                        forcedname = filename .. "." .. s
+                        if resolvers.isreadable.file(forcedname) then
+                            if trace_locating then
+                                logs.report("fileio","no suffix, forcing format filetype '%s'", s)
+                            end
+                            result, ok = { forcedname }, true
+                            break
+                        end
+                    end
+                end
+            end
+            if not ok and suffix ~= "" then
+                -- try to find in tree (no suffix manipulation), here we search for the
+                -- matching last part of the name
+                local basename = file.basename(filename)
+                local pattern = gsub(filename .. "$","([%.%-])","%%%1")
+                local savedformat = instance.format
+                local format = savedformat or ""
+                if format == "" then
+                    instance.format = resolvers.format_of_suffix(suffix)
+                end
+                if not format then
+                    instance.format = "othertextfiles" -- kind of everything, maybe texinput is better
+                end
+                --
+                if basename ~= filename then
+                    local resolved = collect_instance_files(basename)
+                    if #result == 0 then
+                        local lowered = lower(basename)
+                        if filename ~= lowered then
+                            resolved = collect_instance_files(lowered)
+                        end
+                    end
+                    resolvers.format = savedformat
+                    --
+                    for r=1,#resolved do
+                        local rr = resolved[r]
+                        if find(rr,pattern) then
+                            result[#result+1], ok = rr, true
+                        end
+                    end
+                end
+                -- a real wildcard:
+                --
+                -- if not ok then
+                --     local filelist = collect_files({basename})
+                --     for f=1,#filelist do
+                --         local ff = filelist[f][3] or ""
+                --         if find(ff,pattern) then
+                --             result[#result+1], ok = ff, true
+                --         end
+                --     end
+                -- end
+            end
+            if not ok and trace_locating then
+                logs.report("fileio","qualified name '%s'", filename)
+            end
+        end
+    else
+        -- search spec
+        local filetype, extra, done, wantedfiles, ext = '', nil, false, { }, file.extname(filename)
+        if ext == "" then
+            if not instance.force_suffixes then
+                wantedfiles[#wantedfiles+1] = filename
+            end
+        else
+            wantedfiles[#wantedfiles+1] = filename
+        end
+        if instance.format == "" then
+            if ext == "" then
+                local forcedname = filename .. '.tex'
+                wantedfiles[#wantedfiles+1] = forcedname
+                filetype = resolvers.format_of_suffix(forcedname)
+                if trace_locating then
+                    logs.report("fileio","forcing filetype '%s'",filetype)
+                end
+            else
+                filetype = resolvers.format_of_suffix(filename)
+                if trace_locating then
+                    logs.report("fileio","using suffix based filetype '%s'",filetype)
+                end
+            end
+        else
+            if ext == "" then
+                local suffixes = resolvers.suffixes_of_format(instance.format)
+                for _, s in next, suffixes do
+                    wantedfiles[#wantedfiles+1] = filename .. "." .. s
+                end
+            end
+            filetype = instance.format
+            if trace_locating then
+                logs.report("fileio","using given filetype '%s'",filetype)
+            end
+        end
+        local typespec = resolvers.variable_of_format(filetype)
+        local pathlist = resolvers.expanded_path_list(typespec)
+        if not pathlist or #pathlist == 0 then
+            -- no pathlist, access check only / todo == wildcard
+            if trace_detail then
+                logs.report("fileio","checking filename '%s', filetype '%s', wanted files '%s'",filename, filetype or '?',concat(wantedfiles," | "))
+            end
+            for k=1,#wantedfiles do
+                local fname = wantedfiles[k]
+                if fname and resolvers.isreadable.file(fname) then
+                    filename, done = fname, true
+                    result[#result+1] = file.join('.',fname)
+                    break
+                end
+            end
+            -- this is actually 'other text files' or 'any' or 'whatever'
+            local filelist = collect_files(wantedfiles)
+            local fl = filelist and filelist[1]
+            if fl then
+                filename = fl[3]
+                result[#result+1] = filename
+                done = true
+            end
+        else
+            -- list search
+            local filelist = collect_files(wantedfiles)
+            local dirlist = { }
+            if filelist then
+                for i=1,#filelist do
+                    dirlist[i] = file.dirname(filelist[i][2]) .. "/"
+                end
+            end
+            if trace_detail then
+                logs.report("fileio","checking filename '%s'",filename)
+            end
+            -- a bit messy ... esp the doscan setting here
+            local doscan
+            for k=1,#pathlist do
+                local path = pathlist[k]
+                if find(path,"^!!") then doscan  = false else doscan  = true  end
+                local pathname = gsub(path,"^!+", '')
+                done = false
+                -- using file list
+                if filelist then
+                    local expression
+                    -- compare list entries with permitted pattern -- /xx /xx//
+                    if not find(pathname,"/$") then
+                        expression = pathname .. "/"
+                    else
+                        expression = pathname
+                    end
+                    expression = gsub(expression,"([%-%.])","%%%1") -- this also influences
+                    expression = gsub(expression,"//+$", '/.*')     -- later usage of pathname
+                    expression = gsub(expression,"//", '/.-/')      -- not ok for /// but harmless
+                    expression = "^" .. expression .. "$"
+                    if trace_detail then
+                        logs.report("fileio","using pattern '%s' for path '%s'",expression,pathname)
+                    end
+                    for k=1,#filelist do
+                        local fl = filelist[k]
+                        local f = fl[2]
+                        local d = dirlist[k]
+                        if find(d,expression) then
+                            --- todo, test for readable
+                            result[#result+1] = fl[3]
+                            resolvers.register_in_trees(f) -- for tracing used files
+                            done = true
+                            if instance.allresults then
+                                if trace_detail then
+                                    logs.report("fileio","match in hash for file '%s' on path '%s', continue scanning",f,d)
+                                end
+                            else
+                                if trace_detail then
+                                    logs.report("fileio","match in hash for file '%s' on path '%s', quit scanning",f,d)
+                                end
+                                break
+                            end
+                        elseif trace_detail then
+                            logs.report("fileio","no match in hash for file '%s' on path '%s'",f,d)
+                        end
+                    end
+                end
+                if not done and doscan then
+                    -- check if on disk / unchecked / does not work at all / also zips
+                    if resolvers.splitmethod(pathname).scheme == 'file' then -- ?
+                        local pname = gsub(pathname,"%.%*$",'')
+                        if not find(pname,"%*") then
+                            local ppname = gsub(pname,"/+$","")
+                            if can_be_dir(ppname) then
+                                for k=1,#wantedfiles do
+                                    local w = wantedfiles[k]
+                                    local fname = file.join(ppname,w)
+                                    if resolvers.isreadable.file(fname) then
+                                        if trace_detail then
+                                            logs.report("fileio","found '%s' by scanning",fname)
+                                        end
+                                        result[#result+1] = fname
+                                        done = true
+                                        if not instance.allresults then break end
+                                    end
+                                end
+                            else
+                                -- no access needed for non existing path, speedup (esp in large tree with lots of fake)
+                            end
+                        end
+                    end
+                end
+                if not done and doscan then
+                    -- todo: slow path scanning
+                end
+                if done and not instance.allresults then break end
+            end
+        end
+    end
+    for k=1,#result do
+        result[k] = file.collapse_path(result[k])
+    end
+    if instance.remember then
+        instance.found[stamp] = result
+    end
+    return result
+end
+
+if not resolvers.concatinators  then resolvers.concatinators = { } end
+
+resolvers.concatinators.tex  = file.join
+resolvers.concatinators.file = resolvers.concatinators.tex
+
+function resolvers.find_files(filename,filetype,mustexist)
+    if type(mustexist) == boolean then
+        -- all set
+    elseif type(filetype) == 'boolean' then
+        filetype, mustexist = nil, false
+    elseif type(filetype) ~= 'string' then
+        filetype, mustexist = nil, false
+    end
+    instance.format = filetype or ''
+    local result = collect_instance_files(filename)
+    if #result == 0 then
+        local lowered = lower(filename)
+        if filename ~= lowered then
+            return collect_instance_files(lowered)
+        end
+    end
+    instance.format = ''
+    return result
+end
+
+function resolvers.find_file(filename,filetype,mustexist)
+    return (resolvers.find_files(filename,filetype,mustexist)[1] or "")
+end
+
+function resolvers.find_given_files(filename)
+    local bname, result = file.basename(filename), { }
+    local hashes = instance.hashes
+    for k=1,#hashes do
+        local hash = hashes[k]
+        local files = instance.files[hash.tag] or { }
+        local blist = files[bname]
+        if not blist then
+            local rname = "remap:"..bname
+            blist = files[rname]
+            if blist then
+                bname = files[rname]
+                blist = files[bname]
+            end
+        end
+        if blist then
+            if type(blist) == 'string' then
+                result[#result+1] = resolvers.concatinators[hash.type](hash.tag,blist,bname) or ""
+                if not instance.allresults then break end
+            else
+                for kk=1,#blist do
+                    local vv = blist[kk]
+                    result[#result+1] = resolvers.concatinators[hash.type](hash.tag,vv,bname) or ""
+                    if not instance.allresults then break end
+                end
+            end
+        end
+    end
+    return result
+end
+
+function resolvers.find_given_file(filename)
+    return (resolvers.find_given_files(filename)[1] or "")
+end
+
+local function doit(path,blist,bname,tag,kind,result,allresults)
+    local done = false
+    if blist and kind then
+        if type(blist) == 'string' then
+            -- make function and share code
+            if find(lower(blist),path) then
+                result[#result+1] = resolvers.concatinators[kind](tag,blist,bname) or ""
+                done = true
+            end
+        else
+            for kk=1,#blist do
+                local vv = blist[kk]
+                if find(lower(vv),path) then
+                    result[#result+1] = resolvers.concatinators[kind](tag,vv,bname) or ""
+                    done = true
+                    if not allresults then break end
+                end
+            end
+        end
+    end
+    return done
+end
+
+function resolvers.find_wildcard_files(filename) -- todo: remap:
+    local result = { }
+    local bname, dname = file.basename(filename), file.dirname(filename)
+    local path = gsub(dname,"^*/","")
+    path = gsub(path,"*",".*")
+    path = gsub(path,"-","%%-")
+    if dname == "" then
+        path = ".*"
+    end
+    local name = bname
+    name = gsub(name,"*",".*")
+    name = gsub(name,"-","%%-")
+    path = lower(path)
+    name = lower(name)
+    local files, allresults, done = instance.files, instance.allresults, false
+    if find(name,"%*") then
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            local tag, kind = hash.tag, hash.type
+            for kk, hh in next, files[hash.tag] do
+                if not find(kk,"^remap:") then
+                    if find(lower(kk),name) then
+                        if doit(path,hh,kk,tag,kind,result,allresults) then done = true end
+                        if done and not allresults then break end
+                    end
+                end
+            end
+        end
+    else
+        local hashes = instance.hashes
+        for k=1,#hashes do
+            local hash = hashes[k]
+            local tag, kind = hash.tag, hash.type
+            if doit(path,files[tag][bname],bname,tag,kind,result,allresults) then done = true end
+            if done and not allresults then break end
+        end
+    end
+    -- we can consider also searching the paths not in the database, but then
+    -- we end up with a messy search (all // in all path specs)
+    return result
+end
+
+function resolvers.find_wildcard_file(filename)
+    return (resolvers.find_wildcard_files(filename)[1] or "")
+end
+
+-- main user functions
+
+function resolvers.automount()
+    -- implemented later
+end
+
+function resolvers.load(option)
+    statistics.starttiming(instance)
+    resolvers.resetconfig()
+    resolvers.identify_cnf()
+    resolvers.load_lua() -- will become the new method
+    resolvers.expand_variables()
+    resolvers.load_cnf() -- will be skipped when we have a lua file
+    resolvers.expand_variables()
+    if option ~= "nofiles" then
+        resolvers.load_hash()
+        resolvers.automount()
+    end
+    statistics.stoptiming(instance)
+end
+
+function resolvers.for_files(command, files, filetype, mustexist)
+    if files and #files > 0 then
+        local function report(str)
+            if trace_locating then
+                logs.report("fileio",str) -- has already verbose
+            else
+                print(str)
+            end
+        end
+        if trace_locating then
+            report('') -- ?
+        end
+        for f=1,#files do
+            local file = files[f]
+            local result = command(file,filetype,mustexist)
+            if type(result) == 'string' then
+                report(result)
+            else
+                for i=1,#result do
+                    report(result[i]) -- could be unpack
+                end
+            end
+        end
+    end
+end
+
+-- strtab
+
+resolvers.var_value  = resolvers.variable   -- output the value of variable $STRING.
+resolvers.expand_var = resolvers.expansion  -- output variable expansion of STRING.
+
+function resolvers.show_path(str)     -- output search path for file type NAME
+    return file.join_path(resolvers.expanded_path_list(resolvers.format_of_var(str)))
+end
+
+-- resolvers.find_file(filename)
+-- resolvers.find_file(filename, filetype, mustexist)
+-- resolvers.find_file(filename, mustexist)
+-- resolvers.find_file(filename, filetype)
+
+function resolvers.register_file(files, name, path)
+    if files[name] then
+        if type(files[name]) == 'string' then
+            files[name] = { files[name], path }
+        else
+            files[name] = path
+        end
+    else
+        files[name] = path
+    end
+end
+
+function resolvers.splitmethod(filename)
+    if not filename then
+        return { } -- safeguard
+    elseif type(filename) == "table" then
+        return filename -- already split
+    elseif not find(filename,"://") then
+        return { scheme="file", path = filename, original=filename } -- quick hack
+    else
+        return url.hashed(filename)
+    end
+end
+
+function table.sequenced(t,sep) -- temp here
+    local s = { }
+    for k, v in next, t do -- indexed?
+        s[#s+1] = k .. "=" .. tostring(v)
+    end
+    return concat(s, sep or " | ")
+end
+
+function resolvers.methodhandler(what, filename, filetype) -- ...
+    filename = file.collapse_path(filename)
+    local specification = (type(filename) == "string" and resolvers.splitmethod(filename)) or filename -- no or { }, let it bomb
+    local scheme = specification.scheme
+    if resolvers[what][scheme] then
+        if trace_locating then
+            logs.report("fileio","handler '%s' -> '%s' -> '%s'",specification.original,what,table.sequenced(specification))
+        end
+        return resolvers[what][scheme](filename,filetype) -- todo: specification
+    else
+        return resolvers[what].tex(filename,filetype) -- todo: specification
+    end
+end
+
+function resolvers.clean_path(str)
+    if str then
+        str = gsub(str,"\\","/")
+        str = gsub(str,"^!+","")
+        str = gsub(str,"^~",resolvers.homedir)
+        return str
+    else
+        return nil
+    end
+end
+
+function resolvers.do_with_path(name,func)
+    local pathlist = resolvers.expanded_path_list(name)
+    for i=1,#pathlist do
+        func("^"..resolvers.clean_path(pathlist[i]))
+    end
+end
+
+function resolvers.do_with_var(name,func)
+    func(expanded_var(name))
+end
+
+function resolvers.with_files(pattern,handle)
+    local hashes = instance.hashes
+    for i=1,#hashes do
+        local hash = hashes[i]
+        local blobpath = hash.tag
+        local blobtype = hash.type
+        if blobpath then
+            local files = instance.files[blobpath]
+            if files then
+                for k,v in next, files do
+                    if find(k,"^remap:") then
+                        k = files[k]
+                        v = files[k] -- chained
+                    end
+                    if find(k,pattern) then
+                        if type(v) == "string" then
+                            handle(blobtype,blobpath,v,k)
+                        else
+                            for _,vv in next, v do -- indexed
+                                handle(blobtype,blobpath,vv,k)
+                            end
+                        end
+                    end
+                end
+            end
+        end
+    end
+end
+
+function resolvers.locate_format(name)
+    local barename, fmtname = gsub(name,"%.%a+$",""), ""
+    if resolvers.usecache then
+        local path = file.join(caches.setpath("formats")) -- maybe platform
+        fmtname = file.join(path,barename..".fmt") or ""
+    end
+    if fmtname == "" then
+        fmtname = resolvers.find_files(barename..".fmt")[1] or ""
+    end
+    fmtname = resolvers.clean_path(fmtname)
+    if fmtname ~= "" then
+        local barename = file.removesuffix(fmtname)
+        local luaname, lucname, luiname = barename .. ".lua", barename .. ".luc", barename .. ".lui"
+        if lfs.isfile(luiname) then
+            return barename, luiname
+        elseif lfs.isfile(lucname) then
+            return barename, lucname
+        elseif lfs.isfile(luaname) then
+            return barename, luaname
+        end
+    end
+    return nil, nil
+end
+
+function resolvers.boolean_variable(str,default)
+    local b = resolvers.expansion(str)
+    if b == "" then
+        return default
+    else
+        b = toboolean(b)
+        return (b == nil and default) or b
+    end
+end
+
+texconfig.kpse_init = false
+
+kpse = { original = kpse } setmetatable(kpse, { __index = function(k,v) return resolvers[v] end } )
+
+-- for a while
+
+input = resolvers
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-tmp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+--[[ldx--
+<p>This module deals with caching data. It sets up the paths and
+implements loaders and savers for tables. Best is to set the
+following variable. When not set, the usual paths will be
+checked. Personally I prefer the (users) temporary path.</p>
+
+</code>
+TEXMFCACHE=$TMP;$TEMP;$TMPDIR;$TEMPDIR;$HOME;$TEXMFVAR;$VARTEXMF;.
+</code>
+
+<p>Currently we do no locking when we write files. This is no real
+problem because most caching involves fonts and the chance of them
+being written at the same time is small. We also need to extend
+luatools with a recache feature.</p>
+--ldx]]--
+
+local format, lower, gsub = string.format, string.lower, string.gsub
+
+local trace_cache = false  trackers.register("resolvers.cache", function(v) trace_cache = v end) -- not used yet
+
+caches = caches or { }
+
+caches.path     = caches.path or nil
+caches.base     = caches.base or "luatex-cache"
+caches.more     = caches.more or "context"
+caches.direct   = false -- true is faster but may need huge amounts of memory
+caches.tree     = false
+caches.paths    = caches.paths or nil
+caches.force    = false
+caches.defaults = { "TEXMFCACHE", "TMPDIR", "TEMPDIR", "TMP", "TEMP", "HOME", "HOMEPATH" }
+
+function caches.temp()
+    local cachepath = nil
+    local function check(list,isenv)
+        if not cachepath then
+            for k=1,#list do
+                local v = list[k]
+                cachepath = (isenv and (os.env[v] or "")) or v or ""
+                if cachepath == "" then
+                    -- next
+                else
+                    cachepath = resolvers.clean_path(cachepath)
+                    if lfs.isdir(cachepath) and file.iswritable(cachepath) then -- lfs.attributes(cachepath,"mode") == "directory"
+                        break
+                    elseif caches.force or io.ask(format("\nShould I create the cache path %s?",cachepath), "no", { "yes", "no" }) == "yes" then
+                        dir.mkdirs(cachepath)
+                        if lfs.isdir(cachepath) and file.iswritable(cachepath) then
+                            break
+                        end
+                    end
+                end
+                cachepath = nil
+            end
+        end
+    end
+    check(resolvers.clean_path_list("TEXMFCACHE") or { })
+    check(caches.defaults,true)
+    if not cachepath then
+        print("\nfatal error: there is no valid (writable) cache path defined\n")
+        os.exit()
+    elseif not lfs.isdir(cachepath) then -- lfs.attributes(cachepath,"mode") ~= "directory"
+        print(format("\nfatal error: cache path %s is not a directory\n",cachepath))
+        os.exit()
+    end
+    cachepath = file.collapse_path(cachepath)
+    function caches.temp()
+        return cachepath
+    end
+    return cachepath
+end
+
+function caches.configpath()
+    return table.concat(resolvers.instance.cnffiles,";")
+end
+
+function caches.hashed(tree)
+    return md5.hex(gsub(lower(tree),"[\\\/]+","/"))
+end
+
+function caches.treehash()
+    local tree = caches.configpath()
+    if not tree or tree == "" then
+        return false
+    else
+        return caches.hashed(tree)
+    end
+end
+
+function caches.setpath(...)
+    if not caches.path then
+        if not caches.path then
+            caches.path = caches.temp()
+        end
+        caches.path = resolvers.clean_path(caches.path) -- to be sure
+        caches.tree = caches.tree or caches.treehash()
+        if caches.tree then
+            caches.path = dir.mkdirs(caches.path,caches.base,caches.more,caches.tree)
+        else
+            caches.path = dir.mkdirs(caches.path,caches.base,caches.more)
+        end
+    end
+    if not caches.path then
+        caches.path = '.'
+    end
+    caches.path = resolvers.clean_path(caches.path)
+    local dirs = { ... }
+    if #dirs > 0 then
+        local pth = dir.mkdirs(caches.path,...)
+        return pth
+    end
+    caches.path = dir.expand_name(caches.path)
+    return caches.path
+end
+
+function caches.definepath(category,subcategory)
+    return function()
+        return caches.setpath(category,subcategory)
+    end
+end
+
+function caches.setluanames(path,name)
+    return path .. "/" .. name .. ".tma", path .. "/" .. name .. ".tmc"
+end
+
+function caches.loaddata(path,name)
+    local tmaname, tmcname = caches.setluanames(path,name)
+    local loader = loadfile(tmcname) or loadfile(tmaname)
+    if loader then
+        loader = loader()
+        collectgarbage("step")
+        return loader
+    else
+        return false
+    end
+end
+
+--~ function caches.loaddata(path,name)
+--~     local tmaname, tmcname = caches.setluanames(path,name)
+--~     return dofile(tmcname) or dofile(tmaname)
+--~ end
+
+function caches.iswritable(filepath,filename)
+    local tmaname, tmcname = caches.setluanames(filepath,filename)
+    return file.iswritable(tmaname)
+end
+
+function caches.savedata(filepath,filename,data,raw)
+    local tmaname, tmcname = caches.setluanames(filepath,filename)
+    local reduce, simplify = true, true
+    if raw then
+        reduce, simplify = false, false
+    end
+    data.cache_uuid = os.uuid()
+    if caches.direct then
+        file.savedata(tmaname, table.serialize(data,'return',false,true,false)) -- no hex
+    else
+        table.tofile(tmaname, data,'return',false,true,false) -- maybe not the last true
+    end
+    local cleanup = resolvers.boolean_variable("PURGECACHE", false)
+    local strip = resolvers.boolean_variable("LUACSTRIP", true)
+    utils.lua.compile(tmaname, tmcname, cleanup, strip)
+end
+
+-- here we use the cache for format loading (texconfig.[formatname|jobname])
+
+--~ if tex and texconfig and texconfig.formatname and texconfig.formatname == "" then
+if tex and texconfig and (not texconfig.formatname or texconfig.formatname == "") and input and resolvers.instance then
+    if not texconfig.luaname then texconfig.luaname = "cont-en.lua" end -- or luc
+    texconfig.formatname = caches.setpath("formats") .. "/" .. gsub(texconfig.luaname,"%.lu.$",".fmt")
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-res'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+--~ print(resolvers.resolve("abc env:tmp file:cont-en.tex path:cont-en.tex full:cont-en.tex rel:zapf/one/p-chars.tex"))
+
+local upper, lower, gsub = string.upper, string.lower, string.gsub
+
+local prefixes = { }
+
+prefixes.environment = function(str)
+    return resolvers.clean_path(os.getenv(str) or os.getenv(upper(str)) or os.getenv(lower(str)) or "")
+end
+
+prefixes.relative = function(str,n)
+    if io.exists(str) then
+        -- nothing
+    elseif io.exists("./" .. str) then
+        str = "./" .. str
+    else
+        local p = "../"
+        for i=1,n or 2 do
+            if io.exists(p .. str) then
+                str = p .. str
+                break
+            else
+                p = p .. "../"
+            end
+        end
+    end
+    return resolvers.clean_path(str)
+end
+
+prefixes.auto = function(str)
+    local fullname = prefixes.relative(str)
+    if not lfs.isfile(fullname) then
+        fullname = prefixes.locate(str)
+    end
+    return fullname
+end
+
+prefixes.locate = function(str)
+    local fullname = resolvers.find_given_file(str) or ""
+    return resolvers.clean_path((fullname ~= "" and fullname) or str)
+end
+
+prefixes.filename = function(str)
+    local fullname = resolvers.find_given_file(str) or ""
+    return resolvers.clean_path(file.basename((fullname ~= "" and fullname) or str))
+end
+
+prefixes.pathname = function(str)
+    local fullname = resolvers.find_given_file(str) or ""
+    return resolvers.clean_path(file.dirname((fullname ~= "" and fullname) or str))
+end
+
+prefixes.env  = prefixes.environment
+prefixes.rel  = prefixes.relative
+prefixes.loc  = prefixes.locate
+prefixes.kpse = prefixes.locate
+prefixes.full = prefixes.locate
+prefixes.file = prefixes.filename
+prefixes.path = prefixes.pathname
+
+function resolvers.allprefixes(separator)
+    local all = table.sortedkeys(prefixes)
+    if separator then
+        for i=1,#all do
+            all[i] = all[i] .. ":"
+        end
+    end
+    return all
+end
+
+local function _resolve_(method,target)
+    if prefixes[method] then
+        return prefixes[method](target)
+    else
+        return method .. ":" .. target
+    end
+end
+
+local function resolve(str)
+    if type(str) == "table" then
+        for k=1,#str do
+            local v = str[k]
+            str[k] = resolve(v) or v
+        end
+    elseif str and str ~= "" then
+        str = gsub(str,"([a-z]+):([^ \"\']*)",_resolve_)
+    end
+    return str
+end
+
+resolvers.resolve = resolve
+
+if os.uname then
+
+    for k, v in next, os.uname() do
+        if not prefixes[k] then
+            prefixes[k] = function() return v end
+        end
+    end
+
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-inp'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+resolvers.finders = resolvers.finders or { }
+resolvers.openers = resolvers.openers or { }
+resolvers.loaders = resolvers.loaders or { }
+
+resolvers.finders.notfound  = { nil }
+resolvers.openers.notfound  = { nil }
+resolvers.loaders.notfound  = { false, nil, 0 }
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-out'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+outputs = outputs or { }
+
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-con'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, lower, gsub = string.format, string.lower, string.gsub
+
+local trace_cache      = false  trackers.register("resolvers.cache",      function(v) trace_cache      = v end)
+local trace_containers = false  trackers.register("resolvers.containers", function(v) trace_containers = v end)
+local trace_storage    = false  trackers.register("resolvers.storage",    function(v) trace_storage    = v end)
+
+--[[ldx--
+<p>Once we found ourselves defining similar cache constructs
+several times, containers were introduced. Containers are used
+to collect tables in memory and reuse them when possible based
+on (unique) hashes (to be provided by the calling function).</p>
+
+<p>Caching to disk is disabled by default. Version numbers are
+stored in the saved table which makes it possible to change the
+table structures without bothering about the disk cache.</p>
+
+<p>Examples of usage can be found in the font related code.</p>
+--ldx]]--
+
+containers = containers or { }
+
+containers.usecache = true
+
+local function report(container,tag,name)
+    if trace_cache or trace_containers then
+        logs.report(format("%s cache",container.subcategory),"%s: %s",tag,name or 'invalid')
+    end
+end
+
+local allocated = { }
+
+-- tracing
+
+function containers.define(category, subcategory, version, enabled)
+    return function()
+        if category and subcategory then
+            local c = allocated[category]
+            if not c then
+                c  = { }
+                allocated[category] = c
+            end
+            local s = c[subcategory]
+            if not s then
+                s = {
+                    category = category,
+                    subcategory = subcategory,
+                    storage = { },
+                    enabled = enabled,
+                    version = version or 1.000,
+                    trace = false,
+                    path = caches and caches.setpath and caches.setpath(category,subcategory),
+                }
+                c[subcategory] = s
+            end
+            return s
+        else
+            return nil
+        end
+    end
+end
+
+function containers.is_usable(container, name)
+    return container.enabled and caches and caches.iswritable(container.path, name)
+end
+
+function containers.is_valid(container, name)
+    if name and name ~= "" then
+        local storage = container.storage[name]
+        return storage and storage.cache_version == container.version
+    else
+        return false
+    end
+end
+
+function containers.read(container,name)
+    if container.enabled and caches and not container.storage[name] and containers.usecache then
+        container.storage[name] = caches.loaddata(container.path,name)
+        if containers.is_valid(container,name) then
+            report(container,"loaded",name)
+        else
+            container.storage[name] = nil
+        end
+    end
+    if container.storage[name] then
+        report(container,"reusing",name)
+    end
+    return container.storage[name]
+end
+
+function containers.write(container, name, data)
+    if data then
+        data.cache_version = container.version
+        if container.enabled and caches then
+            local unique, shared = data.unique, data.shared
+            data.unique, data.shared = nil, nil
+            caches.savedata(container.path, name, data)
+            report(container,"saved",name)
+            data.unique, data.shared = unique, shared
+        end
+        report(container,"stored",name)
+        container.storage[name] = data
+    end
+    return data
+end
+
+function containers.content(container,name)
+    return container.storage[name]
+end
+
+function containers.cleanname(name)
+    return (gsub(lower(name),"[^%w%d]+","-"))
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-use'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, lower, gsub, find = string.format, string.lower, string.gsub, string.find
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+-- since we want to use the cache instead of the tree, we will now
+-- reimplement the saver.
+
+local save_data = resolvers.save_data
+local load_data = resolvers.load_data
+
+resolvers.cachepath = nil  -- public, for tracing
+resolvers.usecache  = true -- public, for tracing
+
+function resolvers.save_data(dataname)
+    save_data(dataname, function(cachename,dataname)
+        resolvers.usecache = not toboolean(resolvers.expansion("CACHEINTDS") or "false",true)
+        if resolvers.usecache then
+            resolvers.cachepath = resolvers.cachepath or caches.definepath("trees")
+            return file.join(resolvers.cachepath(),caches.hashed(cachename))
+        else
+            return file.join(cachename,dataname)
+        end
+    end)
+end
+
+function resolvers.load_data(pathname,dataname,filename)
+    load_data(pathname,dataname,filename,function(dataname,filename)
+        resolvers.usecache = not toboolean(resolvers.expansion("CACHEINTDS") or "false",true)
+        if resolvers.usecache then
+            resolvers.cachepath = resolvers.cachepath or caches.definepath("trees")
+            return file.join(resolvers.cachepath(),caches.hashed(pathname))
+        else
+            if not filename or (filename == "") then
+                filename = dataname
+            end
+            return file.join(pathname,filename)
+        end
+    end)
+end
+
+-- we will make a better format, maybe something xml or just text or lua
+
+resolvers.automounted = resolvers.automounted or { }
+
+function resolvers.automount(usecache)
+    local mountpaths = resolvers.clean_path_list(resolvers.expansion('TEXMFMOUNT'))
+    if (not mountpaths or #mountpaths == 0) and usecache then
+        mountpaths = { caches.setpath("mount") }
+    end
+    if mountpaths and #mountpaths > 0 then
+        statistics.starttiming(resolvers.instance)
+        for k=1,#mountpaths do
+            local root = mountpaths[k]
+            local f = io.open(root.."/url.tmi")
+            if f then
+                for line in f:lines() do
+                    if line then
+                        if find(line,"^[%%#%-]") then -- or %W
+                            -- skip
+                        elseif find(line,"^zip://") then
+                            if trace_locating then
+                                logs.report("fileio","mounting %s",line)
+                            end
+                            table.insert(resolvers.automounted,line)
+                            resolvers.usezipfile(line)
+                        end
+                    end
+                end
+                f:close()
+            end
+        end
+        statistics.stoptiming(resolvers.instance)
+    end
+end
+
+-- status info
+
+statistics.register("used config path", function() return caches.configpath()  end)
+statistics.register("used cache path",  function() return caches.temp() or "?" end)
+
+-- experiment (code will move)
+
+function statistics.save_fmt_status(texname,formatbanner,sourcefile) -- texname == formatname
+    local enginebanner = status.list().banner
+    if formatbanner and enginebanner and sourcefile then
+        local luvname = file.replacesuffix(texname,"luv")
+        local luvdata = {
+            enginebanner = enginebanner,
+            formatbanner = formatbanner,
+            sourcehash   = md5.hex(io.loaddata(resolvers.find_file(sourcefile)) or "unknown"),
+            sourcefile   = sourcefile,
+        }
+        io.savedata(luvname,table.serialize(luvdata,true))
+    end
+end
+
+function statistics.check_fmt_status(texname)
+    local enginebanner = status.list().banner
+    if enginebanner and texname then
+        local luvname = file.replacesuffix(texname,"luv")
+        if lfs.isfile(luvname) then
+            local luv = dofile(luvname)
+            if luv and luv.sourcefile then
+                local sourcehash = md5.hex(io.loaddata(resolvers.find_file(luv.sourcefile)) or "unknown")
+                local luvbanner = luv.enginebanner or "?"
+                if luvbanner ~= enginebanner then
+                    return string.format("engine mismatch (luv:%s <> bin:%s)",luvbanner,enginebanner)
+                end
+                local luvhash = luv.sourcehash or "?"
+                if luvhash ~= sourcehash then
+                    return string.format("source mismatch (luv:%s <> bin:%s)",luvhash,sourcehash)
+                end
+            else
+                return "invalid status file"
+            end
+        else
+            return "missing status file"
+        end
+    end
+    return true
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-zip'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local format, find, match = string.format, string.find, string.match
+local unpack = unpack or table.unpack
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+-- zip:///oeps.zip?name=bla/bla.tex
+-- zip:///oeps.zip?tree=tex/texmf-local
+-- zip:///texmf.zip?tree=/tex/texmf
+-- zip:///texmf.zip?tree=/tex/texmf-local
+-- zip:///texmf-mine.zip?tree=/tex/texmf-projects
+
+zip                 = zip or { }
+zip.archives        = zip.archives or { }
+zip.registeredfiles = zip.registeredfiles or { }
+
+local finders, openers, loaders = resolvers.finders, resolvers.openers, resolvers.loaders
+local locators, hashers, concatinators = resolvers.locators, resolvers.hashers, resolvers.concatinators
+
+local archives = zip.archives
+
+local function validzip(str) -- todo: use url splitter
+    if not find(str,"^zip://") then
+        return "zip:///" .. str
+    else
+        return str
+    end
+end
+
+function zip.openarchive(name)
+    if not name or name == "" then
+        return nil
+    else
+        local arch = archives[name]
+        if not arch then
+           local full = resolvers.find_file(name) or ""
+           arch = (full ~= "" and zip.open(full)) or false
+           archives[name] = arch
+        end
+       return arch
+    end
+end
+
+function zip.closearchive(name)
+    if not name or (name == "" and archives[name]) then
+        zip.close(archives[name])
+        archives[name] = nil
+    end
+end
+
+function locators.zip(specification) -- where is this used? startup zips (untested)
+    specification = resolvers.splitmethod(specification)
+    local zipfile = specification.path
+    local zfile = zip.openarchive(name) -- tricky, could be in to be initialized tree
+    if trace_locating then
+        if zfile then
+            logs.report("fileio","zip locator, archive '%s' found",specification.original)
+        else
+            logs.report("fileio","zip locator, archive '%s' not found",specification.original)
+        end
+    end
+end
+
+function hashers.zip(tag,name)
+    if trace_locating then
+        logs.report("fileio","loading zip file '%s' as '%s'",name,tag)
+    end
+    resolvers.usezipfile(format("%s?tree=%s",tag,name))
+end
+
+function concatinators.zip(tag,path,name)
+    if not path or path == "" then
+        return format('%s?name=%s',tag,name)
+    else
+        return format('%s?name=%s/%s',tag,path,name)
+    end
+end
+
+function resolvers.isreadable.zip(name)
+    return true
+end
+
+function finders.zip(specification,filetype)
+    specification = resolvers.splitmethod(specification)
+    if specification.path then
+        local q = url.query(specification.query)
+        if q.name then
+            local zfile = zip.openarchive(specification.path)
+            if zfile then
+                if trace_locating then
+                    logs.report("fileio","zip finder, archive '%s' found",specification.path)
+                end
+                local dfile = zfile:open(q.name)
+                if dfile then
+                    dfile = zfile:close()
+                    if trace_locating then
+                        logs.report("fileio","zip finder, file '%s' found",q.name)
+                    end
+                    return specification.original
+                elseif trace_locating then
+                    logs.report("fileio","zip finder, file '%s' not found",q.name)
+                end
+            elseif trace_locating then
+                logs.report("fileio","zip finder, unknown archive '%s'",specification.path)
+            end
+        end
+    end
+    if trace_locating then
+        logs.report("fileio","zip finder, '%s' not found",filename)
+    end
+    return unpack(finders.notfound)
+end
+
+function openers.zip(specification)
+    local zipspecification = resolvers.splitmethod(specification)
+    if zipspecification.path then
+        local q = url.query(zipspecification.query)
+        if q.name then
+            local zfile = zip.openarchive(zipspecification.path)
+            if zfile then
+                if trace_locating then
+                    logs.report("fileio","zip opener, archive '%s' opened",zipspecification.path)
+                end
+                local dfile = zfile:open(q.name)
+                if dfile then
+                    logs.show_open(specification)
+                    if trace_locating then
+                        logs.report("fileio","zip opener, file '%s' found",q.name)
+                    end
+                    return openers.text_opener(specification,dfile,'zip')
+                elseif trace_locating then
+                    logs.report("fileio","zip opener, file '%s' not found",q.name)
+                end
+            elseif trace_locating then
+                logs.report("fileio","zip opener, unknown archive '%s'",zipspecification.path)
+            end
+        end
+    end
+    if trace_locating then
+        logs.report("fileio","zip opener, '%s' not found",filename)
+    end
+    return unpack(openers.notfound)
+end
+
+function loaders.zip(specification)
+    specification = resolvers.splitmethod(specification)
+    if specification.path then
+        local q = url.query(specification.query)
+        if q.name then
+            local zfile = zip.openarchive(specification.path)
+            if zfile then
+                if trace_locating then
+                    logs.report("fileio","zip loader, archive '%s' opened",specification.path)
+                end
+                local dfile = zfile:open(q.name)
+                if dfile then
+                    logs.show_load(filename)
+                    if trace_locating then
+                        logs.report("fileio","zip loader, file '%s' loaded",filename)
+                    end
+                    local s = dfile:read("*all")
+                    dfile:close()
+                    return true, s, #s
+                elseif trace_locating then
+                    logs.report("fileio","zip loader, file '%s' not found",q.name)
+                end
+            elseif trace_locating then
+                logs.report("fileio","zip loader, unknown archive '%s'",specification.path)
+            end
+        end
+    end
+    if trace_locating then
+        logs.report("fileio","zip loader, '%s' not found",filename)
+    end
+    return unpack(openers.notfound)
+end
+
+-- zip:///somefile.zip
+-- zip:///somefile.zip?tree=texmf-local -> mount
+
+function resolvers.usezipfile(zipname)
+    zipname = validzip(zipname)
+    local specification = resolvers.splitmethod(zipname)
+    local zipfile = specification.path
+    if zipfile and not zip.registeredfiles[zipname] then
+        local tree = url.query(specification.query).tree or ""
+        local z = zip.openarchive(zipfile)
+        if z then
+            local instance = resolvers.instance
+            if trace_locating then
+                logs.report("fileio","zip registering, registering archive '%s'",zipname)
+            end
+            statistics.starttiming(instance)
+            resolvers.prepend_hash('zip',zipname,zipfile)
+            resolvers.extend_texmf_var(zipname) -- resets hashes too
+            zip.registeredfiles[zipname] = z
+            instance.files[zipname] = resolvers.register_zip_file(z,tree or "")
+            statistics.stoptiming(instance)
+        elseif trace_locating then
+            logs.report("fileio","zip registering, unknown archive '%s'",zipname)
+        end
+    elseif trace_locating then
+        logs.report("fileio","zip registering, '%s' not found",zipname)
+    end
+end
+
+function resolvers.register_zip_file(z,tree)
+    local files, filter = { }, ""
+    if tree == "" then
+        filter = "^(.+)/(.-)$"
+    else
+        filter = format("^%s/(.+)/(.-)$",tree)
+    end
+    if trace_locating then
+        logs.report("fileio","zip registering, using filter '%s'",filter)
+    end
+    local register, n = resolvers.register_file, 0
+    for i in z:files() do
+        local path, name = match(i.filename,filter)
+        if path then
+            if name and name ~= '' then
+                register(files, name, path)
+                n = n + 1
+            else
+                -- directory
+            end
+        else
+            register(files, i.filename, '')
+            n = n + 1
+        end
+    end
+    logs.report("fileio","zip registering, %s files registered",n)
+    return files
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-crl'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local gsub = string.gsub
+
+curl = curl or { }
+
+curl.cached    = { }
+curl.cachepath = caches.definepath("curl")
+
+local finders, openers, loaders = resolvers.finders, resolvers.openers, resolvers.loaders
+
+function curl.fetch(protocol, name)
+    local cachename = curl.cachepath() .. "/" .. gsub(name,"[^%a%d%.]+","-")
+--  cachename = gsub(cachename,"[\\/]", io.fileseparator)
+    cachename = gsub(cachename,"[\\]", "/") -- cleanup
+    if not curl.cached[name] then
+        if not io.exists(cachename) then
+            curl.cached[name] = cachename
+            local command = "curl --silent --create-dirs --output " .. cachename .. " " .. name -- no protocol .. "://"
+            os.spawn(command)
+        end
+        if io.exists(cachename) then
+            curl.cached[name] = cachename
+        else
+            curl.cached[name] = ""
+        end
+    end
+    return curl.cached[name]
+end
+
+function finders.curl(protocol,filename)
+    local foundname = curl.fetch(protocol, filename)
+    return finders.generic(protocol,foundname,filetype)
+end
+
+function openers.curl(protocol,filename)
+    return openers.generic(protocol,filename)
+end
+
+function loaders.curl(protocol,filename)
+    return loaders.generic(protocol,filename)
+end
+
+-- todo: metamethod
+
+function curl.install(protocol)
+    finders[protocol] = function (filename,filetype) return finders.curl(protocol,filename) end
+    openers[protocol] = function (filename)          return openers.curl(protocol,filename) end
+    loaders[protocol] = function (filename)          return loaders.curl(protocol,filename) end
+end
+
+curl.install('http')
+curl.install('https')
+curl.install('ftp')
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-lua'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- some loading stuff ... we might move this one to slot 2 depending
+-- on the developments (the loaders must not trigger kpse); we could
+-- of course use a more extensive lib path spec
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+local gsub, insert = string.gsub, table.insert
+local unpack = unpack or table.unpack
+
+local  libformats = { 'luatexlibs', 'tex', 'texmfscripts', 'othertextfiles' } -- 'luainputs'
+local clibformats = { 'lib' }
+
+local _path_, libpaths, _cpath_, clibpaths
+
+function package.libpaths()
+    if not _path_ or package.path ~= _path_ then
+        _path_ = package.path
+        libpaths = file.split_path(_path_,";")
+    end
+    return libpaths
+end
+
+function package.clibpaths()
+    if not _cpath_ or package.cpath ~= _cpath_ then
+        _cpath_ = package.cpath
+        clibpaths = file.split_path(_cpath_,";")
+    end
+    return clibpaths
+end
+
+local function thepath(...)
+    local t = { ... } t[#t+1] = "?.lua"
+    local path = file.join(unpack(t))
+    if trace_locating then
+        logs.report("fileio","! appending '%s' to 'package.path'",path)
+    end
+    return path
+end
+
+local p_libpaths, a_libpaths = { }, { }
+
+function package.append_libpath(...)
+    insert(a_libpath,thepath(...))
+end
+
+function package.prepend_libpath(...)
+    insert(p_libpaths,1,thepath(...))
+end
+
+-- beware, we need to return a loadfile result !
+
+local function loaded(libpaths,name,simple)
+    for i=1,#libpaths do -- package.path, might become option
+        local libpath = libpaths[i]
+        local resolved = gsub(libpath,"%?",simple)
+        if trace_locating then -- more detail
+            logs.report("fileio","! checking for '%s' on 'package.path': '%s' => '%s'",simple,libpath,resolved)
+        end
+        if resolvers.isreadable.file(resolved) then
+            if trace_locating then
+                logs.report("fileio","! lib '%s' located via 'package.path': '%s'",name,resolved)
+            end
+            return loadfile(resolved)
+        end
+    end
+end
+
+
+package.loaders[2] = function(name) -- was [#package.loaders+1]
+    if trace_locating then -- mode detail
+        logs.report("fileio","! locating '%s'",name)
+    end
+    for i=1,#libformats do
+        local format = libformats[i]
+        local resolved = resolvers.find_file(name,format) or ""
+        if trace_locating then -- mode detail
+            logs.report("fileio","! checking for '%s' using 'libformat path': '%s'",name,format)
+        end
+        if resolved ~= "" then
+            if trace_locating then
+                logs.report("fileio","! lib '%s' located via environment: '%s'",name,resolved)
+            end
+            return loadfile(resolved)
+        end
+    end
+    -- libpaths
+    local libpaths, clibpaths = package.libpaths(), package.clibpaths()
+    local simple = gsub(name,"%.lua$","")
+    local simple = gsub(simple,"%.","/")
+    local resolved = loaded(p_libpaths,name,simple) or loaded(libpaths,name,simple) or loaded(a_libpaths,name,simple)
+    if resolved then
+        return resolved
+    end
+    --
+    local libname = file.addsuffix(simple,os.libsuffix)
+    for i=1,#clibformats do
+        -- better have a dedicated loop
+        local format = clibformats[i]
+        local paths = resolvers.expanded_path_list_from_var(format)
+        for p=1,#paths do
+            local path = paths[p]
+            local resolved = file.join(path,libname)
+            if trace_locating then -- mode detail
+                logs.report("fileio","! checking for '%s' using 'clibformat path': '%s'",libname,path)
+            end
+            if resolvers.isreadable.file(resolved) then
+                if trace_locating then
+                    logs.report("fileio","! lib '%s' located via 'clibformat': '%s'",libname,resolved)
+                end
+                return package.loadlib(resolved,name)
+            end
+        end
+    end
+    for i=1,#clibpaths do -- package.path, might become option
+        local libpath = clibpaths[i]
+        local resolved = gsub(libpath,"?",simple)
+        if trace_locating then -- more detail
+            logs.report("fileio","! checking for '%s' on 'package.cpath': '%s'",simple,libpath)
+        end
+        if resolvers.isreadable.file(resolved) then
+            if trace_locating then
+                logs.report("fileio","! lib '%s' located via 'package.cpath': '%s'",name,resolved)
+            end
+            return package.loadlib(resolved,name)
+        end
+    end
+    -- just in case the distribution is messed up
+    if trace_loading then -- more detail
+        logs.report("fileio","! checking for '%s' using 'luatexlibs': '%s'",name)
+    end
+    local resolved = resolvers.find_file(file.basename(name),'luatexlibs') or ""
+    if resolved ~= "" then
+        if trace_locating then
+            logs.report("fileio","! lib '%s' located by basename via environment: '%s'",name,resolved)
+        end
+        return loadfile(resolved)
+    end
+    if trace_locating then
+        logs.report("fileio",'? unable to locate lib: %s',name)
+    end
+--  return "unable to locate " .. name
+end
+
+resolvers.loadlualib = require
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['luat-kps'] = {
+    version   = 1.001,
+    comment   = "companion to luatools.lua",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+--[[ldx--
+<p>This file is used when we want the input handlers to behave like
+<type>kpsewhich</type>. What to do with the following:</p>
+
+<typing>
+{$SELFAUTOLOC,$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,}/web2c}
+$SELFAUTOLOC    : /usr/tex/bin/platform
+$SELFAUTODIR    : /usr/tex/bin
+$SELFAUTOPARENT : /usr/tex
+</typing>
+
+<p>How about just forgetting about them?</p>
+--ldx]]--
+
+local suffixes = resolvers.suffixes
+local formats  = resolvers.formats
+
+suffixes['gf']                       = { '<resolution>gf' }
+suffixes['pk']                       = { '<resolution>pk' }
+suffixes['base']                     = { 'base' }
+suffixes['bib']                      = { 'bib' }
+suffixes['bst']                      = { 'bst' }
+suffixes['cnf']                      = { 'cnf' }
+suffixes['mem']                      = { 'mem' }
+suffixes['mf']                       = { 'mf' }
+suffixes['mfpool']                   = { 'pool' }
+suffixes['mft']                      = { 'mft' }
+suffixes['mppool']                   = { 'pool' }
+suffixes['graphic/figure']           = { 'eps', 'epsi' }
+suffixes['texpool']                  = { 'pool' }
+suffixes['PostScript header']        = { 'pro' }
+suffixes['ist']                      = { 'ist' }
+suffixes['web']                      = { 'web', 'ch' }
+suffixes['cweb']                     = { 'w', 'web', 'ch' }
+suffixes['cmap files']               = { 'cmap' }
+suffixes['lig files']                = { 'lig' }
+suffixes['bitmap font']              = { }
+suffixes['MetaPost support']         = { }
+suffixes['TeX system documentation'] = { }
+suffixes['TeX system sources']       = { }
+suffixes['dvips config']             = { }
+suffixes['type42 fonts']             = { }
+suffixes['web2c files']              = { }
+suffixes['other text files']         = { }
+suffixes['other binary files']       = { }
+suffixes['opentype fonts']           = { 'otf' }
+
+suffixes['fmt']                      = { 'fmt' }
+suffixes['texmfscripts']             = { 'rb','lua','py','pl' }
+
+suffixes['pdftex config']            = { }
+suffixes['Troff fonts']              = { }
+
+suffixes['ls-R']                     = { }
+
+--[[ldx--
+<p>If you wondered abou tsome of the previous mappings, how about
+the next bunch:</p>
+--ldx]]--
+
+formats['bib']                      = ''
+formats['bst']                      = ''
+formats['mft']                      = ''
+formats['ist']                      = ''
+formats['web']                      = ''
+formats['cweb']                     = ''
+formats['MetaPost support']         = ''
+formats['TeX system documentation'] = ''
+formats['TeX system sources']       = ''
+formats['Troff fonts']              = ''
+formats['dvips config']             = ''
+formats['graphic/figure']           = ''
+formats['ls-R']                     = ''
+formats['other text files']         = ''
+formats['other binary files']       = ''
+
+formats['gf']                       = ''
+formats['pk']                       = ''
+formats['base']                     = 'MFBASES'
+formats['cnf']                      = ''
+formats['mem']                      = 'MPMEMS'
+formats['mf']                       = 'MFINPUTS'
+formats['mfpool']                   = 'MFPOOL'
+formats['mppool']                   = 'MPPOOL'
+formats['texpool']                  = 'TEXPOOL'
+formats['PostScript header']        = 'TEXPSHEADERS'
+formats['cmap files']               = 'CMAPFONTS'
+formats['type42 fonts']             = 'T42FONTS'
+formats['web2c files']              = 'WEB2C'
+formats['pdftex config']            = 'PDFTEXCONFIG'
+formats['texmfscripts']             = 'TEXMFSCRIPTS'
+formats['bitmap font']              = ''
+formats['lig files']                = 'LIGFONTS'
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-aux'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local find = string.find
+
+local trace_locating = false  trackers.register("resolvers.locating", function(v) trace_locating = v end)
+
+function resolvers.update_script(oldname,newname) -- oldname -> own.name, not per se a suffix
+    local scriptpath = "scripts/context/lua"
+    newname = file.addsuffix(newname,"lua")
+    local oldscript = resolvers.clean_path(oldname)
+    if trace_locating then
+        logs.report("fileio","to be replaced old script %s", oldscript)
+    end
+    local newscripts = resolvers.find_files(newname) or { }
+    if #newscripts == 0 then
+        if trace_locating then
+            logs.report("fileio","unable to locate new script")
+        end
+    else
+        for i=1,#newscripts do
+            local newscript = resolvers.clean_path(newscripts[i])
+            if trace_locating then
+                logs.report("fileio","checking new script %s", newscript)
+            end
+            if oldscript == newscript then
+                if trace_locating then
+                    logs.report("fileio","old and new script are the same")
+                end
+            elseif not find(newscript,scriptpath) then
+                if trace_locating then
+                    logs.report("fileio","new script should come from %s",scriptpath)
+                end
+            elseif not (find(oldscript,file.removesuffix(newname).."$") or find(oldscript,newname.."$")) then
+                if trace_locating then
+                    logs.report("fileio","invalid new script name")
+                end
+            else
+                local newdata = io.loaddata(newscript)
+                if newdata then
+                    if trace_locating then
+                        logs.report("fileio","old script content replaced by new content")
+                    end
+                    io.savedata(oldscript,newdata)
+                    break
+                elseif trace_locating then
+                    logs.report("fileio","unable to load new script")
+                end
+            end
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['data-tmf'] = {
+    version   = 1.001,
+    comment   = "companion to luat-lib.mkiv",
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+local find, gsub, match = string.find, string.gsub, string.match
+local getenv, setenv = os.getenv, os.setenv
+
+-- loads *.tmf files in minimal tree roots (to be optimized and documented)
+
+function resolvers.check_environment(tree)
+    logs.simpleline()
+    setenv('TMP', getenv('TMP') or getenv('TEMP') or getenv('TMPDIR') or getenv('HOME'))
+    setenv('TEXOS', getenv('TEXOS') or ("texmf-" .. os.platform))
+    setenv('TEXPATH', gsub(tree or "tex","\/+$",''))
+    setenv('TEXMFOS', getenv('TEXPATH') .. "/" .. getenv('TEXOS'))
+    logs.simpleline()
+    logs.simple("preset : TEXPATH => %s", getenv('TEXPATH'))
+    logs.simple("preset : TEXOS   => %s", getenv('TEXOS'))
+    logs.simple("preset : TEXMFOS => %s", getenv('TEXMFOS'))
+    logs.simple("preset : TMP     => %s", getenv('TMP'))
+    logs.simple('')
+end
+
+function resolvers.load_environment(name) -- todo: key=value as well as lua
+    local f = io.open(name)
+    if f then
+        for line in f:lines() do
+            if find(line,"^[%%%#]") then
+                -- skip comment
+            else
+                local key, how, value = match(line,"^(.-)%s*([<=>%?]+)%s*(.*)%s*$")
+                if how then
+                    value = gsub(value,"%%(.-)%%", function(v) return getenv(v) or "" end)
+                        if how == "=" or how == "<<" then
+                            setenv(key,value)
+                    elseif how == "?" or how == "??" then
+                            setenv(key,getenv(key) or value)
+                    elseif how == "<" or how == "+=" then
+                        if getenv(key) then
+                            setenv(key,getenv(key) .. io.fileseparator .. value)
+                        else
+                            setenv(key,value)
+                        end
+                    elseif how == ">" or how == "=+" then
+                        if getenv(key) then
+                            setenv(key,value .. io.pathseparator .. getenv(key))
+                        else
+                            setenv(key,value)
+                        end
+                    end
+                end
+            end
+        end
+        f:close()
+    end
+end
+
+function resolvers.load_tree(tree)
+    if tree and tree ~= "" then
+        local setuptex = 'setuptex.tmf'
+        if lfs.attributes(tree, "mode") == "directory" then -- check if not nil
+            setuptex = tree .. "/" .. setuptex
+        else
+            setuptex = tree
+        end
+        if io.exists(setuptex) then
+            resolvers.check_environment(tree)
+            resolvers.load_environment(setuptex)
+        end
+    end
+end
+
+
+end -- of closure
+
+do -- create closure to overcome 200 locals limit
+
+if not modules then modules = { } end modules ['luat-sta'] = {
+    version   = 1.001,
+    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
+    copyright = "PRAGMA ADE / ConTeXt Development Team",
+    license   = "see context related readme files"
+}
+
+-- this code is used in the updater
+
+local gmatch, match = string.gmatch, string.match
+local type = type
+
+states          = states          or { }
+states.data     = states.data     or { }
+states.hash     = states.hash     or { }
+states.tag      = states.tag      or ""
+states.filename = states.filename or ""
+
+function states.save(filename,tag)
+    tag = tag or states.tag
+    filename = file.addsuffix(filename or states.filename,'lus')
+    io.savedata(filename,
+        "-- generator : luat-sta.lua\n" ..
+        "-- state tag : " .. tag .. "\n\n" ..
+        table.serialize(states.data[tag or states.tag] or {},true)
+    )
+end
+
+function states.load(filename,tag)
+    states.filename = filename
+    states.tag = tag or "whatever"
+    states.filename = file.addsuffix(states.filename,'lus')
+    states.data[states.tag], states.hash[states.tag] = (io.exists(filename) and dofile(filename)) or { }, { }
+end
+
+function states.set_by_tag(tag,key,value,default,persistent)
+    local d, h = states.data[tag], states.hash[tag]
+    if d then
+        if type(d) == "table" then
+            local dkey, hkey = key, key
+            local pre, post = match(key,"(.+)%.([^%.]+)$")
+            if pre and post then
+                for k in gmatch(pre,"[^%.]+") do
+                    local dk = d[k]
+                    if not dk then
+                        dk = { }
+                        d[k] = dk
+                    elseif type(dk) == "string" then
+                        -- invalid table, unable to upgrade structure
+                        -- hope for the best or delete the state file
+                        break
+                    end
+                    d = dk
+                end
+                dkey, hkey = post, key
+            end
+            if type(value) == nil then
+                value = value or default
+            elseif persistent then
+                value = value or d[dkey] or default
+            else
+                value = value or default
+            end
+            d[dkey], h[hkey] = value, value
+        elseif type(d) == "string" then
+            -- weird
+            states.data[tag], states.hash[tag] = value, value
+        end
+    end
+end
+
+function states.get_by_tag(tag,key,default)
+    local h = states.hash[tag]
+    if h and h[key] then
+        return h[key]
+    else
+        local d = states.data[tag]
+        if d then
+            for k in gmatch(key,"[^%.]+") do
+                local dk = d[k]
+                if dk then
+                    d = dk
+                else
+                    return default
+                end
+            end
+            return d or default
+        end
+    end
+end
+
+function states.set(key,value,default,persistent)
+    states.set_by_tag(states.tag,key,value,default,persistent)
+end
+
+function states.get(key,default)
+    return states.get_by_tag(states.tag,key,default)
+end
+
+--~ states.data.update = {
+--~ 	["version"] = {
+--~ 		["major"] = 0,
+--~ 		["minor"] = 1,
+--~ 	},
+--~ 	["rsync"] = {
+--~ 		["server"]     = "contextgarden.net",
+--~ 		["module"]     = "minimals",
+--~ 		["repository"] = "current",
+--~ 		["flags"]      = "-rpztlv --stats",
+--~ 	},
+--~ 	["tasks"] = {
+--~ 		["update"] = true,
+--~ 		["make"]   = true,
+--~         ["delete"] = false,
+--~ 	},
+--~ 	["platform"] = {
+--~ 		["host"]  = true,
+--~ 		["other"] = {
+--~ 			["mswin"]     = false,
+--~ 			["linux"]     = false,
+--~ 			["linux-64"]  = false,
+--~ 			["osx-intel"] = false,
+--~ 			["osx-ppc"]   = false,
+--~ 			["sun"]       = false,
+--~ 		},
+--~ 	},
+--~ 	["context"] = {
+--~ 		["available"] = {"current", "beta", "alpha", "experimental"},
+--~ 		["selected"]  = "current",
+--~ 	},
+--~ 	["formats"] = {
+--~ 		["cont-en"] = true,
+--~ 		["cont-nl"] = true,
+--~ 		["cont-de"] = false,
+--~ 		["cont-cz"] = false,
+--~ 		["cont-fr"] = false,
+--~ 		["cont-ro"] = false,
+--~ 	},
+--~ 	["engine"] = {
+--~ 		["pdftex"] = {
+--~ 			["install"] = true,
+--~ 			["formats"] = {
+--~ 				["pdftex"] = true,
+--~ 			},
+--~ 		},
+--~ 		["luatex"] = {
+--~ 			["install"] = true,
+--~ 			["formats"] = {
+--~ 			},
+--~ 		},
+--~ 		["xetex"] = {
+--~ 			["install"] = true,
+--~ 			["formats"] = {
+--~ 				["xetex"] = false,
+--~ 			},
+--~ 		},
+--~ 		["metapost"] = {
+--~ 			["install"] = true,
+--~ 			["formats"] = {
+--~ 				["mpost"] = true,
+--~ 				["metafun"] = true,
+--~ 			},
+--~ 		},
+--~ 	},
+--~ 	["fonts"] = {
+--~ 	},
+--~ 	["doc"] = {
+--~ 	},
+--~ 	["modules"] = {
+--~ 		["f-urwgaramond"] = false,
+--~ 		["f-urwgothic"] = false,
+--~ 		["t-bnf"] = false,
+--~ 		["t-chromato"] = false,
+--~ 		["t-cmscbf"] = false,
+--~ 		["t-cmttbf"] = false,
+--~ 		["t-construction-plan"] = false,
+--~ 		["t-degrade"] = false,
+--~ 		["t-french"] = false,
+--~ 		["t-lettrine"] = false,
+--~ 		["t-lilypond"] = false,
+--~ 		["t-mathsets"] = false,
+--~ 		["t-tikz"] = false,
+--~ 		["t-typearea"] = false,
+--~ 		["t-vim"] = false,
+--~ 	},
+--~ }
+
+--~ states.save("teststate", "update")
+--~ states.load("teststate", "update")
+
+--~ print(states.get_by_tag("update","rsync.server","unknown"))
+--~ states.set_by_tag("update","rsync.server","oeps")
+--~ print(states.get_by_tag("update","rsync.server","unknown"))
+--~ states.save("teststate", "update")
+--~ states.load("teststate", "update")
+--~ print(states.get_by_tag("update","rsync.server","unknown"))
+
+
+end -- of closure
+-- end library merge
+
+own = { } -- not local
+
+own.libs = { -- todo: check which ones are really needed
+    'l-string.lua',
+    'l-lpeg.lua',
+    'l-table.lua',
+    'l-io.lua',
+    'l-number.lua',
+    'l-set.lua',
+    'l-os.lua',
+    'l-file.lua',
+    'l-md5.lua',
+    'l-url.lua',
+    'l-dir.lua',
+    'l-boolean.lua',
+    'l-math.lua',
+--  'l-unicode.lua',
+--  'l-tex.lua',
+    'l-utils.lua',
+    'l-aux.lua',
+--  'l-xml.lua',
+    'trac-tra.lua',
+    'lxml-tab.lua',
+    'lxml-lpt.lua',
+--  'lxml-ent.lua',
+    'lxml-mis.lua',
+    'lxml-aux.lua',
+    'lxml-xml.lua',
+    'luat-env.lua',
+    'trac-inf.lua',
+    'trac-log.lua',
+    'data-res.lua',
+    'data-tmp.lua',
+    'data-pre.lua',
+    'data-inp.lua',
+    'data-out.lua',
+    'data-con.lua',
+    'data-use.lua',
+--  'data-tex.lua',
+--  'data-bin.lua',
+    'data-zip.lua',
+    'data-crl.lua',
+    'data-lua.lua',
+    'data-kps.lua', -- so that we can replace kpsewhich
+    'data-aux.lua', -- updater
+    'data-tmf.lua', -- tree files
+    -- needed ?
+    'luat-sta.lua', -- states
+}
+
+-- We need this hack till luatex is fixed.
+--
+-- for k,v in pairs(arg) do print(k,v) end
+
+if arg and (arg[0] == 'luatex' or arg[0] == 'luatex.exe') and arg[1] == "--luaonly" then
+    arg[-1]=arg[0] arg[0]=arg[2] for k=3,#arg do arg[k-2]=arg[k] end arg[#arg]=nil arg[#arg]=nil
+end
+
+-- End of hack.
+
+own.name = (environment and environment.ownname) or arg[0]  or 'luatools.lua'
+
+
+own.path = string.match(own.name,"^(.+)[\\/].-$") or "."
+own.list = { '.' }
+if own.path ~= '.' then
+    table.insert(own.list,own.path)
+end
+table.insert(own.list,own.path.."/../../../tex/context/base")
+table.insert(own.list,own.path.."/mtx")
+table.insert(own.list,own.path.."/../sources")
+
+local function locate_libs()
+    for _, lib in pairs(own.libs) do
+        for _, pth in pairs(own.list) do
+            local filename = string.gsub(pth .. "/" .. lib,"\\","/")
+            local codeblob = loadfile(filename)
+            if codeblob then
+                codeblob()
+                own.list = { pth } -- speed up te search
+                break
+            end
+        end
+    end
+end
+
+if not resolvers then
+    locate_libs()
+end
+
+if not resolvers then
+    print("")
+    print("Mtxrun is unable to start up due to lack of libraries. You may")
+    print("try to run 'lua mtxrun.lua --selfmerge' in the path where this")
+    print("script is located (normally under ..../scripts/context/lua) which")
+    print("will make this script library independent.")
+    os.exit()
+end
+
+logs.setprogram('MTXrun',"TDS Runner Tool 1.24",environment.arguments["verbose"] or false)
+
+local instance = resolvers.reset()
+
+local trackspec = environment.argument("trackers") or environment.argument("track")
+
+if trackspec then
+    trackers.enable(trackspec)
+end
+
+runners  = runners  or { } -- global
+messages = messages or { }
+
+messages.help = [[
+--script              run an mtx script (lua prefered method) (--noquotes), no script gives list
+--execute             run a script or program (texmfstart method) (--noquotes)
+--resolve             resolve prefixed arguments
+--ctxlua              run internally (using preloaded libs)
+--internal            run script using built in libraries (same as --ctxlua)
+--locate              locate given filename
+
+--autotree            use texmf tree cf. env 'texmfstart_tree' or 'texmfstarttree'
+--tree=pathtotree     use given texmf tree (default file: 'setuptex.tmf')
+--environment=name    use given (tmf) environment file
+--path=runpath        go to given path before execution
+--ifchanged=filename  only execute when given file has changed (md checksum)
+--iftouched=old,new   only execute when given file has changed (time stamp)
+
+--make                create stubs for (context related) scripts
+--remove              remove stubs (context related) scripts
+--stubpath=binpath    paths where stubs wil be written
+--windows             create windows (mswin) stubs
+--unix                create unix (linux) stubs
+
+--verbose             give a bit more info
+--trackers=list       enable given trackers
+--engine=str          target engine
+--progname=str        format or backend
+
+--edit                launch editor with found file
+--launch (--all)      launch files like manuals, assumes os support
+
+--timedrun            run a script an time its run
+--autogenerate        regenerate databases if needed (handy when used to run context in an editor)
+
+--usekpse             use kpse as fallback (when no mkiv and cache installed, often slower)
+--forcekpse           force using kpse (handy when no mkiv and cache installed but less functionality)
+
+--prefixes            show supported prefixes
+]]
+
+runners.applications = {
+    ["lua"] = "luatex --luaonly",
+    ["luc"] = "luatex --luaonly",
+    ["pl"] = "perl",
+    ["py"] = "python",
+    ["rb"] = "ruby",
+}
+
+runners.suffixes = {
+    'rb', 'lua', 'py', 'pl'
+}
+
+runners.registered = {
+    texexec      = { 'texexec.rb',      false },  -- context mkii runner (only tool not to be luafied)
+    texutil      = { 'texutil.rb',      true  },  -- old perl based index sorter for mkii (old versions need it)
+    texfont      = { 'texfont.pl',      true  },  -- perl script that makes mkii font metric files
+    texfind      = { 'texfind.pl',      false },  -- perltk based tex searching tool, mostly used at pragma
+    texshow      = { 'texshow.pl',      false },  -- perltk based context help system, will be luafied
+ -- texwork      = { 'texwork.pl',      false },  -- perltk based editing environment, only used at pragma
+    makempy      = { 'makempy.pl',      true  },
+    mptopdf      = { 'mptopdf.pl',      true  },
+    pstopdf      = { 'pstopdf.rb',      true  },  -- converts ps (and some more) images, does some cleaning (replaced)
+--  examplex     = { 'examplex.rb',     false },
+    concheck     = { 'concheck.rb',     false },
+    runtools     = { 'runtools.rb',     true  },
+    textools     = { 'textools.rb',     true  },
+    tmftools     = { 'tmftools.rb',     true  },
+    ctxtools     = { 'ctxtools.rb',     true  },
+    rlxtools     = { 'rlxtools.rb',     true  },
+    pdftools     = { 'pdftools.rb',     true  },
+    mpstools     = { 'mpstools.rb',     true  },
+--  exatools     = { 'exatools.rb',     true  },
+    xmltools     = { 'xmltools.rb',     true  },
+--  luatools     = { 'luatools.lua',    true  },
+    mtxtools     = { 'mtxtools.rb',     true  },
+    pdftrimwhite = { 'pdftrimwhite.pl', false }
+}
+
+runners.launchers = {
+    windows = { },
+    unix = { }
+}
+
+-- like runners.libpath("framework"): looks on script's subpath
+
+function runners.libpath(...)
+    package.prepend_libpath(file.dirname(environment.ownscript),...)
+    package.prepend_libpath(file.dirname(environment.ownname)  ,...)
+end
+
+function runners.prepare()
+    local checkname = environment.argument("ifchanged")
+    if checkname and checkname ~= "" then
+        local oldchecksum = file.loadchecksum(checkname)
+        local newchecksum = file.checksum(checkname)
+        if oldchecksum == newchecksum then
+            logs.simple("file '%s' is unchanged",checkname)
+            return "skip"
+        else
+            logs.simple("file '%s' is changed, processing started",checkname)
+        end
+        file.savechecksum(checkname)
+    end
+    local oldname, newname = string.split(environment.argument("iftouched") or "", ",")
+    if oldname and newname and oldname ~= "" and newname ~= "" then
+        if not file.needs_updating(oldname,newname) then
+            logs.simple("file '%s' and '%s' have same age",oldname,newname)
+            return "skip"
+        else
+            logs.simple("file '%s' is older than '%s'",oldname,newname)
+        end
+    end
+    local tree = environment.argument('tree') or ""
+    if environment.argument('autotree') then
+        tree = os.getenv('TEXMFSTART_TREE') or os.getenv('TEXMFSTARTTREE') or tree
+    end
+    if tree and tree ~= "" then
+        resolvers.load_tree(tree)
+    end
+    local env = environment.argument('environment') or ""
+    if env and env ~= "" then
+        for _,e in pairs(string.split(env)) do
+            -- maybe force suffix when not given
+            resolvers.load_tree(e)
+        end
+    end
+    local runpath = environment.argument("path")
+    if runpath and not lfs.chdir(runpath) then
+        logs.simple("unable to change to path '%s'",runpath)
+        return "error"
+    end
+    return "run"
+end
+
+function runners.execute_script(fullname,internal,nosplit)
+    local noquote = environment.argument("noquotes")
+    if fullname and fullname ~= "" then
+        local state = runners.prepare()
+        if state == 'error' then
+            return false
+        elseif state == 'skip' then
+            return true
+        elseif state == "run" then
+            instance.progname = environment.argument("progname") or instance.progname
+            instance.format   = environment.argument("format")   or instance.format
+            local path, name, suffix, result = file.dirname(fullname), file.basename(fullname), file.extname(fullname), ""
+            if path ~= "" then
+                result = fullname
+            elseif name then
+                name = name:gsub("^int[%a]*:",function()
+                    internal = true
+                    return ""
+                end )
+                name = name:gsub("^script:","")
+                if suffix == "" and runners.registered[name] and runners.registered[name][1] then
+                    name = runners.registered[name][1]
+                    suffix = file.extname(name)
+                end
+                if suffix == "" then
+                    -- loop over known suffixes
+                    for _,s in pairs(runners.suffixes) do
+                        result = resolvers.find_file(name .. "." .. s, 'texmfscripts')
+                        if result ~= "" then
+                            break
+                        end
+                    end
+                elseif runners.applications[suffix] then
+                    result = resolvers.find_file(name, 'texmfscripts')
+                else
+                    -- maybe look on path
+                    result = resolvers.find_file(name, 'other text files')
+                end
+            end
+            if result and result ~= "" then
+                if not no_split then
+                    local before, after = environment.split_arguments(fullname) -- already done
+                    environment.arguments_before, environment.arguments_after = before, after
+                end
+                if internal then
+                    arg = { } for _,v in pairs(environment.arguments_after) do arg[#arg+1] = v end
+                    environment.ownscript = result
+                    dofile(result)
+                else
+                    local binary = runners.applications[file.extname(result)]
+                    if binary and binary ~= "" then
+                        result = binary .. " " .. result
+                    end
+                    local command = result .. " " .. environment.reconstruct_commandline(environment.arguments_after,noquote)
+                    if logs.verbose then
+                        logs.simpleline()
+                        logs.simple("executing: %s",command)
+                        logs.simpleline()
+                        logs.simpleline()
+                        io.flush()
+                    end
+                    -- no os.exec because otherwise we get the wrong return value
+                    local code = os.execute(command) -- maybe spawn
+                    if code == 0 then
+                        return true
+                    else
+                        if binary then
+                            binary = file.addsuffix(binary,os.binsuffix)
+                            for p in string.gmatch(os.getenv("PATH"),"[^"..io.pathseparator.."]+") do
+                                if lfs.isfile(file.join(p,binary)) then
+                                    return false
+                                end
+                            end
+                            logs.simpleline()
+                            logs.simple("This script needs '%s' which seems not to be installed.",binary)
+                            logs.simpleline()
+                        end
+                        return false
+                    end
+                end
+            end
+        end
+    end
+    return false
+end
+
+function runners.execute_program(fullname)
+    local noquote = environment.argument("noquotes")
+    if fullname and fullname ~= "" then
+        local state = runners.prepare()
+        if state == 'error' then
+            return false
+        elseif state == 'skip' then
+            return true
+        elseif state == "run" then
+            local before, after = environment.split_arguments(fullname)
+            environment.initialize_arguments(after)
+            fullname = fullname:gsub("^bin:","")
+            local command = fullname .. " " .. (environment.reconstruct_commandline(after or "",noquote) or "")
+            logs.simpleline()
+            logs.simple("executing: %s",command)
+            logs.simpleline()
+            logs.simpleline()
+            io.flush()
+            local code = os.exec(command) -- (fullname,unpack(after)) does not work / maybe spawn
+            return code == 0
+        end
+    end
+    return false
+end
+
+-- the --usekpse flag will fallback on kpse (hm, we can better update mtx-stubs)
+
+local windows_stub = '@echo off\013\010setlocal\013\010set ownpath=%%~dp0%%\013\010texlua "%%ownpath%%mtxrun.lua" --usekpse --execute %s %%*\013\010endlocal\013\010'
+local unix_stub    = '#!/bin/sh\010mtxrun --usekpse --execute %s \"$@\"\010'
+
+function runners.handle_stubs(create)
+    local stubpath = environment.argument('stubpath') or '.' -- 'auto' no longer subpathssupported
+    local windows  = environment.argument('windows') or environment.argument('mswin') or false
+    local unix     = environment.argument('unix') or environment.argument('linux') or false
+    if not windows and not unix then
+        if os.platform == "unix" then
+            unix = true
+        else
+            windows = true
+        end
+    end
+    for _,v in pairs(runners.registered) do
+        local name, doit = v[1], v[2]
+        if doit then
+            local base = string.gsub(file.basename(name), "%.(.-)$", "")
+            if create then
+                if windows then
+                    io.savedata(file.join(stubpath,base..".bat"),string.format(windows_stub,name))
+                    logs.simple("windows stub for '%s' created",base)
+                end
+                if unix then
+                    io.savedata(file.join(stubpath,base),string.format(unix_stub,name))
+                    logs.simple("unix stub for '%s' created",base)
+                end
+            else
+                if windows and (os.remove(file.join(stubpath,base..'.bat')) or os.remove(file.join(stubpath,base..'.cmd'))) then
+                    logs.simple("windows stub for '%s' removed", base)
+                end
+                if unix and (os.remove(file.join(stubpath,base)) or os.remove(file.join(stubpath,base..'.sh'))) then
+                    logs.simple("unix stub for '%s' removed",base)
+                end
+            end
+        end
+    end
+end
+
+function runners.resolve_string(filename)
+    if filename and filename ~= "" then
+        runners.report_location(resolvers.resolve(filename))
+    end
+end
+
+function runners.locate_file(filename)
+    -- differs from texmfstart where locate appends .com .exe .bat ... todo
+    if filename and filename ~= "" then
+        runners.report_location(resolvers.find_given_file(filename))
+    end
+end
+
+function runners.locate_platform()
+    runners.report_location(os.platform)
+end
+
+function runners.report_location(result)
+    if logs.verbose then
+        logs.simpleline()
+        if result and result ~= "" then
+            logs.simple(result)
+        else
+            logs.simple("not found")
+        end
+    else
+        io.write(result)
+    end
+end
+
+function runners.edit_script(filename) -- we assume that vim is present on most systems
+    local editor = os.getenv("MTXRUN_EDITOR") or os.getenv("TEXMFSTART_EDITOR") or os.getenv("EDITOR") or 'vim'
+    local rest = resolvers.resolve(filename)
+    if rest ~= "" then
+        local command = editor .. " " .. rest
+        if logs.verbose then
+            logs.simpleline()
+            logs.simple("starting editor: %s",command)
+            logs.simple_line()
+            logs.simple_line()
+        end
+        os.launch(command)
+    end
+end
+
+function runners.save_script_session(filename, list)
+    local t = { }
+    for i=1,#list do
+        local key = list[i]
+        t[key] = environment.arguments[key]
+    end
+    io.savedata(filename,table.serialize(t,true))
+end
+
+function runners.load_script_session(filename)
+    if lfs.isfile(filename) then
+        local t = io.loaddata(filename)
+        if t then
+            t = loadstring(t)
+            if t then t = t() end
+            for key, value in pairs(t) do
+                environment.arguments[key] = value
+            end
+        end
+    end
+end
+
+function resolvers.launch(str)
+    -- maybe we also need to test on mtxrun.launcher.suffix environment
+    -- variable or on windows consult the assoc and ftype vars and such
+    local launchers = runners.launchers[os.platform] if launchers then
+        local suffix = file.extname(str) if suffix then
+            local runner = launchers[suffix] if runner then
+                str = runner .. " " .. str
+            end
+        end
+    end
+    os.launch(str)
+end
+
+function runners.launch_file(filename)
+    instance.allresults = true
+    logs.setverbose(true)
+    local pattern = environment.arguments["pattern"]
+    if not pattern or pattern == "" then
+        pattern = filename
+    end
+    if not pattern or pattern == "" then
+        logs.simple("provide name or --pattern=")
+    else
+        local t = resolvers.find_files(pattern)
+        if not t or #t == 0 then
+            t = resolvers.find_files("*/" .. pattern)
+        end
+        if not t or #t == 0 then
+            t = resolvers.find_files("*/" .. pattern .. "*")
+        end
+        if t and #t > 0 then
+            if environment.arguments["all"] then
+                for _, v in pairs(t) do
+                    logs.simple("launching %s", v)
+                    resolvers.launch(v)
+                end
+            else
+                logs.simple("launching %s", t[1])
+                resolvers.launch(t[1])
+            end
+        else
+            logs.simple("no match for %s", pattern)
+        end
+    end
+end
+
+function runners.find_mtx_script(filename)
+    local function found(name)
+        local path = file.dirname(name)
+        if path and path ~= "" then
+            return false
+        else
+            local fullname = own and own.path and file.join(own.path,name)
+            return io.exists(fullname) and fullname
+        end
+    end
+    filename = file.addsuffix(filename,"lua")
+    local basename = file.removesuffix(file.basename(filename))
+    local suffix = file.extname(filename)
+    -- qualified path, raw name
+    local fullname = file.is_qualified_path(filename) and io.exists(filename) and filename
+    if fullname and fullname ~= "" then
+        return fullname
+    end
+    -- current path, raw name
+    fullname = "./" .. filename
+    fullname = io.exists(fullname) and fullname
+    if fullname and fullname ~= "" then
+        return fullname
+    end
+    -- mtx- prefix checking
+    local mtxprefix = (filename:find("^mtx%-") and "") or "mtx-"
+    -- context namespace, mtx-<filename>
+    fullname = mtxprefix .. filename
+    fullname = found(fullname) or resolvers.find_file(fullname)
+    if fullname and fullname ~= "" then
+        return fullname
+    end
+    -- context namespace, mtx-<filename>s
+    fullname = mtxprefix .. basename .. "s" .. "." .. suffix
+    fullname = found(fullname) or resolvers.find_file(fullname)
+    if fullname and fullname ~= "" then
+        return fullname
+    end
+    -- context namespace, mtx-<filename minus trailing s>
+    fullname = mtxprefix .. basename:gsub("s$","") .. "." .. suffix
+    fullname = found(fullname) or resolvers.find_file(fullname)
+    if fullname and fullname ~= "" then
+        return fullname
+    end
+    -- context namespace, just <filename>
+    fullname = resolvers.find_file(filename)
+    return fullname
+end
+
+function runners.execute_ctx_script(filename)
+    local arguments = environment.arguments_after
+    local fullname = runners.find_mtx_script(filename) or ""
+    if file.extname(fullname) == "cld" then
+        -- handy in editors where we force --autopdf
+        logs.simple("running cld script: %s",filename)
+        table.insert(arguments,1,fullname)
+        table.insert(arguments,"--autopdf")
+        fullname = runners.find_mtx_script("context") or ""
+    end
+    -- retry after generate but only if --autogenerate
+    if fullname == "" and environment.argument("autogenerate") then -- might become the default
+        instance.renewcache = true
+        logs.setverbose(true)
+        resolvers.load()
+        --
+        fullname = runners.find_mtx_script(filename) or ""
+    end
+    -- that should do it
+    if fullname ~= "" then
+        local state = runners.prepare()
+        if state == 'error' then
+            return false
+        elseif state == 'skip' then
+            return true
+        elseif state == "run" then
+            -- load and save ... kind of undocumented
+            arg = { } for _,v in pairs(arguments) do arg[#arg+1] = resolvers.resolve(v) end
+            environment.initialize_arguments(arg)
+            local loadname = environment.arguments['load']
+            if loadname then
+                if type(loadname) ~= "string" then loadname = file.basename(fullname) end
+                loadname = file.replacesuffix(loadname,"cfg")
+                runners.load_script_session(loadname)
+            end
+            filename = environment.files[1]
+            if logs.verbose then
+                logs.simple("using script: %s\n",fullname)
+            end
+            environment.ownscript = fullname
+            dofile(fullname)
+            local savename = environment.arguments['save']
+            if savename then
+                local save_list = runners.save_list
+                if save_list and next(save_list) then
+                    if type(savename) ~= "string" then savename = file.basename(fullname) end
+                    savename = file.replacesuffix(savename,"cfg")
+                    runners.save_script_session(savename,save_list)
+                end
+            end
+            return true
+        end
+    else
+    --  logs.setverbose(true)
+        if filename == "" or filename == "help" then
+            local context = resolvers.find_file("mtx-context.lua")
+            logs.setverbose(true)
+            if context ~= "" then
+                local result = dir.glob((string.gsub(context,"mtx%-context","mtx-*"))) -- () needed
+                local valid = { }
+                table.sort(result)
+                for i=1,#result do
+                    local scriptname = result[i]
+                    local scriptbase = string.match(scriptname,".*mtx%-([^%-]-)%.lua")
+                    if scriptbase then
+                        local data = io.loaddata(scriptname)
+                        local banner, version = string.match(data,"[\n\r]logs%.extendbanner%s*%(%s*[\"\']([^\n\r]+)%s*(%d+%.%d+)")
+                        if banner then
+                            valid[#valid+1] = { scriptbase, version, banner }
+                        end
+                    end
+                end
+                if #valid > 0 then
+                    logs.reportbanner()
+                    logs.reportline()
+                    logs.simple("no script name given, known scripts:")
+                    logs.simple()
+                    for k=1,#valid do
+                        local v = valid[k]
+                        logs.simple("%-12s  %4s  %s",v[1],v[2],v[3])
+                    end
+                end
+            else
+                logs.simple("no script name given")
+            end
+        else
+            filename = file.addsuffix(filename,"lua")
+            if file.is_qualified_path(filename) then
+                logs.simple("unknown script '%s'",filename)
+            else
+                logs.simple("unknown script '%s' or 'mtx-%s'",filename,filename)
+            end
+        end
+        return false
+    end
+end
+
+function runners.prefixes()
+    logs.reportbanner()
+    logs.reportline()
+    logs.simple(table.concat(resolvers.allprefixes(true)," "))
+end
+
+function runners.timedrun(filename) -- just for me
+    if filename and filename ~= "" then
+        runners.timed(function() os.execute(filename) end)
+    end
+end
+
+function runners.timed(action)
+    statistics.timed(action)
+end
+
+-- this is a bit dirty ... first we store the first filename and next we
+-- split the arguments so that we only see the ones meant for this script
+-- ... later we will use the second half
+
+local filename = environment.files[1] or ""
+local ok      = true
+
+local before, after = environment.split_arguments(filename)
+environment.arguments_before, environment.arguments_after = before, after
+environment.initialize_arguments(before)
+
+instance.engine   = environment.argument("engine")   or 'luatex'
+instance.progname = environment.argument("progname") or 'context'
+instance.lsrmode  = environment.argument("lsr")      or false
+
+-- maybe the unset has to go to this level
+
+local is_mkii_stub = runners.registered[file.removesuffix(file.basename(filename))]
+
+if environment.argument("usekpse") or environment.argument("forcekpse") or is_mkii_stub then
+
+    os.setenv("engine","")
+    os.setenv("progname","")
+
+    local remapper = {
+        otf = "opentype fonts",
+        ttf = "truetype fonts",
+        ttc = "truetype fonts",
+        pfb = "type1 fonts",
+        other = "other text files",
+    }
+
+    local function kpse_initialized()
+        texconfig.kpse_init = true
+        local t = os.clock()
+        local k = kpse.original.new("luatex",instance.progname)
+        local dummy = k:find_file("mtxrun.lua") -- so that we're initialized
+        logs.simple("kpse fallback with progname '%s' initialized in %s seconds",instance.progname,os.clock()-t)
+        kpse_initialized = function() return k end
+        return k
+    end
+
+    local find_file = resolvers.find_file
+    local show_path = resolvers.show_path
+
+    if environment.argument("forcekpse") then
+
+        function resolvers.find_file(name,kind)
+            return (kpse_initialized():find_file(resolvers.clean_path(name),(kind ~= "" and (remapper[kind] or kind)) or "tex") or "") or ""
+        end
+        function resolvers.show_path(name)
+            return (kpse_initialized():show_path(name)) or ""
+        end
+
+    elseif environment.argument("usekpse") or is_mkii_stub then
+
+        resolvers.load()
+
+        function resolvers.find_file(name,kind)
+            local found = find_file(name,kind) or ""
+            if found ~= "" then
+                return found
+            else
+                return (kpse_initialized():find_file(resolvers.clean_path(name),(kind ~= "" and (remapper[kind] or kind)) or "tex") or "") or ""
+            end
+        end
+        function resolvers.show_path(name)
+            local found = show_path(name) or ""
+            if found ~= "" then
+                return found
+            else
+                return (kpse_initialized():show_path(name)) or ""
+            end
+        end
+
+    end
+
+else
+
+    resolvers.load()
+
+end
+
+if environment.argument("selfmerge") then
+    -- embed used libraries
+    utils.merger.selfmerge(own.name,own.libs,own.list)
+elseif environment.argument("selfclean") then
+    -- remove embedded libraries
+    utils.merger.selfclean(own.name)
+elseif environment.argument("selfupdate") then
+    logs.setverbose(true)
+    resolvers.update_script(own.name,"mtxrun")
+elseif environment.argument("ctxlua") or environment.argument("internal") then
+    -- run a script by loading it (using libs)
+    ok = runners.execute_script(filename,true)
+elseif environment.argument("script") or environment.argument("scripts") then
+    -- run a script by loading it (using libs), pass args
+    if is_mkii_stub then
+    -- execute mkii script
+        ok = runners.execute_script(filename,false,true)
+    else
+        ok = runners.execute_ctx_script(filename)
+    end
+elseif environment.argument("execute") then
+    -- execute script
+    ok = runners.execute_script(filename)
+elseif environment.argument("direct") then
+    -- equals bin:
+    ok = runners.execute_program(filename)
+elseif environment.argument("edit") then
+    -- edit file
+    runners.edit_script(filename)
+elseif environment.argument("launch") then
+    runners.launch_file(filename)
+elseif environment.argument("make") then
+    -- make stubs
+    runners.handle_stubs(true)
+elseif environment.argument("remove") then
+    -- remove stub
+    runners.handle_stubs(false)
+elseif environment.argument("resolve") then
+    -- resolve string
+    runners.resolve_string(filename)
+elseif environment.argument("locate") then
+    -- locate file
+    runners.locate_file(filename)
+elseif environment.argument("platform")then
+    -- locate platform
+    runners.locate_platform()
+elseif environment.argument("prefixes") then
+    runners.prefixes()
+elseif environment.argument("timedrun") then
+    -- locate platform
+    runners.timedrun(filename)
+elseif environment.argument("help") or filename=='help' or filename == "" then
+    logs.help(messages.help)
+    -- execute script
+elseif filename:find("^bin:") then
+    ok = runners.execute_program(filename)
+elseif is_mkii_stub then
+    -- execute mkii script
+    ok = runners.execute_script(filename,false,true)
+else
+    ok = runners.execute_ctx_script(filename)
+    if not ok then
+        ok = runners.execute_script(filename)
+    end
+end
+
+if os.platform == "unix" then
+    io.write("\n")
+end
+
+if ok == false then ok = 1 elseif ok == true then ok = 0 end
+
+os.exit(ok)
diff --git a/scripts/context/stubs/unix/texexec b/scripts/context/stubs/unix/texexec
new file mode 100644
index 000000000..cd5900ff8
--- /dev/null
+++ b/scripts/context/stubs/unix/texexec
@@ -0,0 +1,2 @@
+#!/bin/sh
+mtxrun --usekpse --execute texexec "$@"
diff --git a/scripts/context/stubs/unix/texmfstart b/scripts/context/stubs/unix/texmfstart
new file mode 100644
index 000000000..1799b3579
--- /dev/null
+++ b/scripts/context/stubs/unix/texmfstart
@@ -0,0 +1,2 @@
+#!/bin/sh
+mtxrun --usekpse "$@"
-- 
cgit v1.2.3