From 85b7bc695629926641c7cb752fd478adfdf374f3 Mon Sep 17 00:00:00 2001 From: Marius 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 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 '' + 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-- +

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 structured file. Actually, any logging that +is hooked into callbacks will be \XML\ by default.

+--ldx]]-- + +logs = logs or { } +logs.xml = logs.xml or { } +logs.tex = logs.tex or { } + +--[[ldx-- +

This looks pretty ugly but we need to speed things up a bit.

+--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("%s",category,format(fmt,...))) + else + write_nl(format("",category)) + end +end +function logs.xml.line(fmt,...) -- new + if fmt then + write_nl(format("%s",format(fmt,...))) + else + write_nl("") + 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("") end end +function logs.xml.push () if logs.level > 0 then tw("" ) end end + +function logs.xml.start_run() + write_nl("") + write_nl("") -- xmlns='www.pragma-ade.com/luatex/schemas/context-job.rng' + write_nl("") +end + +function logs.xml.stop_run() + write_nl("") +end + +function logs.xml.start_page_number() + write_nl(format("

") + write_nl("") +end + +function logs.xml.report_output_pages(p,b) + write_nl(format("", p)) + write_nl(format("", b)) + write_nl("") +end + +function logs.xml.report_output_log() +end + +function logs.xml.report_tex_stat(k,v) + texiowrite_nl("log",""..tostring(v).."") +end + +local level = 0 + +function logs.xml.show_open(name) + level = level + 1 + texiowrite_nl(format("",level,name)) +end + +function logs.xml.show_close(name) + texiowrite(" ") + level = level - 1 +end + +function logs.xml.show_load(name) + texiowrite_nl(format("",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: + 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-- +

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.

+ + +TEXMFCACHE=$TMP;$TEMP;$TMPDIR;$TEMPDIR;$HOME;$TEXMFVAR;$VARTEXMF;. + + +

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.

+--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-- +

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).

+ +

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.

+ +

Examples of usage can be found in the font related code.

+--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-- +

This file is used when we want the input handlers to behave like +kpsewhich. What to do with the following:

+ + +{$SELFAUTOLOC,$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,}/web2c} +$SELFAUTOLOC : /usr/tex/bin/platform +$SELFAUTODIR : /usr/tex/bin +$SELFAUTOPARENT : /usr/tex + + +

How about just forgetting about them?

+--ldx]]-- + +local suffixes = resolvers.suffixes +local formats = resolvers.formats + +suffixes['gf'] = { 'gf' } +suffixes['pk'] = { '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-- +

If you wondered abou tsome of the previous mappings, how about +the next bunch:

+--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 .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"] = " ", + ["\r"] = " ", + ["\t"] = " ", + }) + 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("\n\n") + f:write(string.format("\n",yn(ctxdata.runlocal))) + local sorted = table.sortedkeys(prepfiles) + for i=1,#sorted do + local name = sorted[i] + f:write(string.format("\t%s\n",yn(prepfiles[name]),name)) + end + f:write("\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("^&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 "") + 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- 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 "") + logs.simple("weight : %s",specification.weight or "") + logs.simple("style : %s",specification.style or "") + logs.simple("width : %s",specification.width or "") + logs.simple("variant : %s",specification.variant or "") + 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 "", + entry.weight or "", + entry.style or "", + entry.width or "", + entry.variant or "", + 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 ",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 ",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 ",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 ",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] = "" + result[#result+1] = "\n" + result[#result+1] = "" + result[#result+1] = "\t" + result[#result+1] = "\t\t" + for i=1,#collection do + local command = collection[i] + result[#result+1] = format("\t\t\t%s",command) + end + result[#result+1] = "\t\t" + result[#result+1] = "\t" + result[#result+1] = "" + io.savedata(format("context-jedit-%s.xml",interface), table.concat(result),"\n") +end + +function flushers.bbedit(interface,collection) + local result = {} + result[#result+1] = "" + result[#result+1] = "BBLMKeywordList" + result[#result+1] = "" + for i=1,#collection do + local command = collection[i] + result[#result+1] = format("\t\\%s",command) + end + result[#result+1] = "" + 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\n",what,language) + xmlresult[#xmlresult+1] = format("\t",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",tag,key,value) + end + end + xmlresult[#xmlresult+1] = format("\t\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("\n",tag) + xmlresult[#xmlresult+1] = format("\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("") + 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("( 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 + safe name     + family name     + style-variant-weight-width     + font name     + weight     + filename +]] + +local template_d = [[ + + %s     + %s     + %s-%s-%s-%s     + %s     + %s     + %s +]] + +local function select_font() + local t = fonts.names.list(".*",false,true) + if t then + local listoffonts = { } + listoffonts[#listoffonts+1] = "" + 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] = "
" + return concat(listoffonts,"\n") + end + return "no fonts" +end + +local edit_template = [[ + +

name:    title:  +

scripts: %s +

languages: %s +

features: %s +

options: %s +]] + +local result_template = [[ +

+ +

results: + tex file + pdf file +

+]] + +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(" %s",s,v,v,v,v) + else + scripts[#scripts+1] = format(" %s",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(" %s",l,v,v,v,v) + else + languages[#languages+1] = format(" %s",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(" %s",f,v,v,v,v) + else + features[#features+1] = format(" %s",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(" %s",v,v,v) + else + options[#options+1] = format(" %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 = [[ +

+%s
+
+]] + +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("

names

",what) + result[#result+1] = "" + result[#result+1] = format("",currentfont) + result[#result+1] = format("",specification.fontname or "-") + result[#result+1] = format("",specification.fontfile or "-") + result[#result+1] = format("",specification.familyname or "-") + result[#result+1] = format("",specification.fontweight or "-") + result[#result+1] = format("",specification.format or "-") + result[#result+1] = format("",specification.fullname or "-") + result[#result+1] = format("",specification.subfamily or "-") + result[#result+1] = format("",specification.rawname or "-") + result[#result+1] = format("",specification.designsize or "-") + result[#result+1] = format("",specification.minsize or "-") + result[#result+1] = format("",specification.maxsize or "-") + result[#result+1] = format("",specification.style ~= "" and specification.style or "normal") + result[#result+1] = format("",specification.variant ~= "" and specification.variant or "normal") + result[#result+1] = format("",specification.weight ~= "" and specification.weight or "normal") + result[#result+1] = format("",specification.width ~= "" and specification.width or "normal") + result[#result+1] = "
fontname: %s
fullname: %s
filename: %s
familyname: %s
fontweight: %s
format: %s
fullname: %s
subfamily: %s
rawname: %s
designsize: %s
minimumsize:%s
maximumsize:%s
style: %s
variant: %s
weight: %s
width: %s
" + 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("

%s features

",what) + result[#result+1] = "" + result[#result+1] = "" + 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("",title,f,s,concat(table.sortedkeys(ss)," ")) + end + end + result[#result+1] = "
featuretag script languages 
%s  %s  %s  %s  
" + end + end + else + result[#result+1] = "

This font has no features." + end + return concat(result,"\n") +end + + +local info_template = [[ +

+version   : %s
+comment   : %s
+author    : %s
+copyright : %s
+
+maillist  : ntg-context at ntg.nl
+webpage   : www.pragma-ade.nl
+wiki      : contextgarden.net
+
+]] + +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: +

+ + + + + + + + + +
name  %s
title  %s
font  %s
script  %s
language  %s
features  %s
options  %s
sampletext %s
+]] + +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("del name font fontname script language features title sampletext ") + for k,v in table.sortedhash(storage) do + local fontname, fontfile = get_specification(v.font) + result[#result+1] = format("x %s %s %s %s %s %s %s %s ", + 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("%s
",concat(result,"\n")) + end +end + +local function reset_font(currentfont) + return edit_font(currentfont) +end + +local extras_template = [[ + remake font database (take some time)

+]] + +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 = [[ + +]] + +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,button) + end + + variables.menu = concat(menu," ") + 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 = "
%s
" +} + +document.setups.span = { + pe = "%s" +} + +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 = { [[%s]], [[%s]] }, + href_as_command = { [[\%s]], [[context.%s]] }, + interface = [[%s]], + source = [[%s]], + modes = { [[lua mode]], [[tex mode]] }, + 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 = [[%s%s%s]], + parameters = [[%s
]], + listing = [[
%s]],
+    special         = [[%s]],
+    default         = [[%s]],
+}
+
+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("
") then + parameters[#parameters+1] = formats.parameter:format("
","","") + 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("
","","") + 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,"
") + local i = concat(ints,"

") + + 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,"   ") + 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("%s

",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("%s %s

%s %s

",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 = [[ +

%s (red) %s (blue)

+ + + + + +
%s +   min: %s
+   max: %s
+   pages: %s
+   average: %s
+
+
+]] + +local html_menu = [[ + %s +]] + +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, "  "), + ['nodesmenu'] = concat(htmldata.nodes, "  "), + ['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",d,(longname and path and join(path,name)) or name) + result[#result+1] = format("%s %s",d,removesuffix(name)) + result[#result+1] = format("%s %s",d,extname(name)) + result[#result+1] = format("%s %s",d,attr.size) + result[#result+1] = format("%s %s",d,sub(attr.permissions,7,9)) + result[#result+1] = format("%s %s",d,date(timestamp,attr.modification)) + result[#result+1] = format("%s",d) + elseif mode == "directory" then + result[#result+1] = format("%s",d,name) + flush(attr.list,result,n+1,(path and join(path,name)) or name) + result[#result+1] = format("%s",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 = { "" } + result[#result+1] = format("",url,root,pattern,luapattern,xmlns,date(timestamp)) + flush(list,result) + result[#result+1] = "" + + 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//", "texmf-context" }, + { "context/img/", "texmf-context" }, + { "misc/setuptex/", "." }, + { "misc/web2c", "texmf" }, + { "bin/common//", "texmf-" }, + { "bin/context//", "texmf-" }, + { "bin/metapost//", "texmf-" }, + { "bin/man/", "texmf-" }, +} + +-- 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//", "texmf-" }, + }, + ["xetex"] = { + { "base/xetex/", "texmf" }, + { "fonts/new/", "texmf" }, + { "bin/luatex//", "texmf-" }, -- tools + { "bin/xetex//", "texmf-" }, + }, + ["pdftex"] = { + { "fonts/old/", "texmf" }, + { "bin/luatex//", "texmf-" }, -- tools + { "bin/pdftex//", "texmf-" }, + }, + ["all"] = { + { "fonts/new/", "texmf" }, + { "fonts/old/", "texmf" }, + { "base/xetex/", "texmf" }, + { "bin/luatex//", "texmf-" }, + { "bin/xetex//", "texmf-" }, + { "bin/pdftex//", "texmf-" }, + }, +} + +scripts.update.goodies = { + ["scite"] = { + { "bin//scite/", "texmf-" }, + }, + ["texworks"] = { + { "bin//texworks/", "texmf-" }, + }, +} + +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) + archive = archive:gsub("", 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) + local destination = format("%s/%s", texroot, c[2]:gsub("", platform)) + destination = destination:gsub("\\","/") + archive = archive:gsub("",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] = "\n" + t[#t+1] = "" + for k, v in next, joblog.values do + t[#t+1] = format("\t%s", k, tostring(v)) + end + t[#t+1] = "" + 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 // +--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 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 '' + 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-- +

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 based one. +The find based parser can be found in l-xml-edu.lua along with other older code.

+ +

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.

+--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-- +

First a hack to enable namespace resolving. A namespace is characterized by +a . The following function associates a namespace prefix with a +pattern. We use , 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.

+--ldx]]-- + +xml.xmlns = xml.xmlns or { } + +local check = P(false) +local parse = check + +--[[ldx-- +

The next function associates a namespace prefix with an . This +normally happens independent of parsing.

+ + +xml.registerns("mml","mathml") + +--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-- +

The next function also registers a namespace, but this time we map a +given namespace prefix onto a registered one, using the given +. This used for attributes like xmlns:m.

+ + +xml.checkns("m","http://www.w3.org/mathml") + +--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-- +

Next we provide a way to turn an into a registered +namespace. This used for the xmlns attribute.

+ + +resolvedns = xml.resolvens("http://www.w3.org/mathml") + + +This returns mml. +--ldx]]-- + +function xml.resolvens(url) + return lpegmatch(parse,lower(url)) or "" +end + +--[[ldx-- +

A namespace in an element can be remapped onto the registered +one efficiently by using the xml.xmlns table.

+--ldx]]-- + +--[[ldx-- +

This version uses . 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 files that +took 12.5 seconds to load (1.5 for file io and the rest for tree building). With +the implementation we got that down to less 7.3 seconds. Loading the 14 + interface definition files (2.6 meg) went down from 1.05 seconds to 0.55.

+ +

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 + code to it.

+ + + + + + + + + + +

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:

+ + +local x = xml.convert(somestring) + + +

An optional second boolean argument tells this function not to create a root +element.

+ +

Valid entities are:

+ + + + + + +--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] = "&", + [42] = """, + [47] = "'", + [74] = "<", + [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(" & + 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-- +

Packaging data in an xml like table is done with the following +function. Maybe it will go away (when not used).

+--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-- +

We cannot load an from a filehandle so we need to load +the whole file first. The function accepts a string representing +a filename or a file handle.

+--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-- +

When we inject new elements, we need to convert strings to +valid trees, which is what the next function does.

+--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-- +

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!

+--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-- +

In 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.

+--ldx]]-- + +-- todo: add 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-- +

At the cost of some 25% runtime overhead you can first convert the tree to a string +and then handle the lot.

+--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("") + 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("") + else + if ats then + handle("<",etg," ",concat(ats," "),"/>") + else + handle("<",etg,"/>") + end + end + end +end + +local function verbose_pi(e,handlers) + handlers.handle("") +end + +local function verbose_comment(e,handlers) + handlers.handle("") +end + +local function verbose_cdata(e,handlers) + handlers.handle("") +end + +local function verbose_doctype(e,handlers) + handlers.handle("") +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-- +

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):

+ + +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 + + +

Beware, these were timing with the old routine but measurements will not be that +much different I guess.

+--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-- +

The next function operated on the content only and needs a handle function +that accepts a string.

+--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-- +

A few helpers:

+--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-- +

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:

+--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-- +

The next helper assigns a tree (or string). Usage:

+ + +dt[k] = xml.assign(root) or xml.assign(dt,k,root) + +--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-- +

The next helper assigns a tree (or string). Usage:

+ +xml.tocdata(e) +xml.tocdata(e,"error") + +--ldx]]-- + +function xml.tocdata(e,wrapper) + local whatever = xmltostring(e.dt) + if wrapper then + whatever = format("<%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-- +

This module can be used stand alone but also inside 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.

+

If I can get in the mood I will make a variant that is XSLT compliant +but I wonder if it makes sense.

+--ldx]]-- + +--[[ldx-- +

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 we also need +this module for process management, like handling and +files.

+ + +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) + +--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-- +

We've now arrived at an interesting part: accessing the tree using a subset +of and since we're not compatible we call it . We +will explain more about its usage in other documents.

+--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-- +

This is the main filter function. It returns whatever is asked for.

+--ldx]]-- + +function xml.filter(root,pattern) -- no longer funny attribute handling here + return parse_apply({ root },pattern) +end + +--[[ldx-- +

Often using an iterators looks nicer in the code than passing handler +functions. The book describes how to use coroutines for that +purpose (). This permits +code like:

+ + +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 + +--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-- +

The following helper functions best belong to the lxml-ini +module. Some are here because we need then in the mk +document and other manuals, others came up when playing with +this module. Since this module is also used in we've +put them here instead of loading mode modules there then needed.

+--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 = { ['&'] = '&', ['<'] = '<', ['>'] = '>', ['"'] = '"' } +--~ 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("<")/"<" + S(">")/">" + S("&")/"&" + 1)^0) +local normal = (1 - S("<&>"))^0 +local special = P("<")/"<" + P(">")/">" + P("&")/"&" +local escaped = Cs(normal * (special * normal)^0) + +-- 100 * 1000 * "oeps< oeps> oeps&" : gsub:lpeg == 0153:0280:0151:0080 (last one by roberto) + +local normal = (1 - S"&")^0 +local special = P("<")/"<" + P(">")/">" + P("&")/"&" +local unescaped = Cs(normal * (special * normal)^0) + +-- 100 * 5000 * "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-- +

The following functions collect elements and texts.

+--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-- +

We've now arrived at the functions that manipulate the tree.

+--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-- +

Here are a few synonyms.

+--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-- +

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 structured file. Actually, any logging that +is hooked into callbacks will be \XML\ by default.

+--ldx]]-- + +logs = logs or { } +logs.xml = logs.xml or { } +logs.tex = logs.tex or { } + +--[[ldx-- +

This looks pretty ugly but we need to speed things up a bit.

+--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("%s",category,format(fmt,...))) + else + write_nl(format("",category)) + end +end +function logs.xml.line(fmt,...) -- new + if fmt then + write_nl(format("%s",format(fmt,...))) + else + write_nl("") + 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("") end end +function logs.xml.push () if logs.level > 0 then tw("" ) end end + +function logs.xml.start_run() + write_nl("") + write_nl("") -- xmlns='www.pragma-ade.com/luatex/schemas/context-job.rng' + write_nl("") +end + +function logs.xml.stop_run() + write_nl("") +end + +function logs.xml.start_page_number() + write_nl(format("

") + write_nl("") +end + +function logs.xml.report_output_pages(p,b) + write_nl(format("", p)) + write_nl(format("", b)) + write_nl("") +end + +function logs.xml.report_output_log() +end + +function logs.xml.report_tex_stat(k,v) + texiowrite_nl("log",""..tostring(v).."") +end + +local level = 0 + +function logs.xml.show_open(name) + level = level + 1 + texiowrite_nl(format("",level,name)) +end + +function logs.xml.show_close(name) + texiowrite(" ") + level = level - 1 +end + +function logs.xml.show_load(name) + texiowrite_nl(format("",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: + 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-- +

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.

+ + +TEXMFCACHE=$TMP;$TEMP;$TMPDIR;$TEMPDIR;$HOME;$TEXMFVAR;$VARTEXMF;. + + +

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.

+--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-- +

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).

+ +

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.

+ +

Examples of usage can be found in the font related code.

+--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-- +

This file is used when we want the input handlers to behave like +kpsewhich. What to do with the following:

+ + +{$SELFAUTOLOC,$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,}/web2c} +$SELFAUTOLOC : /usr/tex/bin/platform +$SELFAUTODIR : /usr/tex/bin +$SELFAUTOPARENT : /usr/tex + + +

How about just forgetting about them?

+--ldx]]-- + +local suffixes = resolvers.suffixes +local formats = resolvers.formats + +suffixes['gf'] = { 'gf' } +suffixes['pk'] = { '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-- +

If you wondered abou tsome of the previous mappings, how about +the next bunch:

+--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- + fullname = mtxprefix .. filename + fullname = found(fullname) or resolvers.find_file(fullname) + if fullname and fullname ~= "" then + return fullname + end + -- context namespace, mtx-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- + 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 + 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-- +Lua Documentation Module + +This file is part of the documentation suite and +itself serves as an example of using in combination +with . + +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 +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 + + +mtxrun --internal x-ldx.lua + + +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 l stands for +, the d for documentation and the x for +. +--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 code and comment. +--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 do +construction. +--ldx]]-- + +do + local e = { [">"] = ">", ["<"] = "<", ["&"] = "&" } + 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>" + end) + cod = cod:gsub("%-%-([^\n]*)", function(s) + com[#com+1] = s + return ">>c>" + end) + cod = cod:gsub("(%b\"\")", function(s) + dqs[#dqs+1] = s:sub(2,-2) or "" + return ">>d>" + end) + cod = cod:gsub("(%b\'\')", function(s) + sqs[#sqs+1] = s:sub(2,-2) or "" + return ">>s>" + end) + cod = cod:gsub("(%a+)",function(key) + local class = ldx.keywords.reserved[key] + if class then + return "" .. key .. "" + else + return key + end + end) + cod = cod:gsub(">>s>", function(s) + return "" .. sqs[tonumber(s)] .. "" + end) + cod = cod:gsub(">>d>", function(s) + return "" .. dqs[tonumber(s)] .. "" + end) + cod = cod:gsub(">>c>", function(s) + return "" .. com[tonumber(s)] .. "" + end) + cod = cod:gsub(">>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 = "(function)%s+([%w%.]+)%s*%(" + for k=1,#lines do + local v = lines[k] + -- functies + v = v:gsub(f,function(key, str) + return "" .. str .. "(" + end) + -- variables + v = v:gsub("^([%w][%w%,%s]-)(=[^=])",function(str, rest) + local t = string.split(str, ",%s*") + for k=1,#t do + t[k] = "" .. t[k] .. "" + 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 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 CDATA 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] = "\n" + t[#t+1] = "\n\n" + for k=1,#data do + local v = data[k] + if v.code and not v.code:is_empty() then + t[#t+1] = "\n\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] = "" .. v .. "\n" + else + t[#t+1] = "" .. v .. "\n" + end + elseif v:is_empty() then + if cmode then + t[#t+1] = "\n" + else + t[#t+1] = "\n" + end + elseif v:find("^%-%-%[%[") then + t[#t+1] = "" .. v .. "\n" + cmode= true + elseif v:find("^%]%]%-%-") then + t[#t+1] = "" .. v .. "\n" + cmode= false + elseif cmode then + t[#t+1] = "" .. v .. "\n" + else + t[#t+1] = "" .. v .. "\n" + end + end + t[#t+1] = "\n" + elseif v.comment then + t[#t+1] = "\n\n" .. v.comment .. "\n\n" + else + -- cannot happen + end + end + t[#t+1] = "\n\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: + + +mtxrun --internal x-ldx somefile.lua + + +will produce an ldx file that can be processed with +by running: + + +texexec --use=x-ldx --forcexml somefile.ldx + + +You can do this in one step by saying: + + +texmfstart texexec --ctx=x-ldx somefile.lua + + +This will trigger into loading the mentioned + 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*",) % 65535 } ; + close (MPO) ; + return unless open (MPY,"$mpyfile") ; + my $str = ; 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 = ; + 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 () + { 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 () # 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 () { + 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 () { + 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 [] [] +#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=\n" . + "--offset=\n" . + "--leftcrop=\n" . + "--rightcrop=\n" . + "--topcrop=\n" . + "--bottomcrop=\n" . + "--gsbin=\n" . + "--page=\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 () + { 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 ('' , \&ShowFile ) ; + $dir -> bind ('' , \&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 ('' , \&LocateStrings ) ; +$fil -> bind ('' , \&LocateStrings ) ; +$loc -> bind ('' , \&ChangeLocation ) ; +$log -> bind ('' , \&ShowPath ) ; + +$sea -> bind ('' , \&QuitSearch ) ; +$fil -> bind ('' , \&QuitSearch ) ; +$loc -> bind ('' , \&QuitSearch ) ; + +$sea -> bind ('' , \&QuitSearch ) ; +$fil -> bind ('' , \&QuitSearch ) ; +$loc -> bind ('' , \&QuitSearch ) ; +$log -> bind ('' , \&QuitSearch ) ; + +$sea -> bind ('' , \&LocateStrings ) ; +$fil -> bind ('' , \&LocateStrings ) ; +$loc -> bind ('' , \&ChangeLocation ) ; +$log -> bind ('' , \&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 () + { ++$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 () + { 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 () + { 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 () { 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 () + { 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 () { + 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/<\[/) + { 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 () { + 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/<\[/ $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 = ; + 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!(//,"") + 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 + 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 << "\n\n" + if @local then + log << "\n" + else + log << "\n" + end + @prepfiles.keys.sort.each do |prep| + # log << "\t#{File.basename(prep)}\n" + log << "\t#{prep}\n" + end + log << "\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!(/</o, '<') + str.gsub!(/>/o, '>') + str.gsub!(/&/o, '&') + str.gsub!(/"/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>/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 + "#{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>)/mo + @@redone = /]*?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 = "", "" + binln, einln = "" , "" + egraf = "

" + 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!(/>/o) { '>' } + str.gsub!(/</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!(/\/) 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'] = ['.gf'] # todo + @@suffixes['pk'] = ['.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 + # /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 ; "" + 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(' ','') + end + arg = arg.split(/\s+/o) + arg.collect! do |a| + a.gsub('',' ') + 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'] = <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'] = <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 + 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 nn0 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 column0 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 line0 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 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 =~ /\/o then + mappings[$1] = $2 + end + end + f.close + if f = open("cont-en.xml") then + while str = f.gets do + if str =~ /\/o then + collection["start#{mappings[$1]}"] = '' + collection["stop#{mappings[$1]}"] = '' + elsif str =~ /\/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("\n\n") + f.puts("\n\n") + f.puts("\n") + f.puts(" \n") + f.puts(" \n") + collection.keys.sort.each do |name| + f.puts(" \\#{name}\n") unless name.empty? + end + f.puts(" \n") + f.puts(" \n") + f.puts("\n") + f.close + end + when 'bbedit' then + if f = open("context-bbedit-#{interface}.xml", 'w') then + f.puts("\n\n") + f.puts("BBLMKeywordList\n") + f.puts("\n") + collection.keys.sort.each do |name| + f.puts(" \\#{name}\n") unless name.empty? + end + f.puts("\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!(/(\]*?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 + "\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}\n\n" + else + "\\#{tag} \{#{lst}\}\n\n" + end + end + + def banner + if @commandline.option('xml') then + "\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 = "\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\|/, "[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\|/, "[greeketaiotasubpsili]") + remap(/h\|/, "[greeketaiotasub]") + remap(/\<'w\|/, "[greekomegaiotasubdasiatonos]") + remap(/\>'w\|/, "[greekomegaiotasubpsilitonos]") + remap(/\<`w\|/, "[greekomegaiotasubdasiavaria]") + remap(/\>`w\|/, "[greekomegaiotasubpsilivaria]") + remap(/\<~w\|/, "[greekomegaiotasubdasiaperispomeni]") + remap(/\>~w\|/, "[greekomegaiotasubpsiliperispomeni]") + 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/, "[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/, "[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/, "[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/, "[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/, "[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/, "[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/, "[greekomegapsili]") + remap(/\'w/, "[greekomegaoxia]") + remap(/\`w/, "[greekomegavaria]") + remap(/\~w/, "[greekomegaperispomeni]") + remap(/\r/, "[greekrhopsili]") + remap(/\<\~/, "[greekdasiaperispomeni]") + remap(/\>\~/, "[greekpsiliperispomeni]") + remap(/\<\'/, "[greekdasiatonos]") + remap(/\>\'/, "[greekpsilitonos]") + remap(/\<\`/, "[greekdasiavaria]") + remap(/\>\`/, "[greekpsilivaria]") + remap(/\"\'/, "[greekdialytikatonos]") + remap(/\"\`/, "[greekdialytikavaria]") + remap(/\"\~/, "[greekdialytikaperispomeni]") + 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,"") + else + self.insert(2,"") + 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: + + def flushentities(handle,entities,doctype=nil) # 'stylesheet' + if doctype then + tab = "\t" + handle << "\n\n" + handle << "\n\n" + handle << "\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] == "" 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 + "" + end + + def XML::start(element, attributes='') + if attributes.empty? then + "<#{element}>" + else + "<#{element} #{attributes}>" + end + end + + def XML::end(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 + + # + # + # pstopdf + # E:/tmp/demo.ps + # + # + # false + # -1 + # + # + + 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 + + # + # + # + # + # texmfstart + # --verbose + # --iftouched=/,/ + # pstopdf + # --method=5 + # --inputpath= + # --outputpath=/ + # + # + # + # + # + # + # + # + # figure + # found + # cow.pdf + # pdf + # . + # lowres + # lowres/ + # 276.03125pt + # 200.75pt + # + # + + 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!(/</o, '<') + str.gsub!(/>/o, '>') + str.gsub!(/&/o, '&') + str.gsub!(/"/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 = "" + + def identify(resultfile='rlxtools.rli') + if @commandline.option('collect') then + begin + File.open(resultfile,'w') do |f| + f << "#{@@xmlbanner}\n" + f << "\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 << "\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 << "\n" + str << " #{size}\n" + str << " #{File.dirname(filename).sub(/\\/o,'/')}\n" + str << " #{width}\n" + str << " #{height}\n" + str << "\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}" + 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(/\"/, '"')}\"" + 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("\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("\n") + else + output.xputs("\n") + end + files.each do |f| + bn, dn = File.basename(f), File.dirname(f) + if dirname != dn then + output.xputs("\n", 2) if dirname != '' + output.xputs("\n", 2) + dirname = dn + end + if longname && dn != '.' then + output.xputs("\n", 4) + else + output.xputs("\n", 4) + end + output.xputs("#{bn.sub(/\..*$/,'')}\n", 6) + if File.stat(f).file? then + bt = bn.sub(/^.*\./,'') + if bt != bn then + output.xputs("#{bt}\n", 6) + end + output.xputs("#{File.stat(f).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}\n", 6) unless permissions.empty? + end + output.xputs("#{File.stat(f).mtime.strftime("%Y-%m-%d %H:%M")}\n", 6) + output.xputs("\n", 4) + end + output.xputs("\n", 2) if dirname != '' + output.xputs("\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 =~ /\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 "\n" + f.puts "\n" + if entities.length>0 then + total = 0 + entities.each do |k,v| + total += v + end + f.puts " \n" + entities.keys.asort.each do |entity| + f.puts " \n" + end + f.puts " \n" + end + if utf && (chars.size > 0) then + total = 0 + chars.each do |k,v| + total += v + end + f.puts " \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 " \n" + else + f.puts " \n" + end + end + f.puts " \n" + end + if elements.length>0 then + f.puts " \n" + elements.keys.sort.each do |element| + if attributes.key?(element) then + f.puts " \n" + if attributes.key?(element) then + attributes[element].keys.asort.each do |attribute| + f.puts " \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 " \n" + else + attributes[element][attribute].keys.asort.each do |value| + f.puts " \n" + end + end + f.puts " \n" + end + end + f.puts " \n" + else + f.puts " \n" + end + end + f.puts " \n" + end + f.puts "\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 << "\n\n" + f << "\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 << "\n\n" unless found > 1 + f << "" + f << "#{str.gsub(/^\s*/m,'').gsub(/\s*$/m,'')}" + f << "\n\n" + end + end + f << "\n\n" if found > 0 + end + end + f << "\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}#{$3}" + end + # (abcd + # data.gsub!(/(\()([a-z]{4,})/moi) do + # done = true + # report("compound: #{$1}#{$2}") if verbose + # "#{$2}" + # end + # abcd) + # data.gsub!(/(\()([a-z]{4,})/moi) do + # done = true + # report("compound: #{$1}#{$2}") if verbose + # "#{$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 =~ /" + + data.gsub!(/\<(\/*\w+)\s*(\/*)>/o) do + "<#{$1}#{$2}>" + end + + # remove funny ampersands + # + # data = " B&W " + + data.gsub!(/\&([^\<\>\&]*?)\;/mo) do + "" + end + data.gsub!(/\&/o) do + doxmlreport("&",verbose) + end + data.gsub!(/\/o) do + doxmlreport("&#{$1};",verbose) + end + + # remove funny < > + # + # data = " < 5% " + + 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 /",verbose) + else + "<#{tag}>" + end + end + + # remove funny < > + # + # data = " > 5% " + + data.gsub!(/<([^>].*?)>([^\>\<]*?)>/o) do + doxmlreport("<#{$1}>#{$2}>",verbose) + end + + return data + end + + # puts doxmlcleanup("") + # puts doxmlcleanup(" B&W ") + # puts doxmlcleanup(" < 5% ") + # puts doxmlcleanup(" > 5% ") + +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 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 '' + 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-- +

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 structured file. Actually, any logging that +is hooked into callbacks will be \XML\ by default.

+--ldx]]-- + +logs = logs or { } +logs.xml = logs.xml or { } +logs.tex = logs.tex or { } + +--[[ldx-- +

This looks pretty ugly but we need to speed things up a bit.

+--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("%s",category,format(fmt,...))) + else + write_nl(format("",category)) + end +end +function logs.xml.line(fmt,...) -- new + if fmt then + write_nl(format("%s",format(fmt,...))) + else + write_nl("") + 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("") end end +function logs.xml.push () if logs.level > 0 then tw("" ) end end + +function logs.xml.start_run() + write_nl("") + write_nl("") -- xmlns='www.pragma-ade.com/luatex/schemas/context-job.rng' + write_nl("") +end + +function logs.xml.stop_run() + write_nl("") +end + +function logs.xml.start_page_number() + write_nl(format("

") + write_nl("") +end + +function logs.xml.report_output_pages(p,b) + write_nl(format("", p)) + write_nl(format("", b)) + write_nl("") +end + +function logs.xml.report_output_log() +end + +function logs.xml.report_tex_stat(k,v) + texiowrite_nl("log",""..tostring(v).."") +end + +local level = 0 + +function logs.xml.show_open(name) + level = level + 1 + texiowrite_nl(format("",level,name)) +end + +function logs.xml.show_close(name) + texiowrite(" ") + level = level - 1 +end + +function logs.xml.show_load(name) + texiowrite_nl(format("",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: + 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-- +

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.

+ + +TEXMFCACHE=$TMP;$TEMP;$TMPDIR;$TEMPDIR;$HOME;$TEXMFVAR;$VARTEXMF;. + + +

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.

+--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-- +

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).

+ +

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.

+ +

Examples of usage can be found in the font related code.

+--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-- +

This file is used when we want the input handlers to behave like +kpsewhich. What to do with the following:

+ + +{$SELFAUTOLOC,$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,}/web2c} +$SELFAUTOLOC : /usr/tex/bin/platform +$SELFAUTODIR : /usr/tex/bin +$SELFAUTOPARENT : /usr/tex + + +

How about just forgetting about them?

+--ldx]]-- + +local suffixes = resolvers.suffixes +local formats = resolvers.formats + +suffixes['gf'] = { 'gf' } +suffixes['pk'] = { '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-- +

If you wondered abou tsome of the previous mappings, how about +the next bunch:

+--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 .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 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 '' + 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-- +

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 based one. +The find based parser can be found in l-xml-edu.lua along with other older code.

+ +

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.

+--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-- +

First a hack to enable namespace resolving. A namespace is characterized by +a . The following function associates a namespace prefix with a +pattern. We use , 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.

+--ldx]]-- + +xml.xmlns = xml.xmlns or { } + +local check = P(false) +local parse = check + +--[[ldx-- +

The next function associates a namespace prefix with an . This +normally happens independent of parsing.

+ + +xml.registerns("mml","mathml") + +--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-- +

The next function also registers a namespace, but this time we map a +given namespace prefix onto a registered one, using the given +. This used for attributes like xmlns:m.

+ + +xml.checkns("m","http://www.w3.org/mathml") + +--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-- +

Next we provide a way to turn an into a registered +namespace. This used for the xmlns attribute.

+ + +resolvedns = xml.resolvens("http://www.w3.org/mathml") + + +This returns mml. +--ldx]]-- + +function xml.resolvens(url) + return lpegmatch(parse,lower(url)) or "" +end + +--[[ldx-- +

A namespace in an element can be remapped onto the registered +one efficiently by using the xml.xmlns table.

+--ldx]]-- + +--[[ldx-- +

This version uses . 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 files that +took 12.5 seconds to load (1.5 for file io and the rest for tree building). With +the implementation we got that down to less 7.3 seconds. Loading the 14 + interface definition files (2.6 meg) went down from 1.05 seconds to 0.55.

+ +

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 + code to it.

+ + + + + + + + + + +

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:

+ + +local x = xml.convert(somestring) + + +

An optional second boolean argument tells this function not to create a root +element.

+ +

Valid entities are:

+ + + + + + +--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] = "&", + [42] = """, + [47] = "'", + [74] = "<", + [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(" & + 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-- +

Packaging data in an xml like table is done with the following +function. Maybe it will go away (when not used).

+--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-- +

We cannot load an from a filehandle so we need to load +the whole file first. The function accepts a string representing +a filename or a file handle.

+--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-- +

When we inject new elements, we need to convert strings to +valid trees, which is what the next function does.

+--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-- +

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!

+--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-- +

In 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.

+--ldx]]-- + +-- todo: add 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-- +

At the cost of some 25% runtime overhead you can first convert the tree to a string +and then handle the lot.

+--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("") + 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("") + else + if ats then + handle("<",etg," ",concat(ats," "),"/>") + else + handle("<",etg,"/>") + end + end + end +end + +local function verbose_pi(e,handlers) + handlers.handle("") +end + +local function verbose_comment(e,handlers) + handlers.handle("") +end + +local function verbose_cdata(e,handlers) + handlers.handle("") +end + +local function verbose_doctype(e,handlers) + handlers.handle("") +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-- +

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):

+ + +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 + + +

Beware, these were timing with the old routine but measurements will not be that +much different I guess.

+--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-- +

The next function operated on the content only and needs a handle function +that accepts a string.

+--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-- +

A few helpers:

+--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-- +

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:

+--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-- +

The next helper assigns a tree (or string). Usage:

+ + +dt[k] = xml.assign(root) or xml.assign(dt,k,root) + +--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-- +

The next helper assigns a tree (or string). Usage:

+ +xml.tocdata(e) +xml.tocdata(e,"error") + +--ldx]]-- + +function xml.tocdata(e,wrapper) + local whatever = xmltostring(e.dt) + if wrapper then + whatever = format("<%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-- +

This module can be used stand alone but also inside 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.

+

If I can get in the mood I will make a variant that is XSLT compliant +but I wonder if it makes sense.

+--ldx]]-- + +--[[ldx-- +

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 we also need +this module for process management, like handling and +files.

+ + +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) + +--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-- +

We've now arrived at an interesting part: accessing the tree using a subset +of and since we're not compatible we call it . We +will explain more about its usage in other documents.

+--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-- +

This is the main filter function. It returns whatever is asked for.

+--ldx]]-- + +function xml.filter(root,pattern) -- no longer funny attribute handling here + return parse_apply({ root },pattern) +end + +--[[ldx-- +

Often using an iterators looks nicer in the code than passing handler +functions. The book describes how to use coroutines for that +purpose (). This permits +code like:

+ + +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 + +--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-- +

The following helper functions best belong to the lxml-ini +module. Some are here because we need then in the mk +document and other manuals, others came up when playing with +this module. Since this module is also used in we've +put them here instead of loading mode modules there then needed.

+--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 = { ['&'] = '&', ['<'] = '<', ['>'] = '>', ['"'] = '"' } +--~ 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("<")/"<" + S(">")/">" + S("&")/"&" + 1)^0) +local normal = (1 - S("<&>"))^0 +local special = P("<")/"<" + P(">")/">" + P("&")/"&" +local escaped = Cs(normal * (special * normal)^0) + +-- 100 * 1000 * "oeps< oeps> oeps&" : gsub:lpeg == 0153:0280:0151:0080 (last one by roberto) + +local normal = (1 - S"&")^0 +local special = P("<")/"<" + P(">")/">" + P("&")/"&" +local unescaped = Cs(normal * (special * normal)^0) + +-- 100 * 5000 * "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-- +

The following functions collect elements and texts.

+--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-- +

We've now arrived at the functions that manipulate the tree.

+--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-- +

Here are a few synonyms.

+--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-- +

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 structured file. Actually, any logging that +is hooked into callbacks will be \XML\ by default.

+--ldx]]-- + +logs = logs or { } +logs.xml = logs.xml or { } +logs.tex = logs.tex or { } + +--[[ldx-- +

This looks pretty ugly but we need to speed things up a bit.

+--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("%s",category,format(fmt,...))) + else + write_nl(format("",category)) + end +end +function logs.xml.line(fmt,...) -- new + if fmt then + write_nl(format("%s",format(fmt,...))) + else + write_nl("") + 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("") end end +function logs.xml.push () if logs.level > 0 then tw("" ) end end + +function logs.xml.start_run() + write_nl("") + write_nl("") -- xmlns='www.pragma-ade.com/luatex/schemas/context-job.rng' + write_nl("") +end + +function logs.xml.stop_run() + write_nl("") +end + +function logs.xml.start_page_number() + write_nl(format("

") + write_nl("") +end + +function logs.xml.report_output_pages(p,b) + write_nl(format("", p)) + write_nl(format("", b)) + write_nl("") +end + +function logs.xml.report_output_log() +end + +function logs.xml.report_tex_stat(k,v) + texiowrite_nl("log",""..tostring(v).."") +end + +local level = 0 + +function logs.xml.show_open(name) + level = level + 1 + texiowrite_nl(format("",level,name)) +end + +function logs.xml.show_close(name) + texiowrite(" ") + level = level - 1 +end + +function logs.xml.show_load(name) + texiowrite_nl(format("",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: + 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-- +

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.

+ + +TEXMFCACHE=$TMP;$TEMP;$TMPDIR;$TEMPDIR;$HOME;$TEXMFVAR;$VARTEXMF;. + + +

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.

+--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-- +

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).

+ +

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.

+ +

Examples of usage can be found in the font related code.

+--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-- +

This file is used when we want the input handlers to behave like +kpsewhich. What to do with the following:

+ + +{$SELFAUTOLOC,$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,}/web2c} +$SELFAUTOLOC : /usr/tex/bin/platform +$SELFAUTODIR : /usr/tex/bin +$SELFAUTOPARENT : /usr/tex + + +

How about just forgetting about them?

+--ldx]]-- + +local suffixes = resolvers.suffixes +local formats = resolvers.formats + +suffixes['gf'] = { 'gf' } +suffixes['pk'] = { '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-- +

If you wondered abou tsome of the previous mappings, how about +the next bunch:

+--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- + fullname = mtxprefix .. filename + fullname = found(fullname) or resolvers.find_file(fullname) + if fullname and fullname ~= "" then + return fullname + end + -- context namespace, mtx-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- + 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 + 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 +#include +#include + +//#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 + +__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 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 '' + 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-- +

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 structured file. Actually, any logging that +is hooked into callbacks will be \XML\ by default.

+--ldx]]-- + +logs = logs or { } +logs.xml = logs.xml or { } +logs.tex = logs.tex or { } + +--[[ldx-- +

This looks pretty ugly but we need to speed things up a bit.

+--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("%s",category,format(fmt,...))) + else + write_nl(format("",category)) + end +end +function logs.xml.line(fmt,...) -- new + if fmt then + write_nl(format("%s",format(fmt,...))) + else + write_nl("") + 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("") end end +function logs.xml.push () if logs.level > 0 then tw("" ) end end + +function logs.xml.start_run() + write_nl("") + write_nl("") -- xmlns='www.pragma-ade.com/luatex/schemas/context-job.rng' + write_nl("") +end + +function logs.xml.stop_run() + write_nl("") +end + +function logs.xml.start_page_number() + write_nl(format("

") + write_nl("") +end + +function logs.xml.report_output_pages(p,b) + write_nl(format("", p)) + write_nl(format("", b)) + write_nl("") +end + +function logs.xml.report_output_log() +end + +function logs.xml.report_tex_stat(k,v) + texiowrite_nl("log",""..tostring(v).."") +end + +local level = 0 + +function logs.xml.show_open(name) + level = level + 1 + texiowrite_nl(format("",level,name)) +end + +function logs.xml.show_close(name) + texiowrite(" ") + level = level - 1 +end + +function logs.xml.show_load(name) + texiowrite_nl(format("",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: + 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-- +

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.

+ + +TEXMFCACHE=$TMP;$TEMP;$TMPDIR;$TEMPDIR;$HOME;$TEXMFVAR;$VARTEXMF;. + + +

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.

+--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-- +

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).

+ +

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.

+ +

Examples of usage can be found in the font related code.

+--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-- +

This file is used when we want the input handlers to behave like +kpsewhich. What to do with the following:

+ + +{$SELFAUTOLOC,$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,}/web2c} +$SELFAUTOLOC : /usr/tex/bin/platform +$SELFAUTODIR : /usr/tex/bin +$SELFAUTOPARENT : /usr/tex + + +

How about just forgetting about them?

+--ldx]]-- + +local suffixes = resolvers.suffixes +local formats = resolvers.formats + +suffixes['gf'] = { 'gf' } +suffixes['pk'] = { '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-- +

If you wondered abou tsome of the previous mappings, how about +the next bunch:

+--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 .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 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 '' + 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-- +

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 based one. +The find based parser can be found in l-xml-edu.lua along with other older code.

+ +

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.

+--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-- +

First a hack to enable namespace resolving. A namespace is characterized by +a . The following function associates a namespace prefix with a +pattern. We use , 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.

+--ldx]]-- + +xml.xmlns = xml.xmlns or { } + +local check = P(false) +local parse = check + +--[[ldx-- +

The next function associates a namespace prefix with an . This +normally happens independent of parsing.

+ + +xml.registerns("mml","mathml") + +--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-- +

The next function also registers a namespace, but this time we map a +given namespace prefix onto a registered one, using the given +. This used for attributes like xmlns:m.

+ + +xml.checkns("m","http://www.w3.org/mathml") + +--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-- +

Next we provide a way to turn an into a registered +namespace. This used for the xmlns attribute.

+ + +resolvedns = xml.resolvens("http://www.w3.org/mathml") + + +This returns mml. +--ldx]]-- + +function xml.resolvens(url) + return lpegmatch(parse,lower(url)) or "" +end + +--[[ldx-- +

A namespace in an element can be remapped onto the registered +one efficiently by using the xml.xmlns table.

+--ldx]]-- + +--[[ldx-- +

This version uses . 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 files that +took 12.5 seconds to load (1.5 for file io and the rest for tree building). With +the implementation we got that down to less 7.3 seconds. Loading the 14 + interface definition files (2.6 meg) went down from 1.05 seconds to 0.55.

+ +

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 + code to it.

+ + + + + + + + + + +

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:

+ + +local x = xml.convert(somestring) + + +

An optional second boolean argument tells this function not to create a root +element.

+ +

Valid entities are:

+ + + + + + +--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] = "&", + [42] = """, + [47] = "'", + [74] = "<", + [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(" & + 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-- +

Packaging data in an xml like table is done with the following +function. Maybe it will go away (when not used).

+--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-- +

We cannot load an from a filehandle so we need to load +the whole file first. The function accepts a string representing +a filename or a file handle.

+--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-- +

When we inject new elements, we need to convert strings to +valid trees, which is what the next function does.

+--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-- +

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!

+--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-- +

In 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.

+--ldx]]-- + +-- todo: add 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-- +

At the cost of some 25% runtime overhead you can first convert the tree to a string +and then handle the lot.

+--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("") + 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("") + else + if ats then + handle("<",etg," ",concat(ats," "),"/>") + else + handle("<",etg,"/>") + end + end + end +end + +local function verbose_pi(e,handlers) + handlers.handle("") +end + +local function verbose_comment(e,handlers) + handlers.handle("") +end + +local function verbose_cdata(e,handlers) + handlers.handle("") +end + +local function verbose_doctype(e,handlers) + handlers.handle("") +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-- +

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):

+ + +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 + + +

Beware, these were timing with the old routine but measurements will not be that +much different I guess.

+--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-- +

The next function operated on the content only and needs a handle function +that accepts a string.

+--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-- +

A few helpers:

+--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-- +

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:

+--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-- +

The next helper assigns a tree (or string). Usage:

+ + +dt[k] = xml.assign(root) or xml.assign(dt,k,root) + +--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-- +

The next helper assigns a tree (or string). Usage:

+ +xml.tocdata(e) +xml.tocdata(e,"error") + +--ldx]]-- + +function xml.tocdata(e,wrapper) + local whatever = xmltostring(e.dt) + if wrapper then + whatever = format("<%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-- +

This module can be used stand alone but also inside 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.

+

If I can get in the mood I will make a variant that is XSLT compliant +but I wonder if it makes sense.

+--ldx]]-- + +--[[ldx-- +

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 we also need +this module for process management, like handling and +files.

+ + +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) + +--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-- +

We've now arrived at an interesting part: accessing the tree using a subset +of and since we're not compatible we call it . We +will explain more about its usage in other documents.

+--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-- +

This is the main filter function. It returns whatever is asked for.

+--ldx]]-- + +function xml.filter(root,pattern) -- no longer funny attribute handling here + return parse_apply({ root },pattern) +end + +--[[ldx-- +

Often using an iterators looks nicer in the code than passing handler +functions. The book describes how to use coroutines for that +purpose (). This permits +code like:

+ + +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 + +--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-- +

The following helper functions best belong to the lxml-ini +module. Some are here because we need then in the mk +document and other manuals, others came up when playing with +this module. Since this module is also used in we've +put them here instead of loading mode modules there then needed.

+--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 = { ['&'] = '&', ['<'] = '<', ['>'] = '>', ['"'] = '"' } +--~ 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("<")/"<" + S(">")/">" + S("&")/"&" + 1)^0) +local normal = (1 - S("<&>"))^0 +local special = P("<")/"<" + P(">")/">" + P("&")/"&" +local escaped = Cs(normal * (special * normal)^0) + +-- 100 * 1000 * "oeps< oeps> oeps&" : gsub:lpeg == 0153:0280:0151:0080 (last one by roberto) + +local normal = (1 - S"&")^0 +local special = P("<")/"<" + P(">")/">" + P("&")/"&" +local unescaped = Cs(normal * (special * normal)^0) + +-- 100 * 5000 * "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-- +

The following functions collect elements and texts.

+--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-- +

We've now arrived at the functions that manipulate the tree.

+--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-- +

Here are a few synonyms.

+--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-- +

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 structured file. Actually, any logging that +is hooked into callbacks will be \XML\ by default.

+--ldx]]-- + +logs = logs or { } +logs.xml = logs.xml or { } +logs.tex = logs.tex or { } + +--[[ldx-- +

This looks pretty ugly but we need to speed things up a bit.

+--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("%s",category,format(fmt,...))) + else + write_nl(format("",category)) + end +end +function logs.xml.line(fmt,...) -- new + if fmt then + write_nl(format("%s",format(fmt,...))) + else + write_nl("") + 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("") end end +function logs.xml.push () if logs.level > 0 then tw("" ) end end + +function logs.xml.start_run() + write_nl("") + write_nl("") -- xmlns='www.pragma-ade.com/luatex/schemas/context-job.rng' + write_nl("") +end + +function logs.xml.stop_run() + write_nl("") +end + +function logs.xml.start_page_number() + write_nl(format("

") + write_nl("") +end + +function logs.xml.report_output_pages(p,b) + write_nl(format("", p)) + write_nl(format("", b)) + write_nl("") +end + +function logs.xml.report_output_log() +end + +function logs.xml.report_tex_stat(k,v) + texiowrite_nl("log",""..tostring(v).."") +end + +local level = 0 + +function logs.xml.show_open(name) + level = level + 1 + texiowrite_nl(format("",level,name)) +end + +function logs.xml.show_close(name) + texiowrite(" ") + level = level - 1 +end + +function logs.xml.show_load(name) + texiowrite_nl(format("",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: + 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-- +

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.

+ + +TEXMFCACHE=$TMP;$TEMP;$TMPDIR;$TEMPDIR;$HOME;$TEXMFVAR;$VARTEXMF;. + + +

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.

+--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-- +

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).

+ +

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.

+ +

Examples of usage can be found in the font related code.

+--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-- +

This file is used when we want the input handlers to behave like +kpsewhich. What to do with the following:

+ + +{$SELFAUTOLOC,$SELFAUTODIR,$SELFAUTOPARENT}{,{/share,}/texmf{-local,}/web2c} +$SELFAUTOLOC : /usr/tex/bin/platform +$SELFAUTODIR : /usr/tex/bin +$SELFAUTOPARENT : /usr/tex + + +

How about just forgetting about them?

+--ldx]]-- + +local suffixes = resolvers.suffixes +local formats = resolvers.formats + +suffixes['gf'] = { 'gf' } +suffixes['pk'] = { '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-- +

If you wondered abou tsome of the previous mappings, how about +the next bunch:

+--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- + fullname = mtxprefix .. filename + fullname = found(fullname) or resolvers.find_file(fullname) + if fullname and fullname ~= "" then + return fullname + end + -- context namespace, mtx-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- + 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 + 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