Overview
Comment: | commit to preserve old code im about to axe, parvan is broken currently |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
f996abb5e5417727e253955fa320a03e |
User & Date: | lexi on 2022-04-28 21:01:40 |
Other Links: | manifest | tags |
Context
2022-04-28
| ||
22:52 | clean up rels mechanism, get parvan mostly working again check-in: bc37f02701 user: lexi tags: trunk | |
21:01 | commit to preserve old code im about to axe, parvan is broken currently check-in: f996abb5e5 user: lexi tags: trunk | |
2022-04-26
| ||
02:02 | add syn check-in: 0f6a5bda23 user: lexi tags: trunk | |
Changes
Modified parvan.lua from [2e5da05ad6] to [30400bd2f4].
8 8 -- SSD Social Sciences Directorate 9 9 -- ELS External Linguistics Subdirectorate 10 10 -- +WSO Worlds Security Overdirectorate 11 11 -- EID External Influence Directorate ] 12 12 13 13 local function implies(a,b) return a==b or not(a) end 14 14 15 +local function map(lst,fn) 16 + local new = {} 17 + for k,v in pairs(lst) do 18 + local nv, nk = fn(v,k) 19 + new[nk or k] = nv 20 + end 21 + return new 22 +end 23 +local function mapD(lst,fn) --destructive 24 + -- WARNING: this will not work if nk names an existing key! 25 + for k,v in pairs(lst) do 26 + local nv, nk = fn(v,k) 27 + if nk == nil or k == nk then 28 + lst[k] = nv 29 + else 30 + lst[k] = nil 31 + lst[nk] = nv 32 + end 33 + end 34 + return lst 35 +end 36 +local function pushi(dest, idx, src, ...) 37 + if not src then return end 38 + dest[idx] = src 39 + pushi(dest, idx+1, ...) 40 +end 41 +local function push(dest, ...) pushi(dest,#dest+1,...) end 42 +local function cons(car, cdr) 43 + local new = {car} 44 + for k,v in ipairs(cdr) do new[k+1] = v end 45 + return new 46 +end 47 +local function tcatD(dest, ...) 48 + local i = #dest 49 + local function iter(src, ...) 50 + if src == nil then return end 51 + local sc = #src 52 + for j=1,sc do dest[i+j] = src[j] end 53 + i = i + sc 54 + iter(...) 55 + end 56 + iter(...) 57 +end 58 +local function tcat(...) 59 + local new = {} 60 + tcatD(new, ...) 61 + return new 62 +end 15 63 local ansi = { 16 64 levels = { 17 65 plain = 0; 18 66 ansi = 1; 19 67 color = 2; 20 68 color8b = 3; 21 69 color24b = 4; 22 70 }; 23 71 } 24 72 25 73 ansi.seqs = { 26 - br = {ansi.levels.ansi, "[1m", "[21m"}; 74 + br = {ansi.levels.ansi, "[1m", "[22m"}; 27 75 hl = {ansi.levels.ansi, "[7m", "[27m"}; 28 76 ul = {ansi.levels.ansi, "[4m", "[24m"}; 29 77 em = {ansi.levels.ansi, "[3m", "[23m"}; 30 78 }; 31 79 32 80 function ansi.termclass(fd) -- awkwardly emulate isatty 33 81 if fd:seek('cur',0) then ................................................................................ 83 131 if cl == ansi.levels.color24b then 84 132 function f.rgb(str, r,g,b, bg) 85 133 return string.format("\27[%c8;2;%u;%u;%um", bg and 0x34 or 0x33, 86 134 ftoi(r,g,b)) .. str .. reset 87 135 end 88 136 elseif cl == ansi.levels.color8b then 89 137 function f.rgb(str, r,g,b, bg) 90 - local code = 16 + (r * 5)*36 + (g * 5)*6 + (b * 6) 138 + local code = 16 + math.floor(r * 5)*36 + math.floor(g * 5)*6 + math.floor(b * 6) 91 139 return string.format("\27[%c8;5;%um", bg and 0x34 or 0x33, code) 92 140 .. str .. reset 93 141 end 94 142 elseif cl == ansi.levels.color then 95 143 function f.rgb(str, r,g,b, bg) 96 144 local code = 0x30 + 1 -- TODO 97 145 return string.format("\27[%c%cm", bg and 0x34 or 0x33, code) ................................................................................ 105 153 return f 106 154 end 107 155 108 156 109 157 local function dump(v,pfx,cyc,ismeta) 110 158 pfx = pfx or '' 111 159 cyc = cyc or {} 112 - local np = pfx .. ' ' 160 + local np = pfx .. ' ' 113 161 114 162 if type(v) == 'table' then 115 163 if cyc[v] then return '<...>' else cyc[v] = true end 116 164 end 117 165 118 166 if type(v) == 'string' then 119 167 return string.format('%q', v) ................................................................................ 126 174 local meta = '' 127 175 if getmetatable(v) then 128 176 meta = dump(getmetatable(v),pfx,cyc,true) .. '::' 129 177 end 130 178 if ismeta then 131 179 return string.format('%s<|\n%s%s|>',meta,str,pfx) 132 180 else 133 - return meta..'{\n' .. str .. pfx .. '}\n' 181 + return meta..'{\n' .. str .. pfx .. '}' 134 182 end 135 183 else 136 184 return string.format('%s', v) 137 185 end 138 186 end 139 187 140 188 local struct = { ................................................................................ 186 234 fmt.string = qpack "s4" 187 235 fmt.label = qpack "s2" 188 236 fmt.tag = qpack "s1" 189 237 fmt.u8 = qpack "I1" 190 238 fmt.u16 = qpack "I2" 191 239 fmt.u24 = qpack "I3" 192 240 fmt.u32 = qpack "I4" 241 +fmt.path = { 242 + -- encodes a FIXED path to an arbitrary type of object 243 + encode = function(a) 244 + local kind = 0 245 + local vals = {} 246 + if a.w then kind = 1 247 + table.insert(vals, marshal(fmt.label, a.w)) 248 + if a.dn then kind = 2 249 + table.insert(vals, marshal(fmt.u8, a.dn)) 250 + if a.mn then kind = 3 251 + table.insert(vals, marshal(fmt.u8, a.mn)) 252 + if a.nn then kind = 4 253 + table.insert(vals, marshal(fmt.u8, a.nn)) 254 + end 255 + end 256 + end 257 + end 258 + return marshal(fmt.u8,kind) .. table.concat(vals) 259 + end; 260 + decode = function(s) 261 + local kind <const> = parse(fmt.u8, s) 262 + local path = {} 263 + local components <const> = { 264 + {'w',fmt.label}; 265 + {'dn',fmt.u8}; 266 + {'mn',fmt.u8}; 267 + {'nn',fmt.u8}; 268 + } 269 + for i=1,kind do 270 + local label, ty = table.unpack(components[i]) 271 + path[label] = parse(ty,s) 272 + end 273 + return path 274 + end; 275 +} 193 276 fmt.list = function(t,ty) ty = ty or fmt.u32 194 277 return { 195 278 encode = function(a) 196 279 local vals = {marshal(ty, #a)} 197 280 for i=1,#a do 198 281 table.insert(vals, marshal(t, a[i])) 199 282 end ................................................................................ 228 311 local m = {} 229 312 for _,p in pairs(lst) do m[p.key] = p.val end 230 313 return m 231 314 end; 232 315 } 233 316 end 234 317 235 -fmt.form = { 236 - {'form', fmt.u16}; 237 - {'text', fmt.label}; 238 -} 318 +fmt.enum = function(...) 319 + local vals,rmap = {...},{} 320 + for k,v in pairs(vals) do rmap[v] = k-1 end 321 + local ty = fmt.u8 322 + if #vals > 0xffff then ty = fmt.u32 -- just in pathological case 323 + elseif #vals > 0xff then ty = fmt.u16 end 324 + return { 325 + encode = function(a) 326 + if not rmap[a] then error(string.format('"%s" is not part of enum "%s"', a, table.concat(vals,'","')),3) end 327 + return marshal(ty, rmap[a]) 328 + end; 329 + decode = function(s) 330 + local n = parse(ty,s) 331 + if (n+1) > #vals then error(string.format('enum "%s" does not have %u members', table.concat(vals,'","'),n),3) end 332 + return vals[n+1] 333 + end; 334 + } 335 +end 239 336 240 337 fmt.note = { 241 338 {'kind', fmt.tag}; 242 339 {'paras', fmt.list(fmt.string)}; 243 340 } 244 341 342 +fmt.example = { 343 + {'quote',fmt.string}; 344 + {'src',fmt.label}; 345 +} 245 346 fmt.meaning = { 246 347 {'lit', fmt.string}; 348 + {'examples', fmt.list(fmt.example,fmt.u8)}; 247 349 {'notes', fmt.list(fmt.note,fmt.u8)}; 248 350 } 351 + 352 +fmt.phrase = { 353 + {'str',fmt.label}; 354 + {'means',fmt.list(fmt.meaning,fmt.u8)}; 355 + {'xref',fmt.list(fmt.path,fmt.u16)}; 356 +} 249 357 250 358 fmt.def = { 251 359 {'part', fmt.u8}; 252 360 {'branch', fmt.list(fmt.label,fmt.u8)}; 253 361 {'means', fmt.list(fmt.meaning,fmt.u8)}; 254 - {'forms', fmt.list(fmt.form,fmt.u16)}; 362 + {'forms', fmt.map(fmt.u16,fmt.label,fmt.u16)}; 363 + {'phrases', fmt.list(fmt.phrase,fmt.u16)}; 255 364 } 256 365 257 366 fmt.word = { 258 367 {'defs', fmt.list(fmt.def,fmt.u8)}; 259 368 } 260 369 261 370 fmt.dictHeader = { 262 371 {'lang', fmt.tag}; 263 372 {'meta', fmt.string}; 264 373 {'partsOfSpeech', fmt.list(fmt.tag,fmt.u16)}; 374 + {'inflectionForms', fmt.list({ 375 + {'name', fmt.tag}; 376 + {'abbrev', fmt.tag}; 377 + {'desc', fmt.string}; 378 + {'parts', fmt.list(fmt.tag,fmt.u8)}; 379 + -- which parts of speech does this form apply to? 380 + -- leave empty if not relevant 381 + },fmt.u16)}; 265 382 } 266 383 267 -fmt.synonymSet = { 384 +fmt.relSet = { 268 385 {'uid', fmt.u32}; 269 386 -- IDs are persistent random values so they can be used 270 387 -- as reliable identifiers even when merging exports in 271 388 -- a parvan-unaware VCS 272 - {'members', fmt.list({ 273 - {'word', fmt.label}, {'def', fmt.u8}; 274 - },fmt.u16)}; 389 + {'members', fmt.list(fmt.path,fmt.u16)}; 390 + {'kind', fmt.enum('syn','ant','met')}; 275 391 } 276 392 277 393 fmt.dict = { 278 394 {'header', fmt.dictHeader}; 279 395 {'words', fmt.map(fmt.string,fmt.word)}; 280 - {'synonyms', fmt.list(fmt.synonymSet)}; 396 + {'relsets', fmt.list(fmt.relSet)}; 281 397 } 282 398 283 399 function marshal(ty, val) 284 400 if ty.encode then 285 401 return ty.encode(val) 286 402 end 287 403 local ac = {} 288 404 289 405 for idx,fld in ipairs(ty) do 290 406 local name, fty = table.unpack(fld) 291 - table.insert(ac, marshal(fty, assert(val[name]))) 407 + table.insert(ac, marshal(fty, 408 + assert(val[name], 409 + string.format('marshalling error: missing field %s', name) 410 + ) 411 + )) 292 412 end 293 413 294 414 return table.concat(ac) 295 415 end 296 416 297 417 function parse(ty, stream) 298 418 if ty.decode then ................................................................................ 315 435 if map[v] then return map[v] else 316 436 map[v] = i 317 437 i=i+1 318 438 return i-1 319 439 end 320 440 end, map 321 441 end 442 + 443 +local function rebuildRelationCache(d) 444 +-- (re)build a dictionary's relation cache; needed 445 +-- at load time and whenever any changes to relsets 446 +-- are made (unless they're simple enough to update 447 +-- the cache directly by hand, but that's very eeeh) 448 + local sc = {} 449 + for i,s in ipairs(d.relsets) do 450 + for j,m in ipairs(s.members) do 451 + sc[m.w] = sc[m.w] or {} 452 + table.insert(sc[m.w], s) 453 + end 454 + end 455 + d._relCache = sc 456 +end 322 457 323 458 local function 324 459 writeDict(d) 325 460 local atomizePoS, posMap = atomizer() 326 461 for lit,w in pairs(d.words) do 327 462 for j,def in ipairs(w.defs) do 328 463 def.part = atomizePoS(def.part) ................................................................................ 345 480 local d = parse(fmt.dict, s) 346 481 -- handle atoms 347 482 for lit,w in pairs(d.words) do 348 483 for j,def in ipairs(w.defs) do 349 484 def.part = d.header.partsOfSpeech[def.part] 350 485 end 351 486 end 487 + 488 + -- create cachemaps for complex data structures to 489 + -- enable faster lookup that would otherwise require 490 + -- expensive scans 491 + rebuildRelationCache(d) 352 492 return d 353 493 end 494 + 354 495 355 496 local function strwords(str) -- here be dragons 356 497 local wds = {} 357 498 local w = {} 358 499 local state, d, quo, dquo = 0,0 359 - local function flush(n) 360 - if next(w) then 500 + local function flush(n,final) 501 + if next(w) or state ~= 0 and state < 10 then 361 502 table.insert(wds, utf8.char(table.unpack(w))) 362 503 w = {} 504 + elseif final and state > 10 then 505 + table.insert(wds, '\\') 363 506 end 364 507 state = n 365 508 quo = nil 366 509 dquo = nil 367 510 d = 0 368 511 end 369 512 local function isws(c) ................................................................................ 418 561 table.insert(w,0x0a) 419 562 else 420 563 table.insert(w,cp) 421 564 end 422 565 state = state - 10 423 566 end 424 567 end 425 - flush() 568 + flush(nil,true) 426 569 return wds 427 570 end 428 571 429 572 local predicates 430 573 local function parsefilter(str) 431 574 local f = strwords(str) 432 575 if #f == 1 then return function(e) return predicates.lit.fn(e,f[1]) end end ................................................................................ 631 774 632 775 local function copy(tab) 633 776 local new = {} 634 777 for k,v in pairs(tab) do new[k] = v end 635 778 return new 636 779 end 637 780 638 -local function parsePath(p) 639 - local w,dn,mn,nn = p:match('^(.+)@([0-9]+)/([0-9]+):([0-9]+)$') 640 - if not w then w,dn,mn = p:match('^(.+)@([0-9]+)/([0-9]+)$') end 641 - if not w then w,dn = p:match('^(.+)@([0-9]+)$') end 781 +local function pathParse(p) 782 +-- this is cursed, rewrite without regex pls TODO 783 + if p == '.' then return {} end 784 + local function comp(pfx) 785 + return pfx .. '([0-9]+)' 786 + end 787 + local function mtry(...) 788 + local mstr = '^(.+)' 789 + for _, v in ipairs{...} do 790 + mstr = mstr .. comp(v) 791 + end 792 + return p:match(mstr .. '$') 793 + end 794 + 795 + local xn 796 + local w,dn,pn,mn,nn = mtry('%.','/p','/m','/n') 797 + if not w then w,dn,pn,mn,xn = mtry('%.','/p','/m','/x') end 798 + if not w then w,dn,pn,mn = mtry('%.','/p','/m') end 799 + if not w then w,dn,pn= mtry('%.','/p') end 800 + if not w then 801 + local comps = {'%.','/m','/n'} 802 + for i=#comps, 1, -1 do 803 + local args = {table.unpack(comps,1,i)} 804 + w,dn,mn,nn = mtry(table.unpack(args)) 805 + if not w and args[i] == '/n' then 806 + args[i] = '/x' 807 + w,dn,mn,xn = mtry(table.unpack(args)) 808 + end 809 + if w then break end 810 + end 811 + end 642 812 if not w then w=p:match('^(.-)%.?$') end 643 - return {w = w, dn = tonumber(dn), mn = tonumber(mn), nn = tonumber(nn)} 813 + return {w = w, dn = tonumber(dn), mn = tonumber(mn), pn=tonumber(pn); nn = tonumber(nn), xn = tonumber(xn)} 814 +end 815 +local function pathString(p,styler) 816 + local function s(s, st, ...) 817 + if styler then 818 + return styler[st](tostring(s),...) 819 + else return s end 820 + end 821 + 822 + local function comp(c,n,...) 823 + return s('/','color',5) 824 + .. s(string.format("%s%u",c,n), 'color',...) 825 + end 826 + local t = {} 827 + if p.w then t[1] = s(p.w,'ul') else return '.' end 828 + if p.dn then t[2] = string.format(".%s", s(p.dn,'br')) end 829 + if p.pn then t[#t+1] = comp('p',p.pn,4,true) end 830 + if p.mn then t[#t+1] = comp('m',p.mn,5,true) end 831 + if p.xn then t[#t+1] = comp('x',p.xn,6,true) 832 + elseif p.nn then t[#t+1] = comp('n',p.nn,4) end 833 + if t[2] == nil then 834 + return p.w .. '.' --make sure paths are always valid 835 + end 836 + return s(table.concat(t),'em') 837 +end 838 +local function pathMatch(a,b) 839 + return a.w == b.w 840 + and a.dn == b.dn 841 + and a.mn == b.mn 842 + and a.pn == b.pn 843 + and a.nn == b.nn 844 + and a.xn == b.xn 845 +end 846 +local function pathResolve(ctx, a) 847 + if not a.w then return end -- empty paths are valid! 848 + local function lookup(seg, tbl,val) 849 + if not tbl then error('bad table',2) end 850 + local v = tbl[val] 851 + if v then return v end 852 + id10t('bad %s in path: %s', seg, val) 853 + end 854 + 855 + local res = {} 856 + res.word = lookup('word', ctx.dict.words, a.w) 857 + if not a.dn then return res end 858 + 859 + res.def = lookup('definition', w.defs, a.dn) 860 + if (not a.pn) and (not a.mn) then return res end 861 + 862 + local m if a.pn then 863 + res.phrase = lookup('phrase', d.phrases, a.pn) 864 + res.meaning = lookup('meaning', p.means, a.mn) 865 + else 866 + res.meaning = lookup('meaning', d.means, a.mn) 867 + end 868 + 869 + if a.xn then 870 + res.ex = lookup('example',m.examples,a.xn) 871 + elseif a.nn then 872 + res.note = lookup('note',m.notes,a.nn) 873 + end 874 + 875 + return res 876 +end 877 + 878 +local function pathNav(...) 879 + local t = pathResolve(...) 880 + return t.word,t.def,t.phrase,t.meaning,t.ex or t.note 881 +end 882 + 883 +local function pathRef(ctx, a) 884 + local w,d,p,m,n = pathNav(ctx,a) 885 + return n or m or p or d or w 886 +end 887 + 888 +local function pathSub(super,sub) 889 + if super.w == nil then return true end 890 + if sub.w ~= super.w then return false end 891 + 892 + if super.pn == nil then goto checkMN end 893 + if sub.pn ~= super.pn then return false end 894 + 895 + ::checkMN:: 896 + if super.mn == nil then return true end 897 + if sub.mn ~= super.mn then return false end 898 + 899 + if super.xn then 900 + if sub.nn then return false end 901 + if sub.xn ~= super.xn then return false end 902 + elseif super.nn then 903 + if sub.xn then return false end 904 + if sub.nn ~= super.nn then return false end 905 + end 906 + 907 + return true 644 908 end 645 909 646 910 local cmds = { 647 911 create = { 648 912 help = "initialize a new dictionary file"; 649 913 syntax = "<lang>"; 650 914 raw = true; ................................................................................ 654 918 end 655 919 local fd = safeopen(ctx.file,"wb") 656 920 local new = { 657 921 header = { 658 922 lang = lang; 659 923 meta = ""; 660 924 partsOfSpeech = {}; 661 - branch = {}; 925 + inflectionForms = {}; 662 926 }; 663 927 words = {}; 664 - synonyms = {}; 928 + relsets = {}; 665 929 } 666 930 local o = writeDict(new); 667 931 fd:write(o) 668 932 fd:close() 669 933 end; 670 934 }; 671 935 coin = { ................................................................................ 691 955 if not ctx.dict.words[word] then 692 956 ctx.dict.words[word] = {defs={}} 693 957 end 694 958 local n = #(ctx.dict.words[word].defs)+1 695 959 ctx.dict.words[word].defs[n] = { 696 960 part = part; 697 961 branch = etym; 698 - means = {means and {lit=means,notes={}} or nil}; 962 + means = {means and { 963 + lit=means; 964 + examples={}; 965 + notes={}; 966 + } or nil}; 699 967 forms = {}; 700 968 } 701 969 ctx.log('info', string.format('added definition #%u to “%s”', n, word)) 702 970 end; 703 971 }; 704 972 mean = { 705 973 help = "add a meaning to a definition"; 706 974 syntax = "<word> <def#> <meaning>"; 707 975 write = true; 708 976 exec = function(ctx,word,dn,m) 709 - local _,d = safeNavWord(ctx,word,dn) 710 - table.insert(d.means, {lit=m,notes={}}) 977 + local t = pathResolve(ctx,{w=word,dn=dn}) 978 + table.insert(t.d.means, {lit=m,notes={}}) 711 979 end; 712 980 }; 713 - syn = { 714 - help = "manage synonym groups"; 981 + rel = { 982 + help = "manage groups of related words"; 715 983 syntax = { 716 - "(show|purge) <path>"; 984 + "(show|purge) <path> [<kind>]"; 717 985 "(link|drop) <word> <group#> <path>…"; 718 - "new <path> <path>…"; 719 - "clear <word> [<group#>]"; 986 + "new <rel> <path> <path>…"; 987 + "destroy <word> [<group#>]"; 988 + "rel ::= (syn|ant|co)" 720 989 }; 721 990 write = true; 722 - exec = function(ctx, op, tgtw, ...) 991 + exec = function(ctx, op, ...) 992 + local fo = ctx.sty[io.stdout] 993 + if op == nil then id10t "not enough arguments" end 723 994 local groups = {} 724 - local wp = parsePath(tgtw) 725 - local w,d = safeNavWord(ctx, wp.w, wp.dn) 726 - if not (op=='new' or op=='link' or op=='drop' or op=='clear' or op=='show' or op=='purge') then 727 - id10t('invalid operation “%s” for `syn`', op) 995 + if not (op=='new' or op=='link' or op=='drop' or op=='destroy' or op=='show' or op=='purge') then 996 + id10t('invalid operation “%s” for `rel`', op) 728 997 end 729 998 if op == 'new' then 730 - local links = {{word = wp.w, def = wp.dn or 1}} 731 - for i,l in ipairs{...} do 732 - local parsed = parsePath(l) 733 - links[i+1] = {word = parsed.w, def = parsed.dn or 1} 999 + local rel = ... 1000 + if rel ~= 'syn' and rel ~= 'ant' and rel ~= 'met' then 1001 + id10t 'relationships must be synonymous, antonymous, or metonymous' 734 1002 end 735 - table.insert(ctx.dict.synonyms, { 736 - uid=math.random(0,0xffffFFFF); 1003 + local links={} 1004 + for i,l in ipairs{select(2,...)} do 1005 + links[i] = pathParse(l) 1006 + end 1007 + local newstruct = { 1008 + uid=math.random(1,0xffffFFFF); 737 1009 members=links; 738 - }) 1010 + kind = rel; 1011 + } 1012 + table.insert(ctx.dict.relsets, newstruct) 1013 + 1014 + local rc = ctx.dict._relCache 1015 + for i,l in pairs(links) do 1016 + rc[l.w] = rc[l.w] or {} 1017 + table.insert(rc[l.w], newstruct) 1018 + end 1019 + rebuildRelationCache(ctx.dict) 739 1020 else -- assemble a list of groups 740 - for i,ss in ipairs(ctx.dict.synonyms) do 1021 + local tgtw = ... 1022 + local wp = pathParse(tgtw) 1023 + local w,d,m = pathNav(ctx, wp) 1024 + for i,ss in ipairs(ctx.dict.relsets) do 741 1025 for j,s in ipairs(ss.members) do 742 - if s.word == wp.w and (wp.dn == nil or s.def == wp.dn) then 743 - table.insert(groups, {set = ss, mem = s}) 1026 + if pathSub(s, wp) then 1027 +-- if s.word == wp.w and (wp.dn == nil or s.def == wp.dn) then 1028 + table.insert(groups, {set = ss, mem = s, id = i}) 744 1029 break 745 1030 end 746 1031 end 747 1032 end 748 1033 749 1034 if op == 'show' then 750 1035 for i, g in ipairs(groups) do 751 - local w,d = safeNavWord(ctx, g.mem.word, g.mem.def) 752 - local function label(wd,defn) 753 - local fulldef = {} 754 - for i,v in ipairs(defn.means) do 755 - fulldef[i] = v.lit 1036 + local w = pathResolve(ctx, {w=g.mem.w}).w 1037 + local function label(path,w) 1038 + local repr = path.w 1039 + if path.dn then 1040 + repr = repr .. string.format("(%s)", w.defs[path.dn].part) 1041 + if path.mn then 1042 + repr = repr .. string.format(": %u. %s", path.dn, w.defs[path.dn].means[path.mn].lit) 1043 + else 1044 + local fulldef = {} 1045 + for i,v in ipairs(w.defs) do 1046 + fulldef[i] = v.lit 1047 + end 1048 + repr = repr..table.concat(fulldef, '; ') 1049 + end 756 1050 end 757 - fulldef = table.concat(fulldef, '; ') 758 - return string.format("%s(%s): %s",wd,defn.part,fulldef) 1051 + 1052 + return repr 759 1053 end 1054 + 760 1055 local others = {} 761 1056 for j, o in ipairs(g.set.members) do 762 - if not (o.word == g.mem.word and o.def == (wp.dn or 1)) then 763 - local ow, od = safeNavWord(ctx, o.word,o.def) 764 - table.insert(others, ' '..label(o.word,od)) 1057 + local ow = pathResolve(ctx, {w=o.w}).w 1058 + if (g.set.kind == 'ant' or not pathMatch(o, g.mem)) and 1059 + --exclude antonym headwords 1060 + not (g.set.kind == 'ant' and j==1) then 1061 + table.insert(others, ' '..label(o,ow)) 1062 + end 1063 + end 1064 + local llab do 1065 + local cdw = ctx.dict.words 1066 + if g.set.kind == 'ant' then 1067 + local ap = g.set.members[1] 1068 + llab = fo.br(label(ap,cdw[ap.w]) or '') 1069 + else 1070 + llab = fo.br(label(g.mem,cdw[g.mem.w]) or '') 765 1071 end 766 1072 end 767 - io.stdout:write(string.format("% 4u) %s\n%s", i, label(g.mem.word,d),table.concat(others,'\n'))) 1073 + local kls = { 1074 + syn = fo.color('synonyms',2,true)..' of'; 1075 + ant = fo.color('antonyms',1,true)..' of'; 1076 + met = fo.color('metonyms',4,true)..' of'; 1077 + } 1078 + io.stdout:write(string.format("% 4u) %s\n%s", i, fo.ul(kls[g.set.kind] .. ' ' .. llab), table.concat(others,'\n')) .. '\n') 768 1079 end 1080 + return false -- no changes made 769 1081 elseif op == 'link' or op == 'drop' then 770 - local tgtn, paths = (...), { select(2, ...) } 1082 + local tgtn, paths = (select(2,...)), { select(3, ...) } 1083 + rebuildRelationCache(ctx.dict) 1084 + elseif op == 'destroy' then 1085 + local tgtw, tgtn = ... 1086 + if not tgtn then id10t 'missing group number' end 1087 + local delendum = groups[tonumber(tgtn)] 1088 + if not delendum then id10t 'bad group number' end 1089 + local rs = ctx.dict.relsets 1090 + local last = #rs 1091 + if delendum.id == last then 1092 + rs[delendum.id] = nil 1093 + else -- since order doesn't matter, we can use a 1094 + -- silly swapping trick to reduce the deletion 1095 + -- worst case from O(n) to O(2) 1096 + rs[delendum.id] = rs[last] 1097 + rs[last] = nil 1098 + end 1099 + rebuildRelationCache(ctx.dict) 1100 + else 1101 + id10t 'invalid operation' 771 1102 end 772 1103 end 773 1104 end; 774 1105 }; 775 1106 mod = { 776 1107 help = "move, merge, split, or delete words or definitions"; 777 1108 syntax = { ................................................................................ 783 1114 note = { 784 1115 help = "add a note to a definition or a paragraph to a note"; 785 1116 syntax = {"(<m-path> (add|for) <kind> | <m-path>:<note#>) <para>…"; 786 1117 "m-path ::= <word>@<def#>/<meaning#>"}; 787 1118 write = true; 788 1119 exec = function(ctx,path,...) 789 1120 local paras, mng 790 - local dest = parsePath(path) 1121 + local dest = pathParse(path) 791 1122 local _,_,m = safeNavWord(ctx,dest.w,dest.dn,dest.mn) 792 1123 if dest.nn then 793 1124 paras = {...} 794 1125 else 795 1126 local op, kind = ... 796 1127 paras = { select(3, ...) } 797 1128 if op == 'add' then ................................................................................ 828 1159 predicates = { 829 1160 help = "show available filter predicates"; 830 1161 nofile = true; 831 1162 syntax = "[<predicate>]"; 832 1163 }; 833 1164 export = { 834 1165 help = "create a text file dump compatible with source control"; 1166 + syntax = "[<target-file>]"; 1167 + }; 1168 + import = { 1169 + help = "generate a usable dictionary from a text export file"; 1170 + syntax = "[<input-file>]"; 1171 + raw = true; 1172 + write = true; 835 1173 }; 836 1174 dump = { 837 1175 exec = function(ctx) print(dump(ctx.dict)) end 838 1176 }; 839 1177 ls = { 840 1178 help = "list all words that meet any given <filter>"; 841 1179 syntax = {"[<filter>…]"; ................................................................................ 848 1186 local list = predicates 849 1187 if pred then list = {predicates[pred]} end 850 1188 local f = ctx.sty[io.stderr] 851 1189 for k,p in pairs(predicates) do 852 1190 if p.help then 853 1191 io.stderr:write( 854 1192 f.br(' - ' .. 855 - f.rgb('[',.8,.3,1) .. 1193 + f.rgb('[',1,0,.5) .. 856 1194 k .. ' ' .. 857 1195 (f.color(p.syntax,5) or '…') .. 858 - f.rgb(']',.8,.3,1)) .. ': ' .. 1196 + f.rgb(']',1,0,.5)) .. ': ' .. 859 1197 f.color(p.help,4,true) .. '\n') 860 1198 end 861 1199 end 862 1200 end 863 1201 864 1202 function cmds.ls.exec(ctx,...) 865 1203 local filter = nil ................................................................................ 875 1213 local e = {lit=lit, word=w} 876 1214 if filter == nil or filter(e) then 877 1215 table.insert(out, e) 878 1216 end 879 1217 end 880 1218 table.sort(out, function(a,b) return a.lit < b.lit end) 881 1219 local fo = ctx.sty[io.stdout] 882 - local function meanings(d,md,n) 1220 + 1221 + local function gatherRelSets(path) 1222 + local antonymSets, synonymSets, metonymSets = {},{},{} 1223 + if ctx.dict._relCache[path.w] then 1224 + for i, rel in ipairs(ctx.dict._relCache[path.w]) do 1225 + local specuset,tgt,anto = {} 1226 + for j, mbr in ipairs(rel.members) do 1227 + if pathMatch(mbr, path) then 1228 + if rel.kind == 'syn' then tgt = synonymSets 1229 + elseif rel.kind == 'met' then tgt = metonymSets 1230 + elseif rel.kind == 'ant' then 1231 + if j == 1 -- is this the headword? 1232 + then tgt = antonymSets 1233 + else tgt = synonymSets 1234 + end 1235 + end 1236 + elseif j == 1 and rel.kind == 'ant' then 1237 + anto = mbr 1238 + else 1239 + table.insert(specuset, mbr) 1240 + end 1241 + end 1242 + if tgt then 1243 + table.insert(tgt, specuset) 1244 + if anto then 1245 + table.insert(antonymSets, {anto}) 1246 + end 1247 + end 1248 + end 1249 + end 1250 + local function flatten(lst) 1251 + local new = {} 1252 + for i, l in ipairs(lst) do tcatD(new, l) end 1253 + return new 1254 + end 1255 + return { 1256 + syn = flatten(synonymSets); 1257 + ant = flatten(antonymSets); 1258 + met = flatten(metonymSets); 1259 + } 1260 + end 1261 + 1262 + local function formatRels(rls, padlen) 1263 + -- optimize for the common case 1264 + if next(rls.syn) == nil and 1265 + next(rls.ant) == nil and 1266 + next(rls.met) == nil then return {} end 1267 + local pad = string.rep(' ',padlen) 1268 + local function format(label, set) 1269 + local each = map(set, function(e) 1270 + local ew,ed = pathNav(ctx, e) 1271 + local str = fo.ul(e.w) 1272 + if ed then str = string.format('%s(%s)',str,ed.part) end 1273 + if e.mn then str = string.format('%s§%u',str,e.mn) end 1274 + return str 1275 + end) 1276 + return fo.em(string.format("%s%s %s",pad,label,table.concat(each,', '))) 1277 + end 1278 + local lines = {} 1279 + local function add(l,c,lst) 1280 + table.insert(lines, format(fo.color(l,c,true),lst)) 1281 + end 1282 + if next(rls.syn) then add('synonyms:',2,rls.syn) end 1283 + if next(rls.ant) then add('antonyms:',1,rls.ant) end 1284 + if next(rls.met) then add('metonyms:',4,rls.met) end 1285 + return lines 1286 + end 1287 + 1288 + local function meanings(w,d,md,n) 883 1289 local start = md and 2 or 1 884 1290 local part = string.format('(%s)', d.part) 885 1291 local pad = md and string.rep(' ', #part) or '' 886 1292 local function note(n,insert) 887 1293 if not next(n.paras) then return end 888 1294 local pad = string.rep(' ',#(n.kind) + 9) 889 1295 insert(' ' .. fo.hl(' ' .. n.kind .. ' ') .. ' ' .. n.paras[1]) 890 1296 for i=2,#n.paras do 891 1297 insert(pad..n.paras[2]) 892 1298 end 893 1299 end 894 1300 local m = { (function() 895 1301 if d.means[1] then 896 - if md then return 897 - string.format(" %s 1. %s", fo.em(part), d.means[1].lit) 1302 + if md then 1303 + local id = '' 1304 + if ctx.flags.ident then 1305 + id=' ['..pathString({w=w.lit,dn=n,mn=1}, fo)..']' 1306 + end 1307 + return string.format(" %s %s 1. %s", id, fo.em(part), d.means[1].lit) 898 1308 end 899 1309 else return 900 1310 fo.em(string.format(' %s [empty definition #%u]', part,n)) 901 1311 end 902 1312 end)() } 1313 + tcatD(m, formatRels(gatherRelSets{w=w.lit,dn=n,mn=1}, 6)) 903 1314 for i=start,#d.means do local v = d.means[i] 904 - table.insert(m, string.format(' %s %u. %s', pad, i, v.lit)) 1315 + local id = '' 1316 + if ctx.flags.ident then id='['..pathString({w=w.lit,dn=n,mn=n}, fo)..']' end 1317 + table.insert(m, string.format(' %s%s %u. %s', pad, id, i, v.lit)) 1318 + tcatD(m, formatRels(gatherRelSets{w=w.lit,dn=n,mn=i}, 6)) 905 1319 for j,n in ipairs(v.notes) do 906 1320 note(n, function(v) table.insert(m, v) end) 907 1321 end 908 1322 end 909 1323 return table.concat(m,'\n') 910 1324 end 1325 + local function autobreak(str) 1326 + if str ~= '' then return str..'\n' else return str end 1327 + end 911 1328 for i, w in ipairs(out) do 912 - local d = fo.ul(w.lit) 1329 + local d = fo.ul(fo.br(w.lit)) 1330 + local wordrels = autobreak(table.concat( 1331 + formatRels(gatherRelSets{w=w.lit}, 2), 1332 + '\n' 1333 + )) 1334 + local wc = ctx.dict._relCache[w.lit] 913 1335 if #w.word.defs == 1 then 914 - d=d .. ' ' .. fo.em('('..(w.word.defs[1].part)..')') ..'\n' 915 - .. meanings(w.word.defs[1],false,1) 1336 + d=d .. ' ' 1337 + .. fo.rgb(fo.em('('..(w.word.defs[1].part)..')'),.8,.5,1) .. '\n' 1338 + .. meanings(w,w.word.defs[1],false,1) .. '\n' 1339 + .. autobreak(table.concat(formatRels(gatherRelSets{w=w.lit,dn=1}, 4), '\n')) 1340 + .. wordrels .. '\n' 916 1341 else 917 1342 for j, def in ipairs(w.word.defs) do 918 - d=d .. '\n' .. meanings(def,true,j) 919 - end 920 - end 921 - io.stdout:write(d..'\n') 922 - end 923 -end 924 - 925 -function cmds.export.exec(ctx) 1343 + local syn if wsc and wsc[j] then syn = wsc[j] end 1344 + d=d .. '\n' 1345 + .. meanings(w,syn,def,true,j) .. '\n' 1346 + .. autobreak(table.concat( 1347 + formatRels(gatherRelSets{w=w.lit,dn=j}, 4), 1348 + '\n' 1349 + )) 1350 + end 1351 + d=d .. wordrels .. '\n' 1352 + end 1353 + io.stdout:write(d) 1354 + end 1355 +end 1356 + 1357 +function cmds.import.exec(ctx,file) 1358 + local ifd = io.stdin 1359 + if file then 1360 + ifd = safeopen(file,'r') 1361 + end 1362 + 1363 + local new = { 1364 + header = { 1365 + lang = lang; 1366 + meta = ""; 1367 + partsOfSpeech = {}; 1368 + inflectionForms = {}; 1369 + }; 1370 + words = {}; 1371 + relsets = {}; 1372 + } 1373 + 1374 + local state = 0 1375 + local relsets = {} 1376 + local path = {} 1377 + local inflmap, lastinfl = {} 1378 + for l in ifd:lines() do 1379 + local words = strwords(l) 1380 + local c = words[1] 1381 + local function syn(mn,mx) 1382 + local nw = #words - 1 1383 + if nw < mn or (mx ~= nil and nw > mx) then 1384 + if mx ~= nil then 1385 + id10t('command %s needs between %u~%u words',c,mn,mx) 1386 + else 1387 + id10t('command %s needs at least %u words',c,mn) 1388 + end 1389 + end 1390 + end 1391 + if c ~= '*' and c~='meta' then -- comments 1392 + if state == 0 then 1393 + if c ~= 'pv0' then 1394 + id10t "not a parvan export" 1395 + end 1396 + new.header.lang = words[2] 1397 + new.header.meta = words[3] 1398 + state = 1 1399 + else 1400 + print(pathString(path, ctx.sty[io.stderr])) 1401 + local W,D,M,N = pathNav({dict=new}, path) 1402 + if c == 'w' then syn(1) state = 2 1403 + path = {w=words[2]} 1404 + new.words[words[2]] = {defs={}} 1405 + elseif c == 'f' then syn(1) 1406 + local nf = { 1407 + name = words[2]; 1408 + abbrev = words[3] or ""; 1409 + desc = words[4] or ""; 1410 + parts = {}; 1411 + } 1412 + table.insert(new.header.inflectionForms, nf) 1413 + inflmap[words[2]] = #(new.header.inflectionForms) 1414 + lastinfl = nf 1415 + elseif c == 'fp' then syn(1) 1416 + if not lastinfl then 1417 + id10t 'fp can only be used after f' end 1418 + table.insert(lastinfl.parts,words[2]) 1419 + elseif c == 's' then syn(2) 1420 + relsets[words[3]] = relsets[words[3]] or {} 1421 + relsets[words[3]].kind = words[2] 1422 + relsets[words[3]].uid = tonumber(words[3]) 1423 + relsets[words[3]].members = relsets[words[3]].members or {} 1424 + elseif state >= 2 and c == 'r' then syn(1) 1425 + relsets[words[2]] = relsets[words[2]] or { 1426 + uid = tonumber(words[2]); 1427 + members={}; 1428 + } 1429 + table.insert(relsets[words[2]].members, path) 1430 + elseif state >= 2 and c == 'd' then syn(1) state = 3 1431 + table.insert(W.defs, { 1432 + part = words[2]; 1433 + branch = {}; 1434 + means = {}; 1435 + forms = {}; 1436 + phrases = {}; 1437 + }) 1438 + path = {w = path.w, dn = #(W.defs)} 1439 + elseif state >= 3 and c == 'dr' then syn(1) 1440 + table.insert(D.branch, words[2]) 1441 + elseif state >= 3 and c == 'df' then syn(2) 1442 + if not inflmap[words[2]] then 1443 + id10t('no inflection form %s defined', words[2]) 1444 + end 1445 + D.forms[inflmap[words[2]]] = words[3] 1446 + elseif state >= 3 and c == 'm' then syn(1) state = 4 1447 + table.insert(D.means, { 1448 + lit = words[2]; 1449 + notes = {}; 1450 + examples = {}; 1451 + }); 1452 + path = {w = path.w, dn = path.dn, mn = #(D.means)} 1453 + elseif state >= 4 and c == 'n' then syn(1) state = 5 1454 + table.insert(M.notes, {kind=words[2], paras={}}) 1455 + path = {w = path.w, dn = path.dn, mn = path.mn, nn = #(M.notes)}; 1456 + elseif state >= 5 and c == 'np' then syn(1) 1457 + table.insert(N.paras, words[2]) 1458 + end 1459 + -- we ignore invalid ctls, for sake of forward-compat 1460 + end 1461 + end 1462 + end 1463 + 1464 + for k,v in pairs(relsets) do 1465 + if not v.uid then 1466 + --handle non-numeric export ids 1467 + v.uid = math.random(0,0xffffFFFF) 1468 + end 1469 + table.insert(new.relsets, v) 1470 + end 1471 + 1472 + local ofd = safeopen(ctx.file,"w+b") 1473 + local o = writeDict(new); 1474 + ofd:write(o) 1475 + ofd:close() 1476 +end 1477 + 1478 +function cmds.export.exec(ctx,file) 1479 + local ofd = io.stdout 1480 + if file then ofd = safeopen(file, 'w+') end 926 1481 local function san(str) 927 1482 local d = 0 928 1483 local r = {} 929 1484 for i,cp in utf8.codes(str) do 930 1485 -- insert backslashes for characters that would 931 1486 -- disrupt strwords() parsing 932 - if cp == 0x5b then 933 - d = d + 1 934 - elseif cp == 0x5d then 935 - if d >= 1 then 936 - d = d - 1 937 - else 938 - table.insert(r, 0x5c) 1487 + if cp == 0x0a then 1488 + table.insert(r, 0x5c) 1489 + table.insert(r, 0x6e) 1490 + else 1491 + if cp == 0x5b then 1492 + d = d + 1 1493 + elseif cp == 0x5d then 1494 + if d >= 1 then 1495 + d = d - 1 1496 + else 1497 + table.insert(r, 0x5c) 1498 + end 939 1499 end 1500 + table.insert(r, cp) 940 1501 end 941 - table.insert(r, cp) 942 1502 end 943 1503 return '[' .. utf8.char(table.unpack(r)) .. ']' 944 1504 end 945 - local function o(...) io.stdout:write(string.format(...)..'\n') end 1505 + local function o(lvl,...) 1506 + local pfx = '' 1507 + if ctx.flags.human and lvl > 0 then 1508 + pfx = string.rep('\t', lvl) 1509 + end 1510 + ofd:write(pfx..string.format(...)..'\n') 1511 + end 946 1512 local d = ctx.dict 947 - o('pv0 %s %s', san(d.header.lang), san(d.header.meta)) 1513 + o(0,'pv0 %s %s', san(d.header.lang), san(d.header.meta)) 1514 + local function checksyn(obj) 1515 +-- for _,s in ipairs(d.synonyms) do 1516 + local lvl = 0 1517 + if obj.nn then lvl = 4 1518 + elseif obj.mn then lvl = 3 1519 + elseif obj.dn then lvl = 2 1520 + elseif obj.w then lvl = 1 end 1521 + if not d._relCache[obj.w] then return end 1522 + for _,s in ipairs(d._relCache[obj.w]) do 1523 + for _,sm in ipairs(s.members) do 1524 + if pathMatch(obj, sm) then 1525 + o(lvl,'r %u',s.uid) 1526 + break 1527 + end 1528 + end 1529 + end 1530 + end 1531 + for i,f in pairs(d.header.inflectionForms) do 1532 + o(0,'f %s %s %s', san(f.name), san(f.abbrev), san(f.desc)) 1533 + for j,p in pairs(f.parts) do 1534 + o(1,'fp %s', san(p)) 1535 + end 1536 + end 948 1537 for lit, w in pairs(d.words) do 949 - o('w %s',san(lit)) 1538 + o(0,'w %s',san(lit)) 1539 + checksyn{w=lit} 950 1540 for i,def in ipairs(w.defs) do 951 - o('d %s',san(def.part)) 952 - for _,s in ipairs(d.synonyms) do 953 - for _,sm in ipairs(s.members) do 954 - if sm.word == w and sm.def == i then 955 - o('ds %u',s.uid) 956 - break 957 - end 958 - end 959 - end 1541 + o(1,'d %s',san(def.part)) 1542 + checksyn{w=lit,dn=i} 960 1543 for j,r in ipairs(def.branch) do 961 - o('dr %s',san(r)) 1544 + o(2,'dr %s',san(r)) 962 1545 end 963 1546 for j,m in ipairs(def.means) do 964 - o('m %s', san(m.lit)) 1547 + o(2,'m %s', san(m.lit)) 1548 + checksyn{w=lit,dn=i,mn=j} 965 1549 for k,n in ipairs(m.notes) do 966 - o('n %s', san(n.kind)) 1550 + o(3,'n %s', san(n.kind)) 967 1551 for a,p in ipairs(n.paras) do 968 - o('np %s', san(p)) 1552 + o(4,'np %s', san(p)) 969 1553 end 970 1554 end 971 1555 end 972 1556 end 973 1557 end 974 - for _,s in ipairs(d.synonyms) do o('s %u', s.uid) end 1558 + for _,s in ipairs(d.relsets) do o(0,'s %s %u', s.kind, s.uid) end 1559 +end 1560 + 1561 +local function filterD(lst, fn) 1562 +-- cheap algorithm to destructively filter a list 1563 +-- DOES NOT preserve order!! 1564 + local top = #lst 1565 + for i=top,1,-1 do local m = lst[i] 1566 + if not fn(m,i) then 1567 + lst[i] = lst[top] 1568 + lst[top] = nil 1569 + top = top - 1 1570 + end 1571 + end 1572 + return lst 975 1573 end 976 1574 977 1575 function cmds.mod.exec(ctx, orig, oper, dest, ...) 978 1576 if (not orig) or not oper then 979 1577 id10t '`mod` requires at least an origin and an operation' 980 1578 end 981 - local op, dp = parsePath(orig) 1579 + local op, dp = pathParse(orig) 982 1580 local w,d,m,n = safeNavWord(ctx, op.w,op.dn,op.mn,op.nn) 1581 + -- unfortunately, "pointers" exist elsewhere in the 1582 + -- structure, currently just from relsets, that must 1583 + -- be updated whenever an object moves or changes. 1584 + -- this is miserable and takes a lot of work, using 1585 + -- algorithms provided by the following functions. 1586 + -- note that we don't bother trying to update the 1587 + -- relcache as we go, it's simply not worth the work; 1588 + -- instead we simply rebuild the whole cache when 1589 + -- this command returns 1590 + local function cleanupRels(path, fn) 1591 + local rc = ctx.dict._relCache[path.w] 1592 + if rc then 1593 + for k,s in pairs(rc) do fn(s,k) end 1594 + end 1595 + end 1596 + local function cleanupRelsEach(path, fn) 1597 + cleanupRels(path, function(s,k) 1598 + local top = #s.members 1599 + for i=1,top do local m=s.members[i] 1600 + if pathSub(path, m) then 1601 + local val = fn(m,s,i) 1602 + if val ~= nil then 1603 + s.members[i] = val 1604 + end 1605 + end 1606 + end 1607 + end) 1608 + end 1609 + local function deleteRefsTo(path) 1610 + cleanupRels(path, function(s) 1611 + -- antonyms: delete the headword and transform the group 1612 + -- to a list of synonyms 1613 + if s.kind == 'ant' and pathSub(path,s.members[1]) then 1614 + s.kind = 'syn' 1615 + end 1616 + filterD(s.members, function(m) 1617 + return not pathSub(path, m) 1618 + end) 1619 + end) 1620 + if not path.dn then 1621 + ctx.dict._relCache[path.w] = nil 1622 + end 1623 + end 1624 + local function moveRelTree(op,dp) 1625 + cleanupRelsEach(op, function(old,set,idx) 1626 + local new = {} 1627 + for _,elt in pairs{'w','dn','mn','nn'} do 1628 + if dp[elt] ~= nil then 1629 + new[elt] = dp[elt] 1630 + else 1631 + new[elt] = op[elt] or old[elt] 1632 + end 1633 + end 1634 + return new 1635 + end) 1636 + end 1637 + local function shiftRelTree(dp, fld, nid, amt) 1638 + local cleanupTargetMask = ({ 1639 + dn = {w=dp.w}; 1640 + mn = {w=dp.w,dn=dp.dn}; 1641 + nn = {w=dp.w,dn=dp.dn,mn=dp.mn}; 1642 + })[fld] -- >____< 1643 + cleanupRelsEach(cleanupTargetMask, function(old,set,i) 1644 + if old[fld] >= nid then 1645 + old[fld] = old[fld] + amt 1646 + end 1647 + end) 1648 + end 1649 + local function insertAndMoveRelTree(tbl,n,op,dp,fld) 1650 + local nid = #tbl 1651 + local path = copy(dp) 1652 + path[fld] = nid 1653 + tbl[nid] = n 1654 + shiftRelTree(dp,fld,1) 1655 + moveRelTree(op, path) 1656 + end 983 1657 if oper == 'drop' then 1658 + -- clean out the cache and delete relationships 1659 + deleteRefsTo(op) 984 1660 if not d then 985 1661 ctx.dict.words[op.w] = nil 986 1662 elseif not m then 987 1663 table.remove(w.defs, op.dn) 988 1664 elseif not n then 989 1665 table.remove(d.means, op.mn) 990 1666 else 991 1667 table.remove(m.notes, op.nn) 992 1668 end 993 1669 elseif oper == 'out' then 994 - if n or not m then 995 - id10t '`mod out` must target a meaning' 996 - end 1670 + if n or not m then id10t '`mod out` must target a meaning' end 997 1671 if not dest then id10t '`mod out` requires at least a part of speech' end 998 1672 local newdef = { 999 1673 part = dest; 1000 1674 branch = {...}; 1001 1675 forms = {}; 1002 1676 means = {m}; 1003 1677 } 1678 + shiftRelTree(op, 'dn', op.dn, 1) 1004 1679 table.insert(w.defs,op.dn+1, newdef) 1680 + moveRelTree(op,{w=op.w, dn=op.dn+1, mn=1}) 1005 1681 table.remove(d.means,op.mn) 1006 1682 elseif oper == 'move' or oper == 'merge' or oper == 'clobber' then 1007 1683 if dest 1008 - then dp = parsePath(dest) 1684 + then dp = pathParse(dest) 1009 1685 else id10t('`mod %s` requires a target',oper) 1010 1686 end 1011 1687 if n then 1012 1688 if not dp.mn then 1013 1689 id10t '`mod` on a note requires a note or meaning destination' 1014 1690 end 1015 1691 local _,_,dm = safeNavWord(ctx, dp.w,dp.dn,dp.mn) 1016 1692 if dp.nn then 1017 1693 if oper == 'move' then 1694 + shiftRelTree(dp, 'nn', dp.nn, 1) 1018 1695 table.insert(dm.notes, dp.nn, n) 1019 1696 elseif oper == 'merge' then 1020 1697 local top = #(dm.notes[dp.nn].paras) 1021 1698 for i, v in ipairs(n.paras) do 1022 1699 dm.notes[dp.nn].paras[i+top] = v 1023 1700 end 1024 1701 elseif oper == 'clobber' then 1702 + deleteRefsTo(dp) 1025 1703 dm.notes[dp.nn] = n 1026 1704 end 1705 + moveRelTree(op,dp) 1027 1706 else 1028 1707 if oper ~= 'move' then 1029 1708 id10t('`mod note %s` requires a note target', oper) 1030 1709 end 1031 - table.insert(dm.notes, n) 1710 + insertAndMoveRelTree(dm.notes,n,op,dp,'nn') 1032 1711 end 1033 1712 if oper == 'move' and dp.nn and dm == m and op.nn > dp.nn then 1034 1713 table.remove(m.notes,op.nn+1) 1035 1714 else 1036 1715 table.remove(m.notes,op.nn) 1037 1716 end 1038 1717 elseif m then ................................................................................ 1039 1718 if not dp.dn then 1040 1719 local newdef = { 1041 1720 part = d.part; 1042 1721 branch = copy(d.branch); 1043 1722 forms = copy(d.forms); 1044 1723 means = {m}; 1045 1724 } 1725 + local didx 1046 1726 if ctx.dict.words[dp.w] then 1047 - table.insert(ctx.dict.words[dp.w].defs, newdef) 1727 + local defst = ctx.dict.words[dp.w].defs 1728 + didx = #defst 1729 + defst[didx] = newdef 1048 1730 else 1049 1731 ctx.dict.words[dp.w] = { 1050 1732 defs = {newdef}; 1051 1733 } 1734 + didx = 1 1052 1735 end 1736 + cleanupRelsEach(op, function(oldpath,set,mi) 1737 + return {w=dp.w,dn=didx,mn=1,nn=oldpath.nn} 1738 + end) 1053 1739 table.remove(d.means,dp.mn) 1054 1740 else 1055 1741 local dw, dd = safeNavWord(ctx, dp.w, dp.dn) 1056 1742 if dp.mn then 1057 1743 if dd.means[dp.mn] and (oper == 'merge' or oper=='clobber') then 1058 1744 if oper == 'merge' then 1059 1745 dd.means[dp.mn] = dd.means[dp.mn] .. '; ' .. m 1060 1746 elseif oper == 'clobber' then 1747 + deleteRefsTo(dp) 1061 1748 dd.means[dp.mn] = m 1062 1749 end 1063 1750 else 1064 - if oper == clobber then dd.means = {} end 1751 + cleanupRelsEach({w=dp.w,dn=dp.dn}, function(old,set,i) 1752 + if old.mn >= dp.mn then 1753 + old.mn = old.mn + 1 1754 + end 1755 + end) 1065 1756 table.insert(dd.means, dp.mn, m) 1066 1757 end 1758 + moveRelTree(op,dp) 1067 1759 else 1068 - table.insert(dd.means, m) 1760 + insertAndMoveRelTree(dd.means,m, op,dp,'mn') 1761 +-- table.insert(dd.means, m) 1069 1762 end 1070 1763 if oper == 'move' and dp.mn and dd.means == d.means and op.mn > dp.mn then 1071 1764 table.remove(d.means,op.mn+1) 1072 1765 else 1073 1766 table.remove(d.means,op.mn) 1074 1767 end 1075 1768 end ................................................................................ 1078 1771 if dp.dn then 1079 1772 if oper == 'merge' then 1080 1773 local top = #(ddefs[dp.dn].means) 1081 1774 for i,om in ipairs(d.means) do 1082 1775 ddefs[dp.dn].means[top+i] = om 1083 1776 end 1084 1777 for k,p in pairs(d.forms) do 1778 + deleteRefsTo(dp) 1085 1779 ddefs[dp.dn].forms[k] = p -- clobbers! 1086 1780 end 1087 1781 else 1782 + shiftRelTree(dp,'dn',dp.dn,1) 1088 1783 table.insert(ddefs, dp.dn, d) 1089 1784 end 1785 + moveRelTree(op,dp) 1090 1786 else 1091 - table.insert(ddefs, d) 1787 + insertAndMoveRelTree(ddefs,d, op,dp,'dn') 1788 +-- table.insert(ddefs, d) 1092 1789 end 1093 1790 if oper == 'move' and dp.mn and w.defs == ddefs and op.mn > dp.mn then 1094 1791 table.remove(w.defs,op.dn+1) 1095 1792 else 1096 1793 table.remove(w.defs,op.dn) 1097 1794 end 1098 1795 else 1099 1796 if ctx.dict.words[dp.w] then 1100 1797 if oper ~= 'merge' then 1101 1798 id10t('the word “%s” already exists; use `merge` if you want to merge the words together', dp.w) 1102 1799 end 1103 1800 for i,def in ipairs(w.defs) do 1801 + local odp = copy(op) odp.dn = i 1802 + local ddp = {w=dp.w, dn=dp.dn+i-1} 1104 1803 if dp.dn then 1804 + shiftRelTree(dp, 'dn', dp.dn+i-1, 1) 1105 1805 table.insert(ctx.dict.words[dp.w].defs, dp.dn+i-1, def) 1806 + moveRelTree(odp,ddp) 1106 1807 else 1107 - table.insert(ctx.dict.words[dp.w].defs, def) 1808 +-- table.insert(ctx.dict.words[dp.w].defs, def) 1809 + insertAndMoveRelTree(ctx.dict.words[dp.w].defs, def, 1810 + odp,dp,'dn') 1108 1811 end 1109 1812 end 1110 1813 else 1111 1814 ctx.dict.words[dp.w] = w 1815 + moveRelTree(op,dp) 1816 +-- ctx.dict._relCache[dp.w] = ctx.dict._relCache[op.w] 1817 +-- ctx.dict._relCache[op.w] = nil 1112 1818 end 1113 1819 ctx.dict.words[op.w] = nil 1114 1820 end 1115 1821 end 1822 + rebuildRelationCache(ctx.dict) 1116 1823 end 1117 1824 1118 1825 local function fileLegible(file) 1119 1826 -- check if we can access the file 1120 1827 local fd = io.open(file,"rb") 1121 1828 local ret = false 1122 1829 if fd then ret = true end 1123 1830 fd:close() 1124 1831 return ret 1125 1832 end 1126 1833 1127 -local function map(fn,lst) 1128 - local new = {} 1129 - for k,v in pairs(lst) do 1130 - local nv, nk = fn(v,k) 1131 - new[nk or k] = nv 1132 - end 1133 - return new 1134 -end 1135 -local function mapD(fn,lst) --destructive 1136 - -- WARNING: this will not work if nk names an existing key! 1137 - for k,v in pairs(lst) do 1138 - local nv, nk = fn(v,k) 1139 - if nk == nil or k == nk then 1140 - lst[k] = nv 1141 - else 1142 - lst[k] = nil 1143 - lst[nk] = nv 1144 - end 1145 - end 1146 - return lst 1147 -end 1148 1834 1149 1835 local function 1150 1836 prompt(p,multiline) 1151 1837 -- returns string if successful, nil if EOF, false if ^C 1152 1838 io.stderr:write(p) 1153 1839 local ok, res = pcall(function() 1154 1840 return io.stdin:read(multiline and 'a' or 'l') ................................................................................ 1199 1885 local c = cmds[words[1]] 1200 1886 if c then 1201 1887 if c.raw then 1202 1888 ctx.log('fatal', words[1] .. ' cannot be run from `shell`') 1203 1889 elseif not implies(c.write, rw) then 1204 1890 ctx.log('fatal', ctx.file .. ' is not writable') 1205 1891 else 1206 - local ok = ctx.try(c.exec, ctx, table.unpack(words,2)) 1207 - if ok then written = written or c.write end 1892 + local ok, outcome = ctx.try(c.exec, ctx, table.unpack(words,2)) 1893 + if ok and outcome ~= false then written = written or c.write end 1208 1894 end 1209 1895 elseif cmd == 'save' or cmd == 'wq' then 1210 1896 if not written then 1211 1897 ctx.log('info', 'no changes to save') 1212 1898 end 1213 1899 cmd = nil 1214 1900 elseif cmd == 'quit' or cmd == 'q' then ................................................................................ 1261 1947 showHelp(ctx, cmd, cmds[cmd]) 1262 1948 else 1263 1949 for cmd,c in pairs(cmds) do 1264 1950 showHelp(ctx, cmd, c) 1265 1951 end 1266 1952 end 1267 1953 end 1954 + 1955 +local globalFlags <const> = { 1956 + human = {'h','human','enable human-readable exports'}; 1957 + ident = {'i','ident','show identifier paths for all items'} 1958 +} 1268 1959 1269 1960 local function 1270 1961 usage(me,ctx) 1271 1962 local ln = 0 1272 1963 local ct = {} 1273 1964 local fe = ctx.sty[io.stderr] 1274 - io.stderr:write(string.format(fe.br"usage:".." %s <file> [<command> [args…]]\n",me)) 1965 + local fstr = "" 1966 + local flagHelp = {} 1967 + for k,v in pairs(globalFlags) do 1968 + fstr = fstr .. v[1] 1969 + table.insert(flagHelp, string.format(" -%s --%s: %s\n",table.unpack(v))) 1970 + end 1971 + io.stderr:write(string.format(fe.br"usage:".." %s [-%s] <file> [<command> [args…]]\n",me,fstr) .. table.concat(flagHelp)) 1275 1972 --[[ 1276 1973 for k,v in pairs(cmds) do 1277 1974 local n = 1 + utf8.len(k) + utf8.len(v.syntax) 1278 1975 ct[k] = n 1279 1976 if n > ln then ln = n end 1280 1977 end 1281 1978 for k,v in pairs(cmds) do ................................................................................ 1288 1985 end 1289 1986 return 64 1290 1987 end 1291 1988 1292 1989 local function 1293 1990 dispatch(argv, ctx) 1294 1991 local ferr = ctx.sty[io.stderr] 1295 - local file, cmd = table.unpack(argv) 1992 + local args = {} 1993 + local flags = {} 1994 + local i = 1 while i <= #argv do 1995 + local a = argv[i] 1996 + if a == '--' then i=i+1 break 1997 + elseif a:sub(1,2) == '--' then 1998 + local fs <const> = a:sub(3) 1999 + for k,v in pairs(globalFlags) do 2000 + if v[2] == fs then flags[k] = true end 2001 + end 2002 + elseif a:sub(1,1) == '-' then 2003 + for p,cp in utf8.codes(''), a, #'-' do 2004 + local c <const> = utf8.char(cp) 2005 + for k,v in pairs(globalFlags) do 2006 + if v[1] == c then flags[k] = true break end 2007 + end 2008 + end 2009 + else table.insert(args, a) end 2010 + i = i + 1 end 2011 + for j=i,#argv do table.insert(args,argv[j]) end 2012 + 2013 + local file, cmd = table.unpack(args) 1296 2014 if cmd and cmds[cmd] then 1297 2015 local c,fd,dict = cmds[cmd] 1298 2016 if (not c.raw) and not c.nofile then 1299 2017 fd = safeopen(file, "rb") 1300 2018 dict = readDict(fd:read 'a') 1301 2019 fd:close() 1302 2020 -- lua io has no truncate method, so we must ................................................................................ 1305 2023 end 1306 2024 1307 2025 cmds[cmd].exec({ 1308 2026 sty = ctx.sty; 1309 2027 try = ctx.try; 1310 2028 log = ctx.log; 1311 2029 2030 + flags = flags; 1312 2031 file = file; 1313 2032 fd = fd; 1314 2033 dict = dict; 1315 - }, table.unpack(argv,3)) 2034 + }, table.unpack(args,3)) 1316 2035 1317 2036 if (not c.raw) and c.write then 1318 2037 local output = writeDict(dict) 1319 2038 -- writeDict should always be given a chance to 1320 2039 -- bail before the previous file is destroyed!! 1321 2040 -- you don't want one bug to wipe out your entire 1322 2041 -- dictionary in one fell swoop ................................................................................ 1344 2063 1345 2064 local function log(lvl, msg) 1346 2065 local colors = {fatal=1,warn=3,info=4,debug=2} 1347 2066 local ferr = sty[io.stderr] 1348 2067 io.stderr:write(string.format( 1349 2068 ferr.color(ferr.br("(%s)"),colors[lvl]).." %s\n", lvl, msg)) 1350 2069 end 1351 -local function try(...) 2070 + 2071 +local function stacktrace(err) 2072 + return debug.traceback(err,3) 2073 +end 2074 + 2075 +local function try(fn,...) 1352 2076 -- a wrapper around pcall that produces a standard error 1353 2077 -- message format when an error occurs 1354 - local res = { pcall(...) } 2078 + local res = { xpcall(fn,stacktrace,...) } 1355 2079 if not res[1] then 1356 2080 log('fatal', res[2]) 1357 2081 end 1358 2082 return table.unpack(res) 1359 2083 end 1360 2084 1361 -local function stacktrace(err) 1362 - return debug.traceback(err,3) 1363 -end 1364 2085 local ok, res = xpcall(dispatch, stacktrace, argv, { 1365 2086 try = try, sty = sty, log = log 1366 2087 }) 1367 2088 1368 2089 if not ok then 1369 2090 log('fatal', res) 1370 2091 os.exit(1) 1371 2092 end 1372 2093 1373 2094 os.exit(res)