-- [ʞ] parvan.lua
-- ? tool for maintaining and searching dictionaries
-- [ CONTROL CLASS: GREEN ]
-- [ CODEWORDS: - GENERAL ACCESS - ]
-- [ CONTROLLING AUTHORITY: - INTERDIRECTORIAL -
-- < Commission for Defense Communication >
-- +WCO Worlds Culture Overdirectorate
-- SSD Social Sciences Directorate
-- ELS External Linguistics Subdirectorate
-- +WSO Worlds Security Overdirectorate
-- EID External Influence Directorate ]
local function implies(a,b) return a==b or not(a) end
local function map(lst,fn)
local new = {}
for k,v in pairs(lst) do
local nv, nk = fn(v,k)
new[nk or k] = nv
end
return new
end
local function mapD(lst,fn) --destructive
-- WARNING: this will not work if nk names an existing key!
for k,v in pairs(lst) do
local nv, nk = fn(v,k)
if nk == nil or k == nk then
lst[k] = nv
else
lst[k] = nil
lst[nk] = nv
end
end
return lst
end
local function pushi(dest, idx, src, ...)
if not src then return end
dest[idx] = src
pushi(dest, idx+1, ...)
end
local function push(dest, ...) pushi(dest,#dest+1,...) end
local function cons(car, cdr)
local new = {car}
for k,v in ipairs(cdr) do new[k+1] = v end
return new
end
local function tcatD(dest, ...)
local i = #dest
local function iter(src, ...)
if src == nil then return end
local sc = #src
for j=1,sc do dest[i+j] = src[j] end
i = i + sc
iter(...)
end
iter(...)
end
local function fastDelete(table,idx)
-- delete without preserving table order
local l = #table
table[idx] = table[l]
table[l] = nil
return table
end
local function tcat(...)
local new = {}
tcatD(new, ...)
return new
end
local ansi = {
levels = {
plain = 0;
ansi = 1;
color = 2;
color8b = 3;
color24b = 4;
};
}
ansi.seqs = {
br = {ansi.levels.ansi, "[1m", "[22m"};
hl = {ansi.levels.ansi, "[7m", "[27m"};
ul = {ansi.levels.ansi, "[4m", "[24m"};
em = {ansi.levels.ansi, "[3m", "[23m"};
};
function ansi.termclass(fd) -- awkwardly emulate isatty
if fd:seek('cur',0) then
return ansi.levels.plain
end
if os.getenv('COLORTERM') == 'truecolor' then
return ansi.levels.color24b
end
local term = os.getenv('TERM')
if term then
if term:match '-256color' then
return ansi.levels.color8b
elseif term:match '-color' then
return ansi.levels.color
else
return ansi.levels.ansi
end
end
return ansi.levels.plain
end
function ansi.formatter(fd)
local cl = ansi.termclass(fd)
local id = function(...) return ... end
local esc = '\27'
local f = {}
for k,v in pairs(ansi.seqs) do
local lvl, on, off = table.unpack(v)
if lvl <= cl then
f[k] = function(s)
return esc..on .. s .. esc..off
end
else f[k] = id end
end
local function ftoi(r,g,b)
return math.ceil(r*0xff),
math.ceil(g*0xff),
math.ceil(b*0xff)
end
local reset = "\27[39m"
function f.color(str, n, br)
return string.format("\27[%s%cm",
(bg and 4 or 3) +
(br and 6 or 0), 0x30+n)
.. str .. reset
end
function f.resetLine()
return '\27[1K\13'
end
if cl == ansi.levels.color24b then
function f.rgb(str, r,g,b, bg)
return string.format("\27[%c8;2;%u;%u;%um", bg and 0x34 or 0x33,
ftoi(r,g,b)) .. str .. reset
end
elseif cl == ansi.levels.color8b then
function f.rgb(str, r,g,b, bg)
local code = 16 + math.floor(r * 5)*36 + math.floor(g * 5)*6 + math.floor(b * 6)
return string.format("\27[%c8;5;%um", bg and 0x34 or 0x33, code)
.. str .. reset
end
elseif cl == ansi.levels.color then
function f.rgb(str, r,g,b, bg)
local code = 0x30 + 1 -- TODO
return string.format("\27[%c%cm", bg and 0x34 or 0x33, code)
.. str .. reset
end
else
function f.rgb(s) return s end
function f.color(s) return s end
function f.resetLine() return '' end
end
return f
end
local function dump(v,pfx,cyc,ismeta)
pfx = pfx or ''
cyc = cyc or {}
local np = pfx .. ' '
if type(v) == 'table' then
if cyc[v] then return '<...>' else cyc[v] = true end
end
if type(v) == 'string' then
return string.format('%q', v)
elseif type(v) == 'table' then
local str = ''
for k,v in pairs(v) do
local tkey, tval = dump(k,np,cyc), dump(v,np,cyc)
str = str .. string.format('%s[%s] = %s\n', np, tkey,tval)
end
local meta = ''
if getmetatable(v) then
meta = dump(getmetatable(v),pfx,cyc,true) .. '::'
end
if ismeta then
return string.format('%s<|\n%s%s|>',meta,str,pfx)
else
return meta..'{\n' .. str .. pfx .. '}'
end
else
return string.format('%s', v)
end
end
local struct = {
__call = function(s,...) return s:mk(...) end;
}
function struct:mk(s)
function s.is(o) return getmetatable(o) == s end
return setmetatable(s, self)
end
setmetatable(struct, struct)
local stream = struct {
__index = {
next = function(self, f)
local flds = {string.unpack('<'..f, self.data, self.index)}
self.index = flds[#flds]
flds[#flds] = nil
return table.unpack(flds)
end;
};
mk = function(self, str)
return setmetatable({
data = str;
index = 1;
}, self)
end;
}
local fmt = {}
local userError = struct {
__tostring = function(self) return self.msg end;
mk = function(self, s) return setmetatable({msg=s},self) end;
}
local function id10t(...)
error(userError(string.format(...)),0)
end
local packer,unpacker =
function(f) return function(...) return string.pack ("<"..f, ...) end end,
function(f) return function( s ) return s:next (f) end end
local qpack = function(f) return {
encode = packer(f);
decode = unpacker(f);
} end
local parse, marshal
fmt.string = qpack "s4"
fmt.label = qpack "s2"
fmt.tag = qpack "s1"
fmt.u8 = qpack "I1"
fmt.u16 = qpack "I2"
fmt.u24 = qpack "I3"
fmt.u32 = qpack "I4"
fmt.path = {
-- encodes a FIXED path to an arbitrary type of object
encode = function(a)
local kind = 0
local vals = {}
if a.w then kind = 1
table.insert(vals, marshal(fmt.label, a.w))
if a.dn then kind = 2
table.insert(vals, marshal(fmt.u8, a.dn))
if a.mn then kind = 3
table.insert(vals, marshal(fmt.u8, a.mn))
if a.nn then kind = 4
table.insert(vals, marshal(fmt.u8, a.nn))
end
end
end
end
return marshal(fmt.u8,kind) .. table.concat(vals)
end;
decode = function(s)
local kind <const> = parse(fmt.u8, s)
local path = {}
local components <const> = {
{'w',fmt.label};
{'dn',fmt.u8};
{'mn',fmt.u8};
{'nn',fmt.u8};
}
for i=1,kind do
local label, ty = table.unpack(components[i])
path[label] = parse(ty,s)
end
return path
end;
}
fmt.list = function(t,ty) ty = ty or fmt.u32
return {
encode = function(a)
local vals = {marshal(ty, #a)}
for i=1,#a do
table.insert(vals, marshal(t, a[i]))
end
return table.concat(vals)
end;
decode = function(s)
local n = parse(ty, s)
local vals = {}
for i=1,n do
table.insert(vals, parse(t, s))
end
return vals
end;
}
end
fmt.map = function(from,to,ity)
local ent = fmt.list({
{'key', from},
{'val', to}
}, ity)
return {
encode = function(a)
local m = {}
for k,v in pairs(a) do
table.insert(m, {key=k, val=v})
end
return ent.encode(m)
end;
decode = function(s)
local lst = ent.decode(s)
local m = {}
for _,p in pairs(lst) do m[p.key] = p.val end
return m
end;
}
end
fmt.enum = function(...)
local vals,rmap = {...},{}
for k,v in pairs(vals) do rmap[v] = k-1 end
local ty = fmt.u8
if #vals > 0xffff then ty = fmt.u32 -- just in pathological case
elseif #vals > 0xff then ty = fmt.u16 end
return {
encode = function(a)
if not rmap[a] then error(string.format('"%s" is not part of enum "%s"', a, table.concat(vals,'","')),3) end
return marshal(ty, rmap[a])
end;
decode = function(s)
local n = parse(ty,s)
if (n+1) > #vals then error(string.format('enum "%s" does not have %u members', table.concat(vals,'","'),n),3) end
return vals[n+1]
end;
}
end
fmt.uid = fmt.u32
fmt.relatable = function(ty)
return tcat(ty,{
{'rels',fmt.list(fmt.uid,fmt.u16)};
})
end
fmt.note = {
{'kind', fmt.tag};
{'paras', fmt.list(fmt.string)};
}
fmt.example = {
{'quote',fmt.string};
{'src',fmt.label};
}
fmt.meaning = fmt.relatable {
{'lit', fmt.string};
{'examples', fmt.list(fmt.example,fmt.u8)};
{'notes', fmt.list(fmt.note,fmt.u8)};
}
fmt.phrase = fmt.relatable {
{'str',fmt.label};
{'means',fmt.list(fmt.meaning,fmt.u8)};
}
fmt.def = fmt.relatable {
{'part', fmt.u8};
{'branch', fmt.list(fmt.label,fmt.u8)};
{'means', fmt.list(fmt.meaning,fmt.u8)};
{'forms', fmt.map(fmt.u16,fmt.label,fmt.u16)};
{'phrases', fmt.list(fmt.phrase,fmt.u16)};
}
fmt.word = fmt.relatable {
{'defs', fmt.list(fmt.def,fmt.u8)};
}
fmt.dictHeader = {
{'lang', fmt.tag};
{'meta', fmt.string};
{'partsOfSpeech', fmt.list(fmt.tag,fmt.u16)};
{'inflectionForms', fmt.list({
{'name', fmt.tag};
{'abbrev', fmt.tag};
{'desc', fmt.string};
{'parts', fmt.list(fmt.tag,fmt.u8)};
-- which parts of speech does this form apply to?
-- leave empty if not relevant
},fmt.u16)};
}
fmt.relSet = {
{'uid', fmt.uid};
-- IDs are persistent random values so they can be used
-- as reliable identifiers even when merging exports in
-- a parvan-unaware VCS
{'kind', fmt.enum('syn','ant','met')};
-- membership is stored in individual objects, using a field
-- attached by the 'relatable' template
}
fmt.dict = {
{'header', fmt.dictHeader};
{'words', fmt.map(fmt.string,fmt.word)};
{'relsets', fmt.list(fmt.relSet)};
}
function marshal(ty, val)
if ty.encode then
return ty.encode(val)
end
local ac = {}
for idx,fld in ipairs(ty) do
local name, fty = table.unpack(fld)
table.insert(ac, marshal(fty,
assert(val[name],
string.format('marshalling error: missing field %s', name)
)
))
end
return table.concat(ac)
end
function parse(ty, stream)
if ty.decode then
return ty.decode(stream)
end
local obj = {}
for idx,fld in ipairs(ty) do
local name, fty = table.unpack(fld)
obj[name] = parse(fty, stream)
end
return obj
end
local function
atomizer()
local map = {}
local i = 1
return function(v)
if map[v] then return map[v] else
map[v] = i
i=i+1
return i-1
end
end, map
end
local function rebuildRelationCache(d)
-- (re)build a dictionary's relation cache; needed
-- at load time and whenever any changes to relsets
-- are made (unless they're simple enough to update
-- the cache directly by hand, but that's very eeeh)
local setMems = {} -- indexed by set id
local function scan(obj,path)
for _,v in pairs(obj.rels) do
setMems[v] = setMems[v] or {mems={}}
table.insert(setMems[v].mems, {path=path, obj=obj})
end
end
for wk,wv in pairs(d.words) do
scan(wv, {w=wk})
for dk,dv in pairs(wv.defs) do
scan(dv, {w=wk, dn=dk})
for mk,mv in pairs(dv.means) do
scan(mv, {w=wk, dn=dk, mn=mk})
end
for pk,pv in pairs(dv.phrases) do
scan(pv, {w=wk, dn=dk, pn=pk})
for mk,mv in pairs(pv.means) do
scan(mv, {w=wk, dn=dk, pn=pk, mn=mk})
end
end
end
end
for sk,sv in pairs(d.relsets) do
setMems[sv.uid] = setMems[sv.uid] or {}
setMems[sv.uid].set = sv
end
d._relCache = setMems
end
local function
writeDict(d)
local atomizePoS, posMap = atomizer()
for lit,w in pairs(d.words) do
for j,def in ipairs(w.defs) do
def.part = atomizePoS(def.part)
end
end
d.header.partsOfSpeech = {}
for v,i in pairs(posMap) do
d.header.partsOfSpeech[i] = v
end
return 'PV0\2'..marshal(fmt.dict, d)
end
local function
readDict(file)
local s = stream(file)
local magic = s:next 'c4'
if magic ~= 'PV0\2' then
id10t 'not a parvan file'
end
local d = parse(fmt.dict, s)
-- handle atoms
for lit,w in pairs(d.words) do
for j,def in ipairs(w.defs) do
def.part = d.header.partsOfSpeech[def.part]
end
end
-- create cachemaps for complex data structures to
-- enable faster lookup that would otherwise require
-- expensive scans
rebuildRelationCache(d)
return d
end
local function strwords(str) -- here be dragons
local wds = {}
local w = {}
local state, d, quo, dquo = 0,0
local function flush(n,final)
if next(w) or state ~= 0 and state < 10 then
table.insert(wds, utf8.char(table.unpack(w)))
w = {}
elseif final and state > 10 then
table.insert(wds, '\\')
end
state = n
quo = nil
dquo = nil
d = 0
end
local function isws(c)
return c == 0x20 or c == 0x09 or c == 0x0a
end
for p,cp in utf8.codes(str) do
if state == 0 then -- begin
if not(isws(cp)) then
if cp == 0x22 or cp == 0x27 then
quo = cp
elseif cp == 0x5b then -- boxquote
quo = 0x5d
dquo = 0x5b
elseif cp == 0x7b then -- curlquote
quo = 0x7d
dquo = 0x7b
elseif cp == 0x201c then -- fancyquote
quo = 0x201d
dquo = 0x201c
end
if quo then
state = 2
d = 1
elseif cp == 0x5c then -- escape
state = 11
else
state = 1
table.insert(w, cp)
end
end
elseif state == 1 then -- word
if isws(cp) then flush(0)
elseif cp == 0x5c then state = 11 else
table.insert(w,cp)
end
elseif state == 2 then -- (nested?) quote
if cp == 0x5c then state = 12
elseif cp == quo then
d = d - 1
if d == 0 then
flush(0)
else
table.insert(w,cp)
end
else
if cp == dquo then d = d + 1 end
table.insert(w,cp)
end
elseif state == 11 or state == 12 then -- escape
-- 12 = quote escape, 11 = raw escape
if cp == 0x63 then --n
table.insert(w,0x0a)
else
table.insert(w,cp)
end
state = state - 10
end
end
flush(nil,true)
return wds
end
local predicates
local function parsefilter(str)
local f = strwords(str)
if #f == 1 then return function(e) return predicates.lit.fn(e,f[1]) end end
if not predicates[f[1]] then
id10t('no such predicate %s',f[1])
else
local p = predicates[f[1]].fn
return function(e)
return p(e, table.unpack(f,2))
end
end
end
do
local function p_all(e,pred,...)
if pred == nil then return true end
pred = parsefilter(pred)
if not pred(e) then return false end
return p_all(e,...)
end;
local function p_any(e,pred,...)
if pred == nil then return false end
pred = parsefilter(pred)
if pred(e) then return true end
return p_any(e,...)
end;
local function p_none(e,pred,...)
if pred == nil then return true end
pred = parsefilter(pred)
if pred(e) then return false end
return p_none(e,...)
end;
local function p_some(e,count,pred,...)
if count == 0 then return true end
if pred == nil then return false end
pred = parsefilter(pred)
if pred(e) then
count = count-1
end
return p_some(e,count,...)
end;
local function prepScan(...)
local map = {}
local tgt = select('#',...)
for _,v in pairs{...} do map[v] = true end
return map,tgt
end
predicates = {
all = {
fn = p_all;
syntax = '<pred>…';
help = 'every sub-<pred> matches'
};
any = {
fn = p_any;
syntax = '<pred>…';
help = 'any sub-<pred> matches'
};
none = {
fn = p_none;
syntax = '<pred>…';
help = 'no sub-<pred> matches'
};
some = {
fn = p_some;
syntax = '<count> <pred>…';
help = '<count> or more sub-<pred>s match'
};
def = {
help = 'word has at least one definition that contains all <keyword>s';
syntax = '<keyword>…';
fn = function(e,...)
local kw = {...}
for i,d in ipairs(e.word.defs) do
for j,m in ipairs(d.means) do
for k,n in ipairs(kw) do
if not string.find(m.lit, n, 1, true) then
goto notfound
end
end
do return true end
::notfound::
end
end
return false
end;
};
lit = {
help = 'word is, begins with, or ends with <word>';
syntax = '<word> [(pfx|sfx)]';
fn = function(e,val,op)
if not op then
return e.lit == val
elseif op == 'pfx' then
return val == string.sub(e.lit,1,#val)
elseif op == 'sfx' then
return val == string.sub(e.lit,(#e.lit) - #val + 1)
else
id10t('[lit %s %s] is not a valid filter, “%s” should be either “pfx” or “sfx”',val,op,op)
end
end;
};
form = {
help = 'match against word\'s inflected forms';
syntax = '(<inflect> | <form> (set | is <inflect> | (pfx|sfx|match) <affix>))';
fn = function(e, k, op, v)
end;
};
part = {
help = 'word has definitions for every <part> of speech';
syntax = '<part>…';
fn = function(e,...)
local map, tgt = prepScan(...)
local matches = 0
for i,d in ipairs(e.word.defs) do
if map[d.part] then matches = matches + 1 end
end
return matches == tgt
end
};
root = {
help = 'match a word that derives from every <word>';
syntax = '<word>…';
fn = function(e,...)
local map, tgt = prepScan(...)
for i,d in ipairs(e.word.defs) do
local matches = 0
for j,r in ipairs(d.branch) do
if map[r] then matches = matches + 1 end
end
if matches == tgt then return true end
end
end
};
note = {
help = 'word has a matching note';
syntax = '([kind <kind> [<term>]] | term <term> | (min|max|count) <n>)';
fn = function(e, op, k, t)
if op == 'kind' or op == 'term' then
if op == 'term' and t then
id10t('too many arguments for [note term <term>]')
end
for _,d in ipairs(e.word.defs) do
for _,m in ipairs(d.means) do
for _,n in ipairs(m.notes) do
if op=='term' or n.kind == k then
if op=='kind' and t == nil then return true end
if string.find(table.concat(n.paras,'\n'), t or k, 1, true) ~= nil then return true end
end
end end end
elseif op == 'min' or op == 'max' or op == 'count' then
if t then
id10t('too many arguments for [note %s <n>]',op)
end
local n = math.floor(tonumber(k))
local total = 0
for i,d in ipairs(e.word.defs) do
for j,m in ipairs(d.means) do
total = total + #m.notes
if op == 'min' and total >= n then return true end
if op == 'max' and total > n then return false end
end end
if op == 'count' then return total == n end
if op == 'max' then return total <= n end
return false
end
end;
};
}
end
local function
safeopen(file,...)
if type(file) == 'string' then
local fd = io.open(file,...)
if not fd then error(userError("cannot open file " .. file),2) end
return fd
else
return file
end
end
local function copy(tab)
local new = {}
for k,v in pairs(tab) do new[k] = v end
return new
end
local function pathParse(p)
-- this is cursed, rewrite without regex pls TODO
if p == '.' then return {} end
local function comp(pfx)
return pfx .. '([0-9]+)'
end
local function mtry(...)
local mstr = '^(.+)'
for _, v in ipairs{...} do
mstr = mstr .. comp(v)
end
return p:match(mstr .. '$')
end
local xn
local w,dn,pn,mn,nn = mtry('%.','/p','/m','/n')
if not w then w,dn,pn,mn,xn = mtry('%.','/p','/m','/x') end
if not w then w,dn,pn,mn = mtry('%.','/p','/m') end
if not w then w,dn,pn= mtry('%.','/p') end
if not w then
local comps = {'%.','/m','/n'}
for i=#comps, 1, -1 do
local args = {table.unpack(comps,1,i)}
w,dn,mn,nn = mtry(table.unpack(args))
if not w and args[i] == '/n' then
args[i] = '/x'
w,dn,mn,xn = mtry(table.unpack(args))
end
if w then break end
end
end
if not w then w=p:match('^(.-)%.?$') end
return {w = w, dn = tonumber(dn), mn = tonumber(mn), pn=tonumber(pn); nn = tonumber(nn), xn = tonumber(xn)}
end
local function pathString(p,styler)
local function s(s, st, ...)
if styler then
return styler[st](tostring(s),...)
else return s end
end
local function comp(c,n,...)
return s('/','color',5)
.. s(string.format("%s%u",c,n), 'color',...)
end
local t = {}
if p.w then t[1] = s(p.w,'ul') else return '.' end
if p.dn then t[2] = string.format(".%s", s(p.dn,'br')) end
if p.pn then t[#t+1] = comp('p',p.pn,4,true) end
if p.mn then t[#t+1] = comp('m',p.mn,5,true) end
if p.xn then t[#t+1] = comp('x',p.xn,6,true)
elseif p.nn then t[#t+1] = comp('n',p.nn,4) end
if t[2] == nil then
return p.w .. '.' --make sure paths are always valid
end
return s(table.concat(t),'em')
end
local function pathMatch(a,b)
return a.w == b.w
and a.dn == b.dn
and a.mn == b.mn
and a.pn == b.pn
and a.nn == b.nn
and a.xn == b.xn
end
local function pathResolve(ctx, a)
local res = {}
if not a.w then return res end -- empty paths are valid!
local function lookup(seg, tbl,val)
if not tbl then error('bad table',2) end
local v = tbl[val]
if v then return v end
id10t('bad %s in path: %s', seg, val)
end
res.word = lookup('word', ctx.dict.words, a.w)
if not a.dn then return res end
res.def = lookup('definition', res.word.defs, a.dn)
if (not a.pn) and (not a.mn) then return res end
local m if a.pn then
res.phrase = lookup('phrase', res.def.phrases, a.pn)
res.meaning = lookup('meaning', res.phrase.means, a.mn)
else
res.meaning = lookup('meaning', res.def.means, a.mn)
end
if a.xn then
res.ex = lookup('example',res.meaning.examples,a.xn)
elseif a.nn then
res.note = lookup('note',res.meaning.notes,a.nn)
end
return res
end
local function pathNav(...)
local t = pathResolve(...)
return t.word,t.def,t.phrase,t.meaning,t.ex or t.note
end
local function pathRef(ctx, a)
local w,d,p,m,n = pathNav(ctx,a)
return n or m or p or d or w
end
local function pathSub(super,sub)
if super.w == nil then return true end
if sub.w ~= super.w then return false end
if super.pn == nil then goto checkMN end
if sub.pn ~= super.pn then return false end
::checkMN::
if super.mn == nil then return true end
if sub.mn ~= super.mn then return false end
if super.xn then
if sub.nn then return false end
if sub.xn ~= super.xn then return false end
elseif super.nn then
if sub.xn then return false end
if sub.nn ~= super.nn then return false end
end
return true
end
local cmds = {
create = {
help = "initialize a new dictionary file";
syntax = "<lang>";
raw = true;
exec = function(ctx, lang)
if not lang then
id10t 'for what language?'
end
local fd = safeopen(ctx.file,"wb")
local new = {
header = {
lang = lang;
meta = "";
partsOfSpeech = {};
inflectionForms = {};
};
words = {};
relsets = {};
}
local o = writeDict(new);
fd:write(o)
fd:close()
end;
};
coin = {
help = "add a new word";
syntax = "<word>";
write = true;
exec = function(ctx,word)
if ctx.dict.words[word] then
id10t "word already coined"
end
ctx.dict.words[word] = {defs={},rels={}}
end;
};
def = {
help = "define a word";
syntax = "<word> <part-of-speech> [<meaning> [<root>…]]";
write = true;
exec = function(ctx,word,part,means,...)
local etym = {...}
if (not word) or not part then
id10t 'bad definition'
end
if not ctx.dict.words[word] then
ctx.dict.words[word] = {defs={},rels={}}
end
local n = #(ctx.dict.words[word].defs)+1
ctx.dict.words[word].defs[n] = {
part = part;
branch = etym;
means = {means and {
lit=means;
examples={};
notes={};
rels={};
} or nil};
forms = {};
phrases = {};
rels={};
}
ctx.log('info', string.format('added definition #%u to “%s”', n, word))
end;
};
mean = {
help = "add a meaning to a definition";
syntax = "<word> <def#> <meaning>";
write = true;
exec = function(ctx,word,dn,m)
local t = pathResolve(ctx,{w=word,dn=dn})
table.insert(t.d.means, {lit=m,notes={}})
end;
};
rel = {
help = "manage groups of related words";
syntax = {
"(show|purge) <path> [<kind>]";
"(link|drop) <word> <group#> <path>…";
"new <rel> <path> <path>…";
"destroy <word> [<group#>]";
"rel ::= (syn|ant|co)"
};
write = true;
exec = function(ctx, op, ...)
local fo = ctx.sty[io.stdout]
if op == nil then id10t "not enough arguments" end
local groups = {}
if not (op=='new' or op=='link' or op=='drop' or op=='destroy' or op=='show' or op=='purge') then
id10t('invalid operation “%s” for `rel`', op)
end
if op == 'new' then
local rel = ...
if rel ~= 'syn' and rel ~= 'ant' and rel ~= 'met' then
id10t 'relationships must be synonymous, antonymous, or metonymous'
end
local links={}
for i,l in ipairs{select(2,...)} do
links[i] = pathParse(l)
end
local newstruct = {
uid=math.random(1,0xffffFFFF);
kind = rel;
}
table.insert(ctx.dict.relsets, newstruct)
for _, m in pairs(links) do
local obj = pathRef(ctx,m)
table.insert(obj.rels,newstruct.uid)
end
rebuildRelationCache(ctx.dict)
else -- assemble a list of groups
local tgtw = ...
local wp = pathParse(tgtw)
local o = pathResolve(ctx, wp)
for i,rs in pairs(ctx.dict.relsets) do
local allMembers = ctx.dict._relCache[rs.uid].mems
for j,s in ipairs(allMembers) do
if pathSub(s.path, wp) then
table.insert(groups, {
set = {
uid = rs.uid;
kind = rs.kind;
members = allMembers;
};
mem = s;
id = i;
})
break
end
end
end
if op == 'show' then
for i, g in ipairs(groups) do
local w = pathResolve(ctx, {w=g.mem.w}).w
local function label(path,w)
local repr = path.w
if path.dn then
repr = repr .. string.format("(%s)", w.defs[path.dn].part)
if path.mn then
repr = repr .. string.format(": %u. %s", path.dn, w.defs[path.dn].means[path.mn].lit)
else
local fulldef = {}
for i,v in ipairs(w.defs) do
fulldef[i] = v.lit
end
repr = repr..table.concat(fulldef, '; ')
end
end
return repr
end
local others = {}
for j, oo in ipairs(g.set.members) do
local o = oo.path
local ow = pathResolve(ctx, {w=o.w}).w
if (g.set.kind == 'ant' or not pathMatch(o, g.mem.path)) and
--exclude antonym headwords
not (g.set.kind == 'ant' and j==1) then
table.insert(others, ' '..label(o,ow))
end
end
local llab do
local cdw = ctx.dict.words
if g.set.kind == 'ant' then
local ap = g.set.members[1].path
llab = fo.br(label(ap,cdw[ap.w]) or '')
else
llab = fo.br(label(g.mem.path,cdw[g.mem.w]) or '')
end
end
local kls = {
syn = fo.color('synonyms',2,true)..' of';
ant = fo.color('antonyms',1,true)..' of';
met = fo.color('metonyms',4,true)..' of';
}
io.stdout:write(string.format("% 4u) %s\n%s", i, fo.ul(kls[g.set.kind] .. ' ' .. llab), table.concat(others,'\n')) .. '\n')
end
return false -- no changes made
elseif op == 'link' or op == 'drop' then
local tgtn, paths = (select(2,...)), { select(3, ...) }
rebuildRelationCache(ctx.dict)
elseif op == 'destroy' then
local tgtw, tgtn = ...
if not tgtn then id10t 'missing group number' end
local delendum = groups[tonumber(tgtn)]
if not delendum then id10t 'bad group number' end
for k,v in pairs(delendum.set.members) do
for idx, e in pairs(v.obj.rels) do
if e == delendum.set.uid then
fastDelete(v.obj.rels,idx)
end
end
end
fastDelete(ctx.dict.relsets, delendum.id)
rebuildRelationCache(ctx.dict)
else
id10t 'invalid operation'
end
end
end;
};
mod = {
help = "move, merge, split, or delete words or definitions";
syntax = {
"<path> (drop | [move|merge|clobber] <path> | out [<part> [<root>…]])";
"path ::= <word>[(@<def#>[/<meaning#>[:<note#>]]|.)]";
};
write = true;
};
note = {
help = "add a note to a definition or a paragraph to a note";
syntax = {"(<m-path> (add|for) <kind> | <m-path>:<note#>) <para>…";
"m-path ::= <word>@<def#>/<meaning#>"};
write = true;
exec = function(ctx,path,...)
local paras, mng
local dest = pathParse(path)
local t = pathResolve(ctx,path)
if dest.nn then
paras = {...}
else
local op, kind = ...
paras = { select(3, ...) }
if op == 'add' then
dest.nn = #(t.m.notes) + 1
t.m.notes[dest.nn] = {kind=kind, paras=paras}
return
elseif op == 'for' then
for i,nn in ipairs(t.m.notes) do
if nn.kind == kind then
dest.nn = i break
end
end
if not dest.nn then
id10t('no note of kind %s in %s',kind,path)
end
end
end
local dpa = t.m.notes[dest.nn].paras
local top = #dpa
for i,p in ipairs(paras) do
dpa[top+i] = p
end
end
};
shell = {
help = "open an interactive prompt";
raw = true;
};
help = {
help = "show help";
nofile = true;
syntax = "[<command>]";
};
predicates = {
help = "show available filter predicates";
nofile = true;
syntax = "[<predicate>]";
};
export = {
help = "create a text file dump compatible with source control";
syntax = "[<target-file>]";
};
import = {
help = "generate a usable dictionary from a text export file";
syntax = "[<input-file>]";
raw = true;
write = true;
};
dump = {
exec = function(ctx) print(dump(ctx.dict)) end
};
ls = {
help = "list all words that meet any given <filter>";
syntax = {"[<filter>…]";
"filter ::= (<word>|<pred> <arg>…)";
"arg ::= (<atom>|'['(<string>|<pred> <arg>…)']')"};
}
}
function cmds.predicates.exec(ctx, pred)
local list = predicates
if pred then list = {predicates[pred]} end
local f = ctx.sty[io.stderr]
for k,p in pairs(predicates) do
if p.help then
io.stderr:write(
f.br(' - ' ..
f.rgb('[',1,0,.5) ..
k .. ' ' ..
(f.color(p.syntax,5) or '…') ..
f.rgb(']',1,0,.5)) .. ': ' ..
f.color(p.help,4,true) .. '\n')
end
end
end
function cmds.ls.exec(ctx,...)
local filter = nil
local out = {}
for i,f in ipairs{...} do
local fn = parsefilter(f)
local of = filter or function() return false end
filter = function(e)
return fn(e) or of(e)
end
end
for lit,w in pairs(ctx.dict.words) do
local e = {lit=lit, word=w}
if filter == nil or filter(e) then
table.insert(out, e)
end
end
table.sort(out, function(a,b) return a.lit < b.lit end)
local fo = ctx.sty[io.stdout]
local function gatherRelSets(path)
local antonymSets, synonymSets, metonymSets = {},{},{}
local obj = pathRef(ctx,path)
if next(obj.rels) then
for i, relid in ipairs(obj.rels) do
local specuset,tgt,anto = {}
local rel = ctx.dict._relCache[relid].set
for j, mbr in ipairs(ctx.dict._relCache[relid].mems) do
if pathMatch(mbr.path, path) then
if rel.kind == 'syn' then tgt = synonymSets
elseif rel.kind == 'met' then tgt = metonymSets
elseif rel.kind == 'ant' then
if j == 1 -- is this the headword?
then tgt = antonymSets
else tgt = synonymSets
end
end
elseif j == 1 and rel.kind == 'ant' then
anto = mbr.path
else
table.insert(specuset, mbr.path)
end
end
if tgt then
table.insert(tgt, specuset)
if anto then
table.insert(antonymSets, {anto})
end
end
end
end
local function flatten(lst)
local new = {}
for i, l in ipairs(lst) do tcatD(new, l) end
return new
end
return {
syn = flatten(synonymSets);
ant = flatten(antonymSets);
met = flatten(metonymSets);
}
end
local function formatRels(rls, padlen)
-- optimize for the common case
if next(rls.syn) == nil and
next(rls.ant) == nil and
next(rls.met) == nil then return {} end
local pad = string.rep(' ',padlen)
local function format(label, set)
local each = map(set, function(e)
local ew,ed = pathNav(ctx, e)
local str = fo.ul(e.w)
if ed then str = string.format('%s(%s)',str,ed.part) end
if e.mn then str = string.format('%s§%u',str,e.mn) end
return str
end)
return fo.em(string.format("%s%s %s",pad,label,table.concat(each,', ')))
end
local lines = {}
local function add(l,c,lst)
table.insert(lines, format(fo.color(l,c,true),lst))
end
if next(rls.syn) then add('synonyms:',2,rls.syn) end
if next(rls.ant) then add('antonyms:',1,rls.ant) end
if next(rls.met) then add('metonyms:',4,rls.met) end
return lines
end
local function meanings(w,d,md,n)
local start = md and 2 or 1
local part = string.format('(%s)', d.part)
local pad = md and string.rep(' ', #part) or ''
local function note(n,insert)
if not next(n.paras) then return end
local pad = string.rep(' ',#(n.kind) + 9)
insert(' ' .. fo.hl(' ' .. n.kind .. ' ') .. ' ' .. n.paras[1])
for i=2,#n.paras do
insert(pad..n.paras[2])
end
end
local m = { (function()
if d.means[1] then
if md then
local id = ''
if ctx.flags.ident then
id=' ['..pathString({w=w.lit,dn=n,mn=1}, fo)..']'
end
return string.format(" %s %s 1. %s", id, fo.em(part), d.means[1].lit)
end
else return
fo.em(string.format(' %s [empty definition #%u]', part,n))
end
end)() }
tcatD(m, formatRels(gatherRelSets{w=w.lit,dn=n,mn=1}, 6))
for i=start,#d.means do local v = d.means[i]
local id = ''
if ctx.flags.ident then id='['..pathString({w=w.lit,dn=n,mn=n}, fo)..']' end
table.insert(m, string.format(' %s%s %u. %s', pad, id, i, v.lit))
tcatD(m, formatRels(gatherRelSets{w=w.lit,dn=n,mn=i}, 6))
for j,n in ipairs(v.notes) do
note(n, function(v) table.insert(m, v) end)
end
end
return table.concat(m,'\n')
end
local function autobreak(str)
if str ~= '' then return str..'\n' else return str end
end
for i, w in ipairs(out) do
local d = fo.ul(fo.br(w.lit))
local wordrels = autobreak(table.concat(
formatRels(gatherRelSets{w=w.lit}, 2),
'\n'
))
if #w.word.defs == 1 then
d=d .. ' '
.. fo.rgb(fo.em('('..(w.word.defs[1].part)..')'),.8,.5,1) .. '\n'
.. meanings(w,w.word.defs[1],false,1) .. '\n'
.. autobreak(table.concat(formatRels(gatherRelSets{w=w.lit,dn=1}, 4), '\n'))
.. wordrels .. '\n'
else
for j, def in ipairs(w.word.defs) do
local syn if wsc and wsc[j] then syn = wsc[j] end
d=d .. '\n'
.. meanings(w,syn,def,true,j) .. '\n'
.. autobreak(table.concat(
formatRels(gatherRelSets{w=w.lit,dn=j}, 4),
'\n'
))
end
d=d .. wordrels .. '\n'
end
io.stdout:write(d)
end
end
function cmds.import.exec(ctx,file)
local ifd = io.stdin
if file then
ifd = safeopen(file,'r')
end
local new = {
header = {
lang = lang;
meta = "";
partsOfSpeech = {};
inflectionForms = {};
};
words = {};
relsets = {};
}
local state = 0
local relsets = {}
local path = {}
local inflmap, lastinfl = {}
for l in ifd:lines() do
local words = strwords(l)
local c = words[1]
local function syn(mn,mx)
local nw = #words - 1
if nw < mn or (mx ~= nil and nw > mx) then
if mx ~= nil then
id10t('command %s needs between %u~%u words',c,mn,mx)
else
id10t('command %s needs at least %u words',c,mn)
end
end
end
if c ~= '*' and c~='meta' then -- comments
if state == 0 then
if c ~= 'pv0' then
id10t "not a parvan export"
end
new.header.lang = words[2]
new.header.meta = words[3]
state = 1
else
local T = pathResolve({dict=new}, path)
local W,D,P,M,N,X =
T.word,
T.def,
T.phrase,
T.meaning,
T.note,
T.ex
if c == 'w' then syn(1) state = 2
path = {w=words[2]}
new.words[words[2]] = {defs={},rels={}}
elseif c == 'f' then syn(1)
local nf = {
name = words[2];
abbrev = words[3] or "";
desc = words[4] or "";
parts = {};
}
table.insert(new.header.inflectionForms, nf)
inflmap[words[2]] = #(new.header.inflectionForms)
lastinfl = nf
elseif c == 'fp' then syn(1)
if not lastinfl then
id10t 'fp can only be used after f' end
table.insert(lastinfl.parts,words[2])
elseif c == 's' then syn(2)
relsets[words[3]] = relsets[words[3]] or {}
relsets[words[3]].kind = words[2]
relsets[words[3]].uid = tonumber(words[3])
relsets[words[3]].members = relsets[words[3]].members or {}
elseif state >= 2 and c == 'r' then syn(1)
local rt
if state == 2 then
rt = W.rels
elseif state == 3 then
rt = D.rels
elseif state == 4 then
rt = D.rels
elseif state == 14 then
rt = P.rels
end
relsets[words[2]] = relsets[words[2]] or {
uid = tonumber(words[2]) or math.random(0,0xffffFFFF);
members={};
}
table.insert(relsets[words[2]].members, path)
elseif state >= 2 and c == 'd' then syn(1) state = 3
table.insert(W.defs, {
part = words[2];
branch = {};
means = {};
forms = {};
phrases = {};
rels = {};
})
path = {w = path.w, dn = #(W.defs)}
elseif state >= 3 and c == 'dr' then syn(1)
table.insert(D.branch, words[2])
elseif state >= 3 and c == 'df' then syn(2)
if not inflmap[words[2]] then
id10t('no inflection form %s defined', words[2])
end
D.forms[inflmap[words[2]]] = words[3]
elseif state >= 3 and c == 'p' then syn(1) state = 14
table.insert(D.phrases, {
str = words[2];
means = {};
rels = {};
})
path = {w = path.w, dn = path.dn, pn = #(D.phrases)}
elseif state >= 3 and c == 'm' then syn(1) state = 4
table.insert(D.means, {
lit = words[2];
notes = {};
examples = {};
rels = {};
});
path = {w = path.w, dn = path.dn, pn=path.pn, mn = #(D.means)}
elseif state >= 4 and c == 'n' then syn(1) state = 5
table.insert(M.notes, {kind=words[2], paras={}})
path = {w = path.w, dn = path.dn, pn = path.pn, mn = path.mn, nn = #(M.notes)};
elseif state >= 5 and c == 'np' then syn(1)
table.insert(N.paras, words[2])
end
-- we ignore invalid ctls, for sake of forward-compat
end
end
end
for k,v in pairs(relsets) do
if not v.uid then
--handle non-numeric export ids
v.uid = math.random(0,0xffffFFFF)
end
table.insert(new.relsets, v)
for q,m in pairs(v.members) do
table.insert(pathRef({dict=new},m).rels, v.uid)
end
end
local ofd = safeopen(ctx.file,"w+b")
local o = writeDict(new);
ofd:write(o)
ofd:close()
end
function cmds.export.exec(ctx,file)
local ofd = io.stdout
if file then ofd = safeopen(file, 'w+') end
local function san(str)
local d = 0
local r = {}
for i,cp in utf8.codes(str) do
-- insert backslashes for characters that would
-- disrupt strwords() parsing
if cp == 0x0a then
table.insert(r, 0x5c)
table.insert(r, 0x6e)
else
if cp == 0x5b then
d = d + 1
elseif cp == 0x5d then
if d >= 1 then
d = d - 1
else
table.insert(r, 0x5c)
end
end
table.insert(r, cp)
end
end
return '[' .. utf8.char(table.unpack(r)) .. ']'
end
local function o(lvl,...)
local pfx = ''
if ctx.flags.human and lvl > 0 then
pfx = string.rep('\t', lvl)
end
ofd:write(pfx..string.format(...)..'\n')
end
local d = ctx.dict
o(0,'pv0 %s %s', san(d.header.lang), san(d.header.meta))
local function checksyn(obj,lvl)
for k,v in pairs(obj.rels) do
o(lvl,'r %u',s.uid)
end
end
for i,f in pairs(d.header.inflectionForms) do
o(0,'f %s %s %s', san(f.name), san(f.abbrev), san(f.desc))
for j,p in pairs(f.parts) do
o(1,'fp %s', san(p))
end
end
local function scanMeans(tbl,path,lvl)
for j,m in ipairs(def.means) do
o(lvl,'m %s', san(m.lit))
local lp = copy(path)
lp.mn = j
checksyn(m,lp,lvl+1)
for k,n in ipairs(m.notes) do
o(lvl+1,'n %s', san(n.kind))
for a,p in ipairs(n.paras) do
o(lvl+2,'np %s', san(p))
end
end
end
end
for lit, w in pairs(d.words) do
o(0,'w %s',san(lit))
checksyn(w,{w=lit},1)
for i,def in ipairs(w.defs) do
o(1,'d %s',san(def.part))
checksyn(def,{w=lit,dn=i},2)
for j,r in ipairs(def.branch) do
o(2,'dr %s',san(r))
end
for j,p in ipairs(def.phrases) do
o(2,'p %s',san(p.str))
scanMeans(p.means, {w=lit,dn=i,pn=j}, 3)
end
scanMeans(def.means, {w=lit,dn=i}, 2)
end
end
for _,s in ipairs(d.relsets) do o(0,'s %s %u', s.kind, s.uid) end
end
local function filterD(lst, fn)
-- cheap algorithm to destructively filter a list
-- DOES NOT preserve order!!
local top = #lst
for i=top,1,-1 do local m = lst[i]
if not fn(m,i) then
lst[i] = lst[top]
lst[top] = nil
top = top - 1
end
end
return lst
end
function cmds.mod.exec(ctx, orig, oper, dest, ...)
rebuildRelationCache(ctx.dict)
end
local function fileLegible(file)
-- check if we can access the file
local fd = io.open(file,"rb")
local ret = false
if fd then ret = true end
fd:close()
return ret
end
local function
prompt(p,multiline)
-- returns string if successful, nil if EOF, false if ^C
io.stderr:write(p)
local ok, res = pcall(function()
return io.stdin:read(multiline and 'a' or 'l')
end)
if ok then return res end
return false
end
function cmds.shell.exec(ctx)
if not fileLegible(ctx.file) then
-- avoid accidentally creating a file without the
-- proper document structure and metadata
id10t("file %s must already exist and be at least readable", ctx.file)
end
local fd, rw = io.open(ctx.file,"r+b"), true
if not fd then -- not writable
ctx.log('warn',string.format('file %s is not writable', ctx.file))
fd, rw = io.open(ctx.file, "rb"), false
end
ctx.fd = fd
ctx.dict = readDict(fd:read 'a')
fd:close()
local written = false
local fo = ctx.sty[io.stdout]
local fe = ctx.sty[io.stderr]
repeat
local cmd = prompt(fe.br(string.format('(parvan %s) ', ctx.file)))
if cmd == false then
io.stderr:write(fe.resetLine())
if written then
ctx.log('warn', 'abandoning changes!')
end
return 0
end
if cmd and cmd ~= '' then
local words = strwords(cmd)
if next(words) then
if words[1] == 'bail' or
words[1] == 'abandon' or
words[1] == 'q!' then
if written then
ctx.log('warn', 'abandoning changes!')
end
return 0
end
local c = cmds[words[1]]
if c then
if c.raw then
ctx.log('fatal', words[1] .. ' cannot be run from `shell`')
elseif not implies(c.write, rw) then
ctx.log('fatal', ctx.file .. ' is not writable')
else
local ok, outcome = ctx.try(c.exec, ctx, table.unpack(words,2))
if ok and outcome ~= false then written = written or c.write end
end
elseif cmd == 'save' or cmd == 'wq' then
if not written then
ctx.log('info', 'no changes to save')
end
cmd = nil
elseif cmd == 'quit' or cmd == 'q' then
if not written then cmd = nil else
ctx.log('fatal', 'dictionary has unsaved changes')
end
else
ctx.log('fatal', words[1] .. ' is not a command')
end
end
end
until cmd == nil
if written then
ctx.log('info', 'saving file')
local out = writeDict(ctx.dict)
local fd = io.open(ctx.file,'w+b')
fd:write(out)
fd:close()
end
end
local function
showHelp(ctx,k,v)
if not v then
id10t 'no such command'
end
if v.help then
local fe = ctx.sty[io.stderr]
local defs, synt = ''
if type(v.syntax) == 'table' then
synt = v.syntax[1]
local pad = string.rep(' ', #k+5)
for i=2,#v.syntax do
defs = defs .. pad .. fe.color(v.syntax[i],5) .. '\n'
end
else synt = v.syntax end
io.stderr:write(string.format(
" > %s %s\n" .. defs ..
" %s\n",
fe.br(k), synt and fe.br(fe.color(synt,5)) or '',
fe.em(fe.color(v.help,4,true))))
end
end
function cmds.help.exec(ctx,cmd)
if cmd then
showHelp(ctx, cmd, cmds[cmd])
else
for cmd,c in pairs(cmds) do
showHelp(ctx, cmd, c)
end
end
end
local globalFlags <const> = {
human = {'h','human','enable human-readable exports'};
ident = {'i','ident','show identifier paths for all items'}
}
local function
usage(me,ctx)
local ln = 0
local ct = {}
local fe = ctx.sty[io.stderr]
local fstr = ""
local flagHelp = {}
for k,v in pairs(globalFlags) do
fstr = fstr .. v[1]
table.insert(flagHelp, string.format(" -%s --%s: %s\n",table.unpack(v)))
end
io.stderr:write(string.format(fe.br"usage:".." %s [-%s] <file> [<command> [args…]]\n",me,fstr) .. table.concat(flagHelp))
--[[
for k,v in pairs(cmds) do
local n = 1 + utf8.len(k) + utf8.len(v.syntax)
ct[k] = n
if n > ln then ln = n end
end
for k,v in pairs(cmds) do
local pad = string.rep(" ", ln - ct[k] + 3)
io.stderr:write(string.format(" "..fe.br'%s %s'.."%s%s\n",
k, v.syntax, pad, v.help))
end]]
for k,v in pairs(cmds) do
showHelp(ctx,k,v)
end
return 64
end
local function
dispatch(argv, ctx)
local ferr = ctx.sty[io.stderr]
local args = {}
local flags = {}
local i = 1 while i <= #argv do
local a = argv[i]
if a == '--' then i=i+1 break
elseif a:sub(1,2) == '--' then
local fs <const> = a:sub(3)
for k,v in pairs(globalFlags) do
if v[2] == fs then flags[k] = true end
end
elseif a:sub(1,1) == '-' then
for p,cp in utf8.codes(''), a, #'-' do
local c <const> = utf8.char(cp)
for k,v in pairs(globalFlags) do
if v[1] == c then flags[k] = true break end
end
end
else table.insert(args, a) end
i = i + 1 end
for j=i,#argv do table.insert(args,argv[j]) end
local file, cmd = table.unpack(args)
if cmd and cmds[cmd] then
local c,fd,dict = cmds[cmd]
if (not c.raw) and not c.nofile then
fd = safeopen(file, "rb")
dict = readDict(fd:read 'a')
fd:close()
-- lua io has no truncate method, so we must
-- rely on the clobbering behavior of the open()
-- call instead :(
end
cmds[cmd].exec({
sty = ctx.sty;
try = ctx.try;
log = ctx.log;
flags = flags;
file = file;
fd = fd;
dict = dict;
}, table.unpack(args,3))
if (not c.raw) and c.write then
local output = writeDict(dict)
-- writeDict should always be given a chance to
-- bail before the previous file is destroyed!!
-- you don't want one bug to wipe out your entire
-- dictionary in one fell swoop
fd = safeopen(file,'w+b')
fd:write(output)
fd:close()
end
return 0
else
return usage(argv[0], ctx)
end
end
local argv if arg
then argv = arg
else argv = {[0] = 'parvan', ...}
end
local sty = {
[io.stdout] = ansi.formatter(io.stdout);
[io.stderr] = ansi.formatter(io.stderr);
};
local function log(lvl, msg)
local colors = {fatal=1,warn=3,info=4,debug=2}
local ferr = sty[io.stderr]
io.stderr:write(string.format(
ferr.color(ferr.br("(%s)"),colors[lvl]).." %s\n", lvl, msg))
end
local function stacktrace(err)
return debug.traceback(err,3)
end
local function try(fn,...)
-- a wrapper around pcall that produces a standard error
-- message format when an error occurs
local res = { xpcall(fn,stacktrace,...) }
if not res[1] then
log('fatal', res[2])
end
return table.unpack(res)
end
local ok, res = xpcall(dispatch, stacktrace, argv, {
try = try, sty = sty, log = log
})
if not ok then
log('fatal', res)
os.exit(1)
end
os.exit(res)