Index: parvan.lua ================================================================== --- parvan.lua +++ parvan.lua @@ -52,10 +52,17 @@ 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 @@ -331,10 +338,18 @@ 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)}; } @@ -341,31 +356,31 @@ fmt.example = { {'quote',fmt.string}; {'src',fmt.label}; } -fmt.meaning = { + +fmt.meaning = fmt.relatable { {'lit', fmt.string}; {'examples', fmt.list(fmt.example,fmt.u8)}; {'notes', fmt.list(fmt.note,fmt.u8)}; } -fmt.phrase = { +fmt.phrase = fmt.relatable { {'str',fmt.label}; {'means',fmt.list(fmt.meaning,fmt.u8)}; - {'xref',fmt.list(fmt.path,fmt.u16)}; } -fmt.def = { +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.word = fmt.relatable { {'defs', fmt.list(fmt.def,fmt.u8)}; } fmt.dictHeader = { {'lang', fmt.tag}; @@ -380,16 +395,17 @@ -- leave empty if not relevant },fmt.u16)}; } fmt.relSet = { - {'uid', fmt.u32}; + {'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 - {'members', fmt.list(fmt.path,fmt.u16)}; {'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)}; @@ -443,18 +459,37 @@ 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 sc = {} - for i,s in ipairs(d.relsets) do - for j,m in ipairs(s.members) do - sc[m.w] = sc[m.w] or {} - table.insert(sc[m.w], s) + 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 - d._relCache = sc + 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() @@ -751,29 +786,10 @@ else return file end end -local function -safeNavWord(ctx, word, dn, mn, nn) - local w = ctx.dict.words[word] - if not w then id10t 'bad word' end - if dn == nil then return w end - - local d = w.defs[tonumber(dn)] - if not d then id10t('no definition #%u',dn) end - if mn == nil then return w,d end - - local m = d.means[tonumber(mn)] - if not m then id10t('no meaning #%u',mn) end - if nn == nil then return w,d,m end - - local n = m.notes[tonumber(nn)] - if not n then id10t('no note #%u',nn) end - return w,d,m,n -end - local function copy(tab) local new = {} for k,v in pairs(tab) do new[k] = v end return new end @@ -842,36 +858,37 @@ and a.pn == b.pn and a.nn == b.nn and a.xn == b.xn end local function pathResolve(ctx, a) - if not a.w then return end -- empty paths are valid! + 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 - local res = {} res.word = lookup('word', ctx.dict.words, a.w) if not a.dn then return res end - res.def = lookup('definition', w.defs, a.dn) + 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', d.phrases, a.pn) - res.meaning = lookup('meaning', p.means, a.mn) + res.phrase = lookup('phrase', res.def.phrases, a.pn) + res.meaning = lookup('meaning', res.phrase.means, a.mn) else - res.meaning = lookup('meaning', d.means, a.mn) + res.meaning = lookup('meaning', res.def.means, a.mn) end if a.xn then - res.ex = lookup('example',m.examples,a.xn) + res.ex = lookup('example',res.meaning.examples,a.xn) elseif a.nn then - res.note = lookup('note',m.notes,a.nn) + res.note = lookup('note',res.meaning.notes,a.nn) end return res end @@ -938,11 +955,11 @@ write = true; exec = function(ctx,word) if ctx.dict.words[word] then id10t "word already coined" end - ctx.dict.words[word] = {defs={}} + ctx.dict.words[word] = {defs={},rels={}} end; }; def = { help = "define a word"; syntax = " [ […]]"; @@ -951,22 +968,25 @@ 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={}} + 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 = { @@ -1004,30 +1024,37 @@ for i,l in ipairs{select(2,...)} do links[i] = pathParse(l) end local newstruct = { uid=math.random(1,0xffffFFFF); - members=links; 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 - local rc = ctx.dict._relCache - for i,l in pairs(links) do - rc[l.w] = rc[l.w] or {} - table.insert(rc[l.w], newstruct) - end rebuildRelationCache(ctx.dict) else -- assemble a list of groups local tgtw = ... local wp = pathParse(tgtw) - local w,d,m = pathNav(ctx, wp) - for i,ss in ipairs(ctx.dict.relsets) do - for j,s in ipairs(ss.members) do - if pathSub(s, wp) then --- if s.word == wp.w and (wp.dn == nil or s.def == wp.dn) then - table.insert(groups, {set = ss, mem = s, id = i}) + 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 @@ -1051,25 +1078,26 @@ return repr end local others = {} - for j, o in ipairs(g.set.members) do + 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)) and + 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] + local ap = g.set.members[1].path llab = fo.br(label(ap,cdw[ap.w]) or '') else - llab = fo.br(label(g.mem,cdw[g.mem.w]) or '') + 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'; @@ -1084,20 +1112,20 @@ 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 - local rs = ctx.dict.relsets - local last = #rs - if delendum.id == last then - rs[delendum.id] = nil - else -- since order doesn't matter, we can use a - -- silly swapping trick to reduce the deletion - -- worst case from O(n) to O(2) - rs[delendum.id] = rs[last] - rs[last] = nil + + 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 @@ -1117,32 +1145,32 @@ "m-path ::= @/"}; write = true; exec = function(ctx,path,...) local paras, mng local dest = pathParse(path) - local _,_,m = safeNavWord(ctx,dest.w,dest.dn,dest.mn) + local t = pathResolve(ctx,path) if dest.nn then paras = {...} else local op, kind = ... paras = { select(3, ...) } if op == 'add' then - dest.nn = #(m.notes) + 1 - m.notes[dest.nn] = {kind=kind, paras=paras} + 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(m.notes) do + 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 = m.notes[dest.nn].paras + local dpa = t.m.notes[dest.nn].paras local top = #dpa for i,p in ipairs(paras) do dpa[top+i] = p end end @@ -1218,27 +1246,29 @@ 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 = {},{},{} - if ctx.dict._relCache[path.w] then - for i, rel in ipairs(ctx.dict._relCache[path.w]) do + local obj = pathRef(ctx,path) + if next(obj.rels) then + for i, relid in ipairs(obj.rels) do local specuset,tgt,anto = {} - for j, mbr in ipairs(rel.members) do - if pathMatch(mbr, path) then + 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 + anto = mbr.path else - table.insert(specuset, mbr) + table.insert(specuset, mbr.path) end end if tgt then table.insert(tgt, specuset) if anto then @@ -1329,11 +1359,10 @@ local d = fo.ul(fo.br(w.lit)) local wordrels = autobreak(table.concat( formatRels(gatherRelSets{w=w.lit}, 2), '\n' )) - local wc = ctx.dict._relCache[w.lit] 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')) @@ -1395,15 +1424,21 @@ end new.header.lang = words[2] new.header.meta = words[3] state = 1 else - print(pathString(path, ctx.sty[io.stderr])) - local W,D,M,N = pathNav({dict=new}, path) + 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={}} + 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 ""; @@ -1420,12 +1455,22 @@ 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]); + 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, { @@ -1432,29 +1477,38 @@ 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, mn = #(D.means)} + 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, mn = path.mn, nn = #(M.notes)}; + 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 @@ -1465,10 +1519,14 @@ 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) @@ -1509,52 +1567,49 @@ 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) --- for _,s in ipairs(d.synonyms) do - local lvl = 0 - if obj.nn then lvl = 4 - elseif obj.mn then lvl = 3 - elseif obj.dn then lvl = 2 - elseif obj.w then lvl = 1 end - if not d._relCache[obj.w] then return end - for _,s in ipairs(d._relCache[obj.w]) do - for _,sm in ipairs(s.members) do - if pathMatch(obj, sm) then - o(lvl,'r %u',s.uid) - break - end - end + 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=lit} + checksyn(w,{w=lit},1) for i,def in ipairs(w.defs) do o(1,'d %s',san(def.part)) - checksyn{w=lit,dn=i} + checksyn(def,{w=lit,dn=i},2) for j,r in ipairs(def.branch) do o(2,'dr %s',san(r)) end - for j,m in ipairs(def.means) do - o(2,'m %s', san(m.lit)) - checksyn{w=lit,dn=i,mn=j} - for k,n in ipairs(m.notes) do - o(3,'n %s', san(n.kind)) - for a,p in ipairs(n.paras) do - o(4,'np %s', san(p)) - end - 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 @@ -1571,256 +1626,10 @@ end return lst end function cmds.mod.exec(ctx, orig, oper, dest, ...) - if (not orig) or not oper then - id10t '`mod` requires at least an origin and an operation' - end - local op, dp = pathParse(orig) - local w,d,m,n = safeNavWord(ctx, op.w,op.dn,op.mn,op.nn) - -- unfortunately, "pointers" exist elsewhere in the - -- structure, currently just from relsets, that must - -- be updated whenever an object moves or changes. - -- this is miserable and takes a lot of work, using - -- algorithms provided by the following functions. - -- note that we don't bother trying to update the - -- relcache as we go, it's simply not worth the work; - -- instead we simply rebuild the whole cache when - -- this command returns - local function cleanupRels(path, fn) - local rc = ctx.dict._relCache[path.w] - if rc then - for k,s in pairs(rc) do fn(s,k) end - end - end - local function cleanupRelsEach(path, fn) - cleanupRels(path, function(s,k) - local top = #s.members - for i=1,top do local m=s.members[i] - if pathSub(path, m) then - local val = fn(m,s,i) - if val ~= nil then - s.members[i] = val - end - end - end - end) - end - local function deleteRefsTo(path) - cleanupRels(path, function(s) - -- antonyms: delete the headword and transform the group - -- to a list of synonyms - if s.kind == 'ant' and pathSub(path,s.members[1]) then - s.kind = 'syn' - end - filterD(s.members, function(m) - return not pathSub(path, m) - end) - end) - if not path.dn then - ctx.dict._relCache[path.w] = nil - end - end - local function moveRelTree(op,dp) - cleanupRelsEach(op, function(old,set,idx) - local new = {} - for _,elt in pairs{'w','dn','mn','nn'} do - if dp[elt] ~= nil then - new[elt] = dp[elt] - else - new[elt] = op[elt] or old[elt] - end - end - return new - end) - end - local function shiftRelTree(dp, fld, nid, amt) - local cleanupTargetMask = ({ - dn = {w=dp.w}; - mn = {w=dp.w,dn=dp.dn}; - nn = {w=dp.w,dn=dp.dn,mn=dp.mn}; - })[fld] -- >____< - cleanupRelsEach(cleanupTargetMask, function(old,set,i) - if old[fld] >= nid then - old[fld] = old[fld] + amt - end - end) - end - local function insertAndMoveRelTree(tbl,n,op,dp,fld) - local nid = #tbl - local path = copy(dp) - path[fld] = nid - tbl[nid] = n - shiftRelTree(dp,fld,1) - moveRelTree(op, path) - end - if oper == 'drop' then - -- clean out the cache and delete relationships - deleteRefsTo(op) - if not d then - ctx.dict.words[op.w] = nil - elseif not m then - table.remove(w.defs, op.dn) - elseif not n then - table.remove(d.means, op.mn) - else - table.remove(m.notes, op.nn) - end - elseif oper == 'out' then - if n or not m then id10t '`mod out` must target a meaning' end - if not dest then id10t '`mod out` requires at least a part of speech' end - local newdef = { - part = dest; - branch = {...}; - forms = {}; - means = {m}; - } - shiftRelTree(op, 'dn', op.dn, 1) - table.insert(w.defs,op.dn+1, newdef) - moveRelTree(op,{w=op.w, dn=op.dn+1, mn=1}) - table.remove(d.means,op.mn) - elseif oper == 'move' or oper == 'merge' or oper == 'clobber' then - if dest - then dp = pathParse(dest) - else id10t('`mod %s` requires a target',oper) - end - if n then - if not dp.mn then - id10t '`mod` on a note requires a note or meaning destination' - end - local _,_,dm = safeNavWord(ctx, dp.w,dp.dn,dp.mn) - if dp.nn then - if oper == 'move' then - shiftRelTree(dp, 'nn', dp.nn, 1) - table.insert(dm.notes, dp.nn, n) - elseif oper == 'merge' then - local top = #(dm.notes[dp.nn].paras) - for i, v in ipairs(n.paras) do - dm.notes[dp.nn].paras[i+top] = v - end - elseif oper == 'clobber' then - deleteRefsTo(dp) - dm.notes[dp.nn] = n - end - moveRelTree(op,dp) - else - if oper ~= 'move' then - id10t('`mod note %s` requires a note target', oper) - end - insertAndMoveRelTree(dm.notes,n,op,dp,'nn') - end - if oper == 'move' and dp.nn and dm == m and op.nn > dp.nn then - table.remove(m.notes,op.nn+1) - else - table.remove(m.notes,op.nn) - end - elseif m then - if not dp.dn then - local newdef = { - part = d.part; - branch = copy(d.branch); - forms = copy(d.forms); - means = {m}; - } - local didx - if ctx.dict.words[dp.w] then - local defst = ctx.dict.words[dp.w].defs - didx = #defst - defst[didx] = newdef - else - ctx.dict.words[dp.w] = { - defs = {newdef}; - } - didx = 1 - end - cleanupRelsEach(op, function(oldpath,set,mi) - return {w=dp.w,dn=didx,mn=1,nn=oldpath.nn} - end) - table.remove(d.means,dp.mn) - else - local dw, dd = safeNavWord(ctx, dp.w, dp.dn) - if dp.mn then - if dd.means[dp.mn] and (oper == 'merge' or oper=='clobber') then - if oper == 'merge' then - dd.means[dp.mn] = dd.means[dp.mn] .. '; ' .. m - elseif oper == 'clobber' then - deleteRefsTo(dp) - dd.means[dp.mn] = m - end - else - cleanupRelsEach({w=dp.w,dn=dp.dn}, function(old,set,i) - if old.mn >= dp.mn then - old.mn = old.mn + 1 - end - end) - table.insert(dd.means, dp.mn, m) - end - moveRelTree(op,dp) - else - insertAndMoveRelTree(dd.means,m, op,dp,'mn') --- table.insert(dd.means, m) - end - if oper == 'move' and dp.mn and dd.means == d.means and op.mn > dp.mn then - table.remove(d.means,op.mn+1) - else - table.remove(d.means,op.mn) - end - end - elseif d then - local ddefs = safeNavWord(ctx, dp.w).defs - if dp.dn then - if oper == 'merge' then - local top = #(ddefs[dp.dn].means) - for i,om in ipairs(d.means) do - ddefs[dp.dn].means[top+i] = om - end - for k,p in pairs(d.forms) do - deleteRefsTo(dp) - ddefs[dp.dn].forms[k] = p -- clobbers! - end - else - shiftRelTree(dp,'dn',dp.dn,1) - table.insert(ddefs, dp.dn, d) - end - moveRelTree(op,dp) - else - insertAndMoveRelTree(ddefs,d, op,dp,'dn') --- table.insert(ddefs, d) - end - if oper == 'move' and dp.mn and w.defs == ddefs and op.mn > dp.mn then - table.remove(w.defs,op.dn+1) - else - table.remove(w.defs,op.dn) - end - else - if ctx.dict.words[dp.w] then - if oper ~= 'merge' then - id10t('the word “%s” already exists; use `merge` if you want to merge the words together', dp.w) - end - for i,def in ipairs(w.defs) do - local odp = copy(op) odp.dn = i - local ddp = {w=dp.w, dn=dp.dn+i-1} - if dp.dn then - shiftRelTree(dp, 'dn', dp.dn+i-1, 1) - table.insert(ctx.dict.words[dp.w].defs, dp.dn+i-1, def) - moveRelTree(odp,ddp) - else --- table.insert(ctx.dict.words[dp.w].defs, def) - insertAndMoveRelTree(ctx.dict.words[dp.w].defs, def, - odp,dp,'dn') - end - end - else - ctx.dict.words[dp.w] = w - moveRelTree(op,dp) --- ctx.dict._relCache[dp.w] = ctx.dict._relCache[op.w] --- ctx.dict._relCache[op.w] = nil - end - ctx.dict.words[op.w] = nil - end - end rebuildRelationCache(ctx.dict) end local function fileLegible(file) -- check if we can access the file