if not modules then modules = { } end modules ['data-exp'] = { version = 1.001, comment = "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, gmatch, lower, char, sub = string.format, string.find, string.gmatch, string.lower, string.char, string.sub local concat, sort = table.concat, table.sort local lpegmatch, lpegpatterns = lpeg.match, lpeg.patterns local Ct, Cs, Cc, P, C, S = lpeg.Ct, lpeg.Cs, lpeg.Cc, lpeg.P, lpeg.C, lpeg.S local type, next = type, next local ostype = os.type local collapsepath = file.collapsepath local trace_locating = false trackers.register("resolvers.locating", function(v) trace_locating = v end) local trace_expansions = false trackers.register("resolvers.expansions", function(v) trace_expansions = v end) local report_expansions = logs.reporter("resolvers","expansions") local resolvers = resolvers -- As this bit of code is somewhat special it gets its own module. After -- all, when working on the main resolver code, I don't want to scroll -- past this every time. See data-obs.lua for the gsub variant. local function f_first(a,b) local t, n = { }, 0 for s in gmatch(b,"[^,]+") do n = n + 1 ; t[n] = a .. s end return concat(t,",") end local function f_second(a,b) local t, n = { }, 0 for s in gmatch(a,"[^,]+") do n = n + 1 ; t[n] = s .. b end return concat(t,",") end -- kpsewhich --expand-braces '{a,b}{c,d}' -- ac:bc:ad:bd -- old {a,b}{c,d} => ac ad bc bd -- -- local function f_both(a,b) -- local t, n = { }, 0 -- for sa in gmatch(a,"[^,]+") do -- for sb in gmatch(b,"[^,]+") do -- n = n + 1 ; t[n] = sa .. sb -- end -- end -- return concat(t,",") -- end -- -- new {a,b}{c,d} => ac bc ad bd local function f_both(a,b) local t, n = { }, 0 for sb in gmatch(b,"[^,]+") do -- and not sa for sa in gmatch(a,"[^,]+") do -- sb n = n + 1 ; t[n] = sa .. sb end end return concat(t,",") end local left = P("{") local right = P("}") local var = P((1 - S("{}" ))^0) local set = P((1 - S("{},"))^0) local other = P(1) local l_first = Cs( ( Cc("{") * (C(set) * left * C(var) * right / f_first) * Cc("}") + other )^0 ) local l_second = Cs( ( Cc("{") * (left * C(var) * right * C(set) / f_second) * Cc("}") + other )^0 ) local l_both = Cs( ( Cc("{") * (left * C(var) * right * left * C(var) * right / f_both) * Cc("}") + other )^0 ) local l_rest = Cs( ( left * var * (left/"") * var * (right/"") * var * right + other )^0 ) local stripper_1 = lpeg.stripper ("{}@") local replacer_1 = lpeg.replacer { { ",}", ",@}" }, { "{,", "{@," }, } local function splitpathexpr(str, newlist, validate) -- I couldn't resist lpegging it (nice exercise). if trace_expansions then report_expansions("expanding variable '%s'",str) end local t, ok, done = newlist or { }, false, false local n = #t str = lpegmatch(replacer_1,str) repeat local old = str repeat local old = str str = lpegmatch(l_first, str) until old == str repeat local old = str str = lpegmatch(l_second,str) until old == str repeat local old = str str = lpegmatch(l_both, str) until old == str repeat local old = str str = lpegmatch(l_rest, str) until old == str until old == str -- or not find(str,"{") str = lpegmatch(stripper_1,str) if validate then for s in gmatch(str,"[^,]+") do s = validate(s) if s then n = n + 1 t[n] = s end end else for s in gmatch(str,"[^,]+") do n = n + 1 t[n] = s end end if trace_expansions then for k=1,#t do report_expansions("% 4i: %s",k,t[k]) end end return t end -- We could make the previous one public. local function validate(s) s = collapsepath(s) -- already keeps the trailing / and // return s ~= "" and not find(s,"^!*unset/*$") and s end resolvers.validatedpath = validate -- keeps the trailing // function resolvers.expandedpathfromlist(pathlist) local newlist = { } for k=1,#pathlist do splitpathexpr(pathlist[k],newlist,validate) end return newlist 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} local cleanup = lpeg.replacer { { "!" , "" }, { "\\" , "/" }, } function resolvers.cleanpath(str) -- tricky, maybe only simple paths local doslashes = (P("\\")/"/" + 1)^0 local donegation = (P("!") /"" )^0 local homedir = lpegmatch(Cs(donegation * doslashes),environment.homedir or "") if homedir == "~" or homedir == "" or not lfs.isdir(homedir) then if trace_expansions then report_expansions("no home dir set, ignoring dependent paths") end function resolvers.cleanpath(str) if not str or find(str,"~") then return "" -- special case else return lpegmatch(cleanup,str) end end else local dohome = ((P("~")+P("$HOME"))/homedir)^0 local cleanup = Cs(donegation * dohome * doslashes) function resolvers.cleanpath(str) return str and lpegmatch(cleanup,str) or "" end end return resolvers.cleanpath(str) end -- print(resolvers.cleanpath("")) -- print(resolvers.cleanpath("!")) -- print(resolvers.cleanpath("~")) -- print(resolvers.cleanpath("~/test")) -- print(resolvers.cleanpath("!~/test")) -- print(resolvers.cleanpath("~/test~test")) -- This one strips quotes and funny tokens. local expandhome = P("~") / "$HOME" -- environment.homedir local dodouble = P('"')/"" * (expandhome + (1 - P('"')))^0 * P('"')/"" local dosingle = P("'")/"" * (expandhome + (1 - P("'")))^0 * P("'")/"" local dostring = (expandhome + 1 )^0 local stripper = Cs( lpegpatterns.unspacer * (dosingle + dodouble + dostring) * lpegpatterns.unspacer ) function resolvers.checkedvariable(str) -- assumes str is a string return type(str) == "string" and lpegmatch(stripper,str) or str end -- The path splitter: -- 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 cache = { } ----- splitter = lpeg.tsplitat(S(ostype == "windows" and ";" or ":;")) -- maybe add , local splitter = lpeg.tsplitat(";") -- as we move towards urls, prefixes and use tables we no longer do : local backslashswapper = lpeg.replacer("\\","/") local function splitconfigurationpath(str) -- beware, this can be either a path or a { specification } if str then local found = cache[str] if not found then if str == "" then found = { } else local split = lpegmatch(splitter,lpegmatch(backslashswapper,str)) -- can be combined found = { } local noffound = 0 for i=1,#split do local s = split[i] if not find(s,"^{*unset}*") then noffound = noffound + 1 found[noffound] = s end end if trace_expansions then report_expansions("splitting path specification '%s'",str) for k=1,noffound do report_expansions("% 4i: %s",k,found[k]) end end cache[str] = found end end return found end end resolvers.splitconfigurationpath = splitconfigurationpath function resolvers.splitpath(str) if type(str) == 'table' then return str else return splitconfigurationpath(str) end end function resolvers.joinpath(str) if type(str) == 'table' then return file.joinpath(str) else return str end end -- The next function scans directories and returns a hash where the -- entries are either strings or tables. -- starting with . or .. etc or funny char --~ local l_forbidden = S("~`!#$%^&*()={}[]:;\"\'||\\/<>,?\n\r\t") --~ local l_confusing = P(" ") --~ local l_character = lpegpatterns.utf8 --~ local l_dangerous = P(".") --~ local l_normal = (l_character - l_forbidden - l_confusing - l_dangerous) * (l_character - l_forbidden - l_confusing^2)^0 * P(-1) --~ ----- l_normal = l_normal * Cc(true) + Cc(false) --~ local function test(str) --~ print(str,lpegmatch(l_normal,str)) --~ end --~ test("ヒラギノ明朝 Pro W3") --~ test("..ヒラギノ明朝 Pro W3") --~ test(":ヒラギノ明朝 Pro W3;") --~ test("ヒラギノ明朝 /Pro W3;") --~ test("ヒラギノ明朝 Pro W3") -- a lot of this caching can be stripped away when we have ssd's everywhere -- -- we could cache all the (sub)paths here if needed local attributes, directory = lfs.attributes, lfs.dir local weird = P(".")^1 + lpeg.anywhere(S("~`!#$%^&*()={}[]:;\"\'||<>,?\n\r\t")) local timer = { } local scanned = { } local nofscans = 0 local scancache = { } local function scan(files,spec,path,n,m,r) local full = (path == "" and spec) or (spec .. path .. '/') local dirs = { } local nofdirs = 0 for name in directory(full) do if not lpegmatch(weird,name) then local mode = attributes(full..name,'mode') if mode == 'file' 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 elseif mode == 'directory' then m = m + 1 nofdirs = nofdirs + 1 if path ~= "" then dirs[nofdirs] = path..'/'..name else dirs[nofdirs] = name end end end end if nofdirs > 0 then sort(dirs) for i=1,nofdirs do files, n, m, r = scan(files,spec,dirs[i],n,m,r) end end scancache[sub(full,1,-2)] = files return files, n, m, r end local fullcache = { } function resolvers.scanfiles(path,branch,usecache) statistics.starttiming(timer) local realpath = resolvers.resolve(path) -- no shortcut if usecache then local files = fullcache[realpath] if files then if trace_locating then report_expansions("using caches scan of path '%s', branch '%s'",path,branch or path) end return files end end if trace_locating then report_expansions("scanning path '%s', branch '%s'",path,branch or path) end local files, n, m, r = scan({ },realpath .. '/',"",0,0,0) files.__path__ = path -- can be selfautoparent:texmf-whatever files.__files__ = n files.__directories__ = m files.__remappings__ = r if trace_locating then report_expansions("%s files found on %s directories with %s uppercase remappings",n,m,r) end if usecache then scanned[#scanned+1] = realpath fullcache[realpath] = files end nofscans = nofscans + 1 statistics.stoptiming(timer) return files end local function simplescan(files,spec,path) -- first match only, no map and such local full = (path == "" and spec) or (spec .. path .. '/') local dirs = { } local nofdirs = 0 for name in directory(full) do if not lpegmatch(weird,name) then local mode = attributes(full..name,'mode') if mode == 'file' then if not files[name] then -- only first match files[name] = path end elseif mode == 'directory' then nofdirs = nofdirs + 1 if path ~= "" then dirs[nofdirs] = path..'/'..name else dirs[nofdirs] = name end end end end if nofdirs > 0 then sort(dirs) for i=1,nofdirs do files = simplescan(files,spec,dirs[i]) end end return files end local simplecache = { } local nofsharedscans = 0 function resolvers.simplescanfiles(path,branch,usecache) statistics.starttiming(timer) local realpath = resolvers.resolve(path) -- no shortcut if usecache then local files = simplecache[realpath] if not files then files = scancache[realpath] if files then nofsharedscans = nofsharedscans + 1 end end if files then if trace_locating then report_expansions("using caches scan of path '%s', branch '%s'",path,branch or path) end return files end end if trace_locating then report_expansions("scanning path '%s', branch '%s'",path,branch or path) end local files = simplescan({ },realpath .. '/',"") if trace_locating then report_expansions("%s files found",table.count(files)) end if usecache then scanned[#scanned+1] = realpath simplecache[realpath] = files end nofscans = nofscans + 1 statistics.stoptiming(timer) return files end function resolvers.scandata() table.sort(scanned) return { n = nofscans, shared = nofsharedscans, time = statistics.elapsedtime(timer), paths = scanned, } end --~ print(table.serialize(resolvers.scanfiles("t:/sources")))