WAJLF2F6XNGO3HUMDW5D3PZ6MDUJJFF6VOLPYPUFMYNTTG3S75EAC local Numword = require 'pls-numword'print( Numword.to_s(arg[1]) )
--package.cpath = package.cpath .. ';../bin/?.so;/usr/lib/lua/5.3/nylabind/?.so'require 'site'--require 'clibs'if not NylonSysCore thenrequire 'nylon.core'()endif not Pdcurses thenrequire 'LbindPdcurses'endlocal Sqlite = require 'NylonSqlite'require 'NylonOs'-- local nylonin = require 'nylon.core'-- local Curses = require 'LbindPdcurses'--local x = Sqlite:new 'plstest.db'print( 'Pdcurses=', Pdcurses )print( 'NylonSqlite=', NylonSqlite )print( 'NylonSysCore=', NylonSysCore )print( 'NylonOs=', NylonOs )Pdcurses.Static.initscr()local db = NylonSqlite 'fts-test.db'print('db=', type(db))print('selectOne=', db.selectOne)-- os.execute('stty sane')print 'done'
require 'site'local Nylon = require 'nylon.core'()local Store = require 'pls-buffer'local function cursorpos( buffer, ptTopLeft, thePoint, wdim )local charsInto = thePoint - ptTopLeftlocal ptRow, ptCollocal drow = 0local dcol = 0local charsDrawn = 0-- local nextiseolfor l, col, eol in buffer:walkFragmentsEOL( ptTopLeft ) dolocal toDraw = eol and (eol - col) or #l-- if nextiseol then drow = drow + 1; nextiseol = false endif ((charsDrawn + toDraw) >= charsInto) thenreturn (dcol + charsInto-charsDrawn), drowelsecharsDrawn = charsDrawn + toDrawendif eol thencharsDrawn = charsDrawn + 1dcol = 0drow = drow + 1-- nextiseol = trueelsedcol = dcol + toDrawendendendlocal function mysert_eq( v, shouldbe )if v ~= shouldbe thenprint(string.format('expected value=%d got=%d',shouldbe,v))assert(false)endendlocal b = Store.withText('12345678a\n')b:append('12345678b\n')b:append('12345678c\n')-- local n,c = b:lcol4point(21)mysert_eq(b:end_point(),30)b:insert(11,'12345678x\n')mysert_eq(b:end_point(),40)-- os.exit()assert(b:char_at_point(3)=='3')assert(b:char_at_point(14)=='4')assert(b:char_at_point(25)=='5')assert(b:char_at_point(36)=='6')-- for l, col, eol in b:walkFragmentsEOL( 1 ) do-- print( 'walk', #l, col, eol, l )-- endlocal function test_buffer( t, buflen)local b = Store.withText()b.setIdealStringLength(buflen)b:insert(1,t)local re = {}local rcvd = {}for l, col, eol in b:walkFragmentsEOL(1) dotable.insert(rcvd, {col,l,eol} )table.insert(re, l:sub(col,eol))endlocal JSON = require 'JSON'if true or ( table.concat(re) ~= t ) thenfor i, v in ipairs(re) doprint(i,JSON:encode(v))print(i,JSON:encode(rcvd[i]) )endend-- b:dump()assert(table.concat(re)==t)return bendif true thenlocal t = 'hello\nthere\nbuddy\nroo\n'test_buffer( t, 99 )test_buffer( t, 10 )local t = '0123456789'test_buffer( t, 5 )test_buffer( t, 3 )test_buffer( t, 2 )test_buffer( t, 1 )end--local b =local t = '0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789'test_buffer( t, 100 )test_buffer( t, 10 )test_buffer( t, 2 )test_buffer( t, 1 )--print 'adding 4'--b:insert(5,'4')--b:dump()-- os.exit(0)--- faillocal t = '0123456789'test_buffer( t, 2 )-- print( 'walk a01', #l, col, eol, l ) end--for i = 1,40 do-- local cx,cy = cursorpos(b,1,i)-- print('cx=',cx,'cy=',cy)---- assert(n==math.floor((i-1)/10)+1)---- assert(c==((i-1)%10)+1)--end-- local b2 = Store.withText('12345678a\n12345678x\n12345678b\n12345678c\n')-- for l, col, eol in b2:walkFragmentsEOL( 3 ) do-- print( 'walk b2', l, col, eol )-- end
local Sqlite = require 'sqlite'local context_t = {url = 1,plsupdate = 2,plsnew = 3,}local db = Sqlite:new 'db-contexts.db'local function addNewClipText( clip )if string.find( clip, '^http://') or string.find( clip, '^https://') thenprint('got URL clip=', clip)wv.log('debug', 'got URL clip=%s', clip)db:exec('insert into context (context_t, dt_created, data) values(?, DATETIME("NOW"), ?)',context_t.url, clip)endendlocal function cord_clipboard_monitor( cord )require 'NylonOs'local lastClipwhile true do-- wv.log('debug', 'clipboard monitor running')local clip = NylonOs.Static.getclipboard()if clip ~= lastClip thenlastClip = clipaddNewClipText( clip )endcord:sleep(0.3)endendlocal cb_cord = Nylon.cord('clipboard-mon', cord_clipboard_monitor )local Table = require 'extable'function cleanupctxrecord( record )if record.extern_id thenrecord.extern_id = tonumber(record.extern_id)endif record.context_t thenrecord.context_t = tonumber(record.context_t)endreturn recordendlocal RPC = {}function RPC.getContext()wv.log('debug', 'got call to getContext')local many = db:selectMany( 'select * from context where dt_created > (datetime("NOW")-14) order by dt_created desc' )--local many = db:selectMany( 'select * from context order by dt_created desc where dt_created limit 100' )return Table.map( cleanupctxrecord, many )endfunction RPC.getRecentContext( N )N = N or 20wv.log('debug', 'got call to getRecentContext, N=%d', N)local many = db:selectMany( 'select * from context where dt_created > (datetime("NOW")-14) order by dt_created desc limit ?', N )--local many = db:selectMany( 'select * from context order by dt_created desc where dt_created limit 100' )return Table.map( cleanupctxrecord, many )endfunction RPC.updatePlsRecord( recordId )wv.log('debug', 'PLS edit, record id=%d', recordId)print('PLS edit, record id=', recordId)db:exec('insert into context (context_t, dt_created, extern_id) values(?, DATETIME("NOW"), ?)',context_t.plsupdate, recordId)endfunction RPC.newPlsRecord( recordId )wv.log('debug', 'NEW PLS record, record id=%d', recordId)print('NEW PLS record, record id=', recordId)db:exec('insert into context (context_t, dt_created, extern_id) values(?, DATETIME("NOW"), ?)',context_t.plsnew, recordId)endreturn RPC
--[[--Provides file archiving services for PLS system; send a list of file names, they will be copied toan archive directory and a URL path (from localhost) is returned.This is used by PLS system e.g., when copying a CF_HDROP file list from the clipboard.--]]--local config = {archiveDir = 'c:/m/notes/monthly',archiveUrl = '/notes/monthly',}local File = require 'filelib'local Table = require 'extable'local Numword = require 'numword'math.randomseed(os.time())require 'psbind'local function runshell(shell, cord, cmd)local results = {}-- local function runone(cord)wv.log('debug','Invoking powershell, cmd=%s',cmd)cord:cthreaded_multi(shell:cthread_invoke(cmd),function( rc ) table.insert(results,rc) end )wv.log('debug','Powershell invocation returned')-- end-- wv.log('debug','starting pshell request cord, cmd=%s', cmd)-- local cord = Nylon.cord('pshell-request', runone)-- Nylon.self:waiton(cord)return resultsendlocal function cordfn_bg_copy( cord )while true docollectgarbage()local msg = cord:getmsg()local shell = Psbind.Shell() -- shell is created here so that it will be closed while idle; these things suck memoryfor _, v in ipairs(msg) dolocal store = v.archpathlocal item = v.itemwv.log('debug','copying "%s" to "%s"', item, store )runshell( shell, cord, string.format( 'mkdir %s', store ) )runshell( shell, cord, string.format( 'copy-item "%s" %s/', item, store ) )endendendlocal cord_bg_copy = Nylon.cord( 'bgcopy', cordfn_bg_copy )local RPC = {}function RPC.archiveFileList(fileList)local date = os.date( '%y.%m/%d', os.time() )local rand = Numword.to_s( math.random(6400) + 1376 )local docopies = {}local results = Table.map( function(item)local base = File.leaf(item)local subdir = date .. '/' .. randlocal store = File.joinpath( config.archiveDir, subdir )table.insert( docopies, { item = item, archpath = store } )local url = table.concat( { config.archiveUrl, subdir, base }, '/' )return urlend, fileList )cord_bg_copy:msg( docopies )return resultsendreturn RPC
require 'site'package.path = package.path .. ';../nylabus/?.lua'print 'Nylabus PLS Record Service'local Nylon = require 'nylon.core'()local wv = require 'nylon.debug'{ name = 'l-svc-plsrecord' }local Sqlite = require 'sqlite'local db = Sqlite:new 'plstest.db'local RPC = { identity = function(a) return a end }local function getMetadataForId( oneId )local recid = (db:selectOne('select title, dt_modified, dt_created from note where ROWID=?', oneId))recid.id = recid.ROWIDrecid.ROWID = nilreturn recidendlocal function getFullRecordForId( oneId )local recid = (db:selectOne('select * from note where ROWID=?', oneId))recid.id = recid.ROWIDrecid.ROWID = nilreturn recidendlocal Table = require 'extable'function RPC.getRecordMetadata( id )if type(id) == 'number' then -- single idreturn getMetadataForId( id )elseif type(id) == 'table' then -- todo: to optimize, use one SQL queryif type(id[1]) == 'number' then -- found a list of idsreturn Table.map( getMetadataForId, id )endendendfunction RPC.getFullRecord( id )if type(id) == 'number' then -- single idreturn getFullRecordForId( id )elseif type(id) == 'table' thenif type(id[1]) == 'number' then -- found a list of idsreturn Table.map( getFullRecordForId, id )endendendlocal Services = require 'nylaservice-svc'Services.register( 'plsrecord', RPC )Nylon.run()
local Nylon = require 'nylon.core'()require 'NylonSqlite'wv = require 'nylon.debug' { name = 'pls-sqlite' }local Sqlite = {}function Sqlite:new(dbname)local db = NylonSqlite(dbname)if not db thenwv.log('error', 'db could not open name=%s', dbname)error 'no database'elsewv.log('debug', 'db opened with success name=%s', dbname)endreturn setmetatable({ db = db }, { __index = Sqlite })-- -- print(string.format("Sqlite:new db=%s", dbname))-- return setmetatable({-- db = NylonSqlite(dbname)-- }, { __index = Sqlite })endfunction Sqlite:selectOne(sql,...)local rcif not self.db thenwv.log('error', 'db invalid db=%s db.db = %s', tostring(self), type(self.db))-- error 'no database'end--print( 'Pdcurses=', Pdcurses )--print( 'NylonSqlite=', NylonSqlite )--print( 'NylonSysCore=', NylonSysCore )--print( 'NylonOs=', NylonOs )--local db = NylonSqlite 'fts-test.db'--print('db=', type(db))--print('selectOne=', db.selectOne)---- self.db:selectOne({}, 'select count(*) from notes', {})--wv.log('debug', 'select one sql=%s', sql);self.db:selectOne( function(r) rc = r end, sql, {...} )if not rc then--elseif rc.ROWID thenrc.ROWID = tonumber(rc.ROWID)endreturn rcendfunction Sqlite:selectMany(sql,...)local results = {}self.db:selectMany( function(row)table.insert(results,row)end, sql, {...} )return resultsendfunction Sqlite:exec(sql,...)return self.db:exec( sql, {...} )endfunction Sqlite:retryexec(sql,...)local dbrclocal args = { ... }for i = 1, 7 dolocal rc, err = pcall(function()dbrc = self.db:exec( sql, args )end)if rc thenbreakendwv.log('error','could not exec [try %d, sql=%s], e=%s', i, sql, err)Nylon.self:sleep( 0.05 * (2 ^ i) )endreturn dbrcendfunction Sqlite:lastRowId()return self.db:lastRowId()endreturn Sqlite
--package.path=package.path .. ';/usr/share/lua5.1/site/?.lua;../nylon-lua/?.lua;c:/pf32/Lua/5.1/lua/?.lua;../Debug/*.lua'--package.cpath=package.cpath .. ';/usr/share/lua5.1/clib/?.dll;c:/pf32/Lua/5.1/clibs/?.dll;../Debug/?.dll'-- package.path=package.path .. ';' .. (os.getenv 'HOME' or 'c:') .. '/.uswmini/?.lua'if true thenloadfile '../site.lua'()else -- linux-ish-- package.path = package.path .. ';../../n3/?.lua;../../nylon/?.lua;../site/?.lua'-- package.cpath = package.cpath .. ';../bin/?.so;/usr/lib/lua/5.3/nylabind/?.so'package.path = package.path .. '../site/?.lua'package.cpath = package.cpath .. ';../bin/?.so;/usr/lib/lua/5.3/nylabind/?.so'end
--[[--Demonstrates application of diffs to restore record text from zero.Maybe useful for calling from external program, ie, ruby script--]]--require 'site'local Nylon = require 'nylon.core'()--local wv = require 'nylon.debug' { name = 'pls-main' }local Sqlite = require 'sqlite'local Numword = require 'pls-numword'local JSON = require 'JSON' -- for debugginglocal db = Sqlite:new 'plstest.db'local nwid = arg[1]local recid = string.match(nwid,'^%d') and tonumber(nwid) or Numword.to_i(nwid)local Diff = require 'diff_match_patch'--local rev = tonumber(arg[2])local inittext = ''local s = Nylon.uptime()local patches = db:selectMany('select rcontent from patch where id_note=? order by revision asc', recid)for ndx, patch in ipairs(patches) dolocal gdiff = Diff.patch_fromText( patch.rcontent )inittext = Diff.patch_apply( gdiff, inittext )endlocal e = Nylon.uptime()print(inittext)print(string.format('Time: %f\n',(e-s)))
local Nylon = require 'nylon.core'()local wv = require 'nylon.debug' { name = 'pls-winman' }local function WindowDim( x, y, w, h )return { x = x, y = y, w = w, h = h }endlocal function WindowMoveResize( w, dim )w:resize( dim.h, dim.w )w:mvwin( dim.y, dim.x )endlocal win_focusedlocal function entryfn_winman( cord, env )local tiled = {}local modal = {}local landscape = falselocal fullscreened = falselocal function moveTiledRight_portrait(nexisting)local halfscreen = math.floor(env.curses.ncols/2)local h = env.curses.nrows / nexistinglocal lasttop = env.curses.nrowsfor i = 1, nexisting dolocal prev = tiled[i]local newtop = math.floor(env.curses.nrows-(i*h))local newdim = WindowDim( halfscreen, newtop, halfscreen, (lasttop-newtop) )lasttop = newtopWindowMoveResize( prev.win, newdim )wv.log('debug','moveTiledRight resized')prev.on.resized( newdim )endendlocal function moveTiledRight_landscape(nexisting)local halfscreen = math.floor(env.curses.nrows/2)local width = env.curses.ncols / nexistinglocal lasttop = env.curses.ncolsfor i = 1, nexisting dolocal prev = tiled[i]local newtop = math.floor(env.curses.ncols-(i*width))local newdim = WindowDim( newtop, halfscreen, (lasttop-newtop), halfscreen )lasttop = newtopWindowMoveResize( prev.win, newdim )wv.log('debug','moveTiledRight resized')prev.on.resized( newdim )endendlocal function moveTiledRight(nexisting)if landscape thenmoveTiledRight_landscape(nexisting)elsemoveTiledRight_portrait(nexisting)endendlocal function resizeAllTiled_portrait()local top = tiled[#tiled]local halfscreen = math.floor(env.curses.ncols/2)local newdim = WindowDim(0,0,halfscreen,env.curses.nrows)WindowMoveResize( top.win, newdim )local nexisting = #tiled -1moveTiledRight(nexisting)if nexisting > 0 thenmoveTiledRight( nexisting )elseenv.curses.screen:clear()env.curses.screen:refresh()endtop.on.resized( newdim )endlocal function resizeAllTiled_landscape()local top = tiled[#tiled]local halfscreen = math.floor(env.curses.nrows/2)local newdim = WindowDim(0,0,env.curses.ncols,halfscreen)WindowMoveResize( top.win, newdim )local nexisting = #tiled -1moveTiledRight(nexisting)if nexisting > 0 thenmoveTiledRight( nexisting )elseenv.curses.screen:clear()env.curses.screen:refresh()endtop.on.resized( newdim )endlocal function fullscreen_thisWindow( top )local newdim = WindowDim(0,0,env.curses.ncols,env.curses.nrows)if top.win thenWindowMoveResize( top.win, newdim )elsewv.log('debug','weird, window has no window top=%s', top)endif top.on and top.on.resized thentop.on.resized( newdim )elsewv.log('debug','weird, th top=%s', top)endendlocal d_minimized = WindowDim( 0, 0, 1, 1 )local function resizeAllTiled()if not tiled[1] then -- no windowsenv.curses.screen:mvaddstr(0,0,"No buffers; press C-l to open or create new record")env.curses.screen:refresh()returnendif #tiled > 1 thenif fullscreened thenfullscreen_thisWindow( fullscreened )-- for _, w in ipairs(tiled) do-- if w ~= fullscreened then-- WindowMoveResize(w.win, d_minimized)-- if w.on.resized then w.on.resized(d_minimized) end-- end-- endelseif landscape thenresizeAllTiled_landscape()elseresizeAllTiled_portrait()endelse -- only one windowfullscreen_thisWindow( tiled[1] )endfor _, w in ipairs(modal) doif w.on.resized thenw.on.resized()endendendlocal function change_focus(towin)if win_focused ~= towin thenlocal prev = win_focusedwin_focused = towinlocal _ = prev and prev.on.resized and prev.on.resized()local _ = towin and towin.on.resized and towin.on.resized()endendlocal function set_best_focus()change_focus( (#modal > 0) and modal[#modal] or tiled[#tiled] )end------------------------------------------------------------------------------------------------------------------------------------function cord.event.toggle_landscape()landscape = not landscaperesizeAllTiled()endfunction cord.event.toggle_fullscreen()if fullscreened thenfullscreened = falseelseif #tiled > 0 thenfullscreened = tiled[#tiled]endendresizeAllTiled()endfunction cord.event.focused_window_to_primary()for i, w in ipairs(tiled) doif w == win_focused thentable.remove(tiled,i)table.insert(tiled,win_focused)resizeAllTiled()returnendendendfunction cord.event.this_window_to_primary(win)for i, w in ipairs(tiled) doif w == win thentable.remove(tiled,i)table.insert(tiled,win)resizeAllTiled()returnendendendfunction cord.event.other_window()if #modal > 0 thenwv.log('debug','no window switching when modal dialog is up')elseif win_focused and #tiled > 1 thenlocal donext = falsefor n = #tiled, 1, -1 doif donext thenchange_focus( tiled[n] )returnenddonext = (win_focused == tiled[n])endchange_focus(tiled[#tiled]) -- we wrapped, last focused was probably window 1endend-- add a managed window to the tiled listfunction cord.event.push_tiled( mw )-- if #tiled > 0 then-- moveTiledRight(#tiled)-- -- mw.cord.event.refresh(true)-- endtable.insert( tiled, mw )resizeAllTiled()change_focus( mw )endfunction cord.event.remove_tiled( mw )local remain = {}for i, v in ipairs(tiled) dowv.log('debug','winman remove_tiled mw=%s v=%s',mw, v)if v ~= mw thentable.insert( remain, v )endendtiled = remainresizeAllTiled()set_best_focus()endfunction cord.event.Refresh()resizeAllTiled()endfunction cord.event.swap_tiled()wv.log('debug','cord.event.swap_tiled #tiled=%d',#tiled)if #tiled > 1 thenlocal prevtop = tiled[#tiled]tiled[#tiled] = tiled[#tiled-1]tiled[#tiled-1] = prevtopif fullscreened == prevtop thenfullscreened = tiled[#tiled]endresizeAllTiled()set_best_focus()endendfunction cord.event.push_modal( mw )table.insert( modal, mw )change_focus( mw )endfunction cord.event.remove_modal( mw )if modal[#modal] ~= mw thenwv.log('error','removed modal dialog not active, fixme')elsetable.remove(modal,#modal)resizeAllTiled() -- just to force refreshset_best_focus()endendwhile true docord:sleep(1)endendlocal Malwin = {}--[[---- "opt" fields---- :win pdcurses window---- :on table of event callbacks (see below)---- :parent receives 'bubble up' of unhandled keys---- :modal? indicates this window should grab input events first while it is active; also,this probably indicates the window should be kept free-floating and not bemanaged by the tiler.-- "opt.on" callbacks---- hidden---- shown---- resized---- destroyed---- links : window should return a list of x,y coordinates which are links. When user requestsvisual link activation, all visible windows are queried for targets and assigneda alphanumeric sequence. If more than 36 links are active, all links must betwo characters. Generally, link enumeration should increase left to right andthen top to bottom within a window. < WATCHU TALKING BOUT WILLIS ???--]]--function Malwin:new( opt )local o = setmetatable( {}, { __index = self } )if not opt.win thenwv.log('abnorm','creating winman managed window with no curses window?')endo.win = opt.wino.on = opt.on or {}return oendlocal function entryfn_key( cord, mainkeyhandler, unhandled_keys )wv.log '001 entryfn_key'cord.event.key = function(k)local mapped = mainkeyhandler(k)if mapped then -- not handled or mapped by main key handleif win_focused and win_focused.on.key thenwv.log('norm','input key/map [%s->%s] not handled by main app, passing to focused win',k,mapped)local rc = win_focused.on.key(mapped)if rc and unhandled_keys thenunhandled_keys(rc)endelsewv.log('abnorm','input key/map [%s->%s] not handled by main app, no focused win with keyhandler',k,mapped)endelsewv.log('norm','input key/map [%s] handled by main app',k)endendcord.event.never.wait()endlocal function entryfn_input( cord, cord_key )wv.log '001 entryfn_input'Pdcurses.Static.keypad(true);cord:cthreaded_multi( Pdcurses.Static.cthread_getch_loop(),function( k )cord_key.event.key( k )end )endlocal cord_keylocal module = {win = Malwin,set_input = function(mainkeyhandler,unhandled_keys)cord_key = Nylon.cord( 'key', entryfn_key, mainkeyhandler,unhandled_keys )local cord_input = Nylon.cord( 'input', entryfn_input, cord_key )-- return cord_keyend,inject_key = function(k)cord_key.event.key(k)end}local function malwin_cleanup(malwin)if not malwin.win thenwv.log('abnorm','removing win twice')elsemalwin.win:clear()malwin.win:refresh()malwin.win:forcedelete()malwin.win = nilendendfunction module.create( env )local cord_winman = Nylon.cord( 'winman', entryfn_winman, env )function module.push(malwin)cord_winman.event.push_tiled(malwin)return function()wv.log('debug','remove pushed win=%s',malwin)cord_winman.event.remove_tiled(malwin)malwin_cleanup(malwin)endendfunction module.swap_tiled()wv.log('debug','Winman.swap_tiled fn')cord_winman.event.swap_tiled()endfunction module.remove_focused()cord_winman.event.remove_focused()end-- returns a function which can be used to remove the windowfunction module.push_modal(malwin)cord_winman.event.push_modal( malwin )return function()wv.log('debug','remove modal win=%s',malwin)cord_winman.event.remove_modal(malwin)malwin_cleanup( malwin )endendfunction module.other_window()cord_winman.event.other_window()endfunction module.toggle_landscape()cord_winman.event.toggle_landscape()endfunction module.toggle_fullscreen()cord_winman.event.toggle_fullscreen()endfunction module.isFocused(w)return w == win_focusedendfunction module.Refresh()cord_winman.event.Refresh()endfunction module.focused_window_to_primary()cord_winman.event.focused_window_to_primary()endfunction module.this_window_to_primary(win)cord_winman.event.this_window_to_primary(win)endendreturn module
local wv = require 'nylon.debug'{ name = 'pls-theme' }pcall( function() require 'NylonOs' end ) -- just to test for windows / not windowslocal WINDOWS = (NylonOs and NylonOs.IsWindows())local maxpairs = Pdcurses.Static.color_pairs()wv.log('debug','WINDOWS=%s color_pairs returned=%d', WINDOWS, maxpairs)--if maxpairs < 1 then -- linux curses returns 0, no idea why but it seems-- maxpairs = 8 -- to work if I say 8 anyway. Maybe because I was--end -- loading this file before calling curses init?local assigned_pairs = {}local free_pairs = {}for i = 1,(maxpairs-1) dotable.insert(free_pairs,i)endwv.log 'welcome to pls-theme'local function get_color( fg, bg )local key = fg * 0x100000 + bgif assigned_pairs[key] thenreturn assigned_pairs[key][1]elselocal pair = free_pairs[#free_pairs]table.remove(free_pairs,#free_pairs)wv.log('debug', 'add color theme, pair=%d fg=%d bg=%d',pair, fg, bg )local color = Pdcurses.Color(pair, fg, bg)assigned_pairs[key] = { color, pair }return colorendend-- weird Windows/Pdcurses color things:---- Pdcurses.Color.yellow is actually bright white, whereas 'white' is more like grey.-- To get actual yellow, use color 'yellow' + 8-- These Pdcurses colors are really messed up on windows. Here's what they really-- seem to be:---- white => white-- black => black-- red => red-- green => green-- blue => blue-- cyan => sort of teal?-- magenta -> indigo?-- yellow => bright white (??)---- With bold attr,-- white => bright white-- black => gray-- red => bright red-- blue => bright blue-- green => bright green-- cyan => actually looks like cyan-- magenta => actually looks like magenta-- yellow => actually looks like yellow---- red, blue, green seem correct but are dull.local theme = {-- Foreground Background boldnormal = { Pdcurses.Color.white, Pdcurses.Color.black, false },inverse = { Pdcurses.Color.black, Pdcurses.Color.white, false },inversehot = { Pdcurses.Color.red, Pdcurses.Color.white, true },heading = { Pdcurses.Color.white, Pdcurses.Color.black, true },classicmenu = { Pdcurses.Color.yellow, Pdcurses.Color.cyan , true },autotext = { Pdcurses.Color.cyan, Pdcurses.Color.black, false },focuswinborder = { Pdcurses.Color.white, Pdcurses.Color.black, true },}if not WINDOWS then-- theme.classicmenu = { Pdcurses.Color.black, Pdcurses.Color.cyan }theme.autotext = { Pdcurses.Color.cyan, Pdcurses.Color.black }endlocal with = setmetatable( {}, {__index = function(t,v)local fgbg = theme[v]if fgbg thenlocal color = get_color( fgbg[1], fgbg[2] )return function(win,withfun)if win thenwin:attron( color )if fgbg[3] thenwin:attron( Pdcurses.Color.a_bold )endendwithfun()if win thenif fgbg[3] thenwin:attroff( Pdcurses.Color.a_bold )endwin:attroff( color )endend -- end, 'with' functionendend })return setmetatable( {color = get_color,with = with,}, {__index = function(t,v)--wv.log('debug', 'request theme color=%s', v)local fgbg = theme[v]--wv.log('debug', 'request theme color=%s fgbg=%s', v, fgbg)if fgbg thenreturn get_color( fgbg[1], fgbg[2] )endend } )
local kIdealStringLength=768local wv = require 'nylon.debug' { name = 'pls-bbuf' }local Bbuf = {}function Bbuf.setIdealStringLength( n )kIdealStringLength = nendfunction Bbuf:_invalidateOnEdit()self.cachept = 9.0E99endlocal function Node(text)return { height = 0, v = text, len=#text }endlocal function tree_delta( self )return (self.l and self.l.height or 0) - (self.r and self.r.height or 0)endlocal tree_balancelocal function tree_rotl(self)local r = self.rself.r = r.lr.l = tree_balance(self)return tree_balance(r)endlocal function tree_rotr( self )local l = self.lself.l = l.rl.r = tree_balance(self)return tree_balance(l)endlocal function node_setlen( node )node.len = (node.l and node.l.len or 0) --node.len = node.len + #node.vnode.len = node.len + (node.r and node.r.len or 0)endtree_balance = function(self)local delta = tree_delta(self)if delta < -1 thenself.r = tree_delta(self.r) > 0 and tree_rotr(self.r) or self.rreturn tree_rotl(self)elseif delta > 1 thenself.l = tree_delta(self.l) < 0 and tree_rotl(self.l) or self.lreturn tree_rotr(self)endself.height = 0if self.l and self.l.height > self.height thenself.height = self.l.heightendif self.r and self.r.height > self.height thenself.height = self.r.heightendself.height = self.height + 1node_setlen(self)return selfendlocal function tree_insert( self, pos, elm )-- wv.log('debug','tree insert, e=%s', elm.v)if not self thenreturn elmendif self.l thenif pos <= self.l.len thenself.l = tree_insert(self.l, pos, elm)node_setlen(self)return tree_balance(self)elsepos = pos - self.l.lenendelseif pos <= 1 thenself.l = tree_insert( self.l, pos, elm )node_setlen(self)return tree_balance( self )endif pos <= (#self.v+1) thenlocal modified = self.v:sub( 1, pos-1 ) .. elm.v .. self.v:sub(pos)-- no nodes change, but in full implementation, we should (possibly) split-- the strings here to make a new node, which I guess would be-- inserted to the right??if #modified < kIdealStringLength thenself.v = modifiednode_setlen(self)return selfelselocal lideal = math.floor(kIdealStringLength * 5.0 / 6.0)self.v = modified:sub(1,lideal)-- wv.log('debug','avl text tree split string=%d -> %d',#modified, lideal)self.r = tree_insert( self.r, 0, Node(modified:sub(lideal+1)) )node_setlen( self )return tree_balance(self)endendpos = pos - #self.vself.r = tree_insert( self.r, pos, elm )node_setlen(self)return tree_balance(self)endlocal function tree_delete( self, posbeg, len )if not self thenreturnendwv.log('debug','tree_delete, self=%s posbeg=%d len=%d #l=%d #v=%d',self, posbeg, len, self.l and self.l.len or -1, #self.v)if self.l thenif posbeg <= self.l.len thenlocal remain = tree_delete(self.l, posbeg, len)if remain thenposbeg = 1len = remainwv.log('debug','back to clip more, remain=%d #v=%d',remain,#self.v)elsenode_setlen(self)return -- tree_balance(self)endelseposbeg = posbeg - self.l.lenendendif posbeg <= #self.v thenlocal remain = #self.v - posbeg - len + 1if remain > 0 thenlocal modified = self.v:sub( 1, posbeg-1 ) .. self.v:sub(posbeg+len)self.v = modifiednode_setlen(self)returnelselocal modified = self.v:sub( 1, posbeg-1 )local removed = #self.v - posbeg + 1self.v = modifiedlen = len - removedposbeg = 1-- tree_delete( self.r, 1, len-removed )-- local lideal = math.floor(#modified / 2)-- self.v = modified:sub(1,lideal)-- -- wv.log('debug','avl text tree split string=%d -> %d',#modified, lideal)-- self.r = tree_insert( self.r, 0, Node(modified:sub(lideal+1)) )-- node_setlen( self )-- return -- tree_balance(self)endelseposbeg = posbeg - #self.vendif self.r thenlocal remain = tree_delete( self.r, posbeg, len )node_setlen(self)return remain -- tree_balance(self)elsewv.log('debug','need to clip more on return, posbeg=%d len=%d', posbeg, len)node_setlen(self)return lenendendfunction treewalk_to( self, point )if not self then -- out of bounds or somethingwv.log('abnorm','avl text search out of bounds, point=%d',point)returnendif self.l thenif point <= self.l.len thenreturn treewalk_to( self.l, point )elsepoint = point - self.l.lenendendif point <= #self.v then-- wv.log('debug','treewalk_to point=%d #l=%d',point,#self.v)return self.v, pointendreturn treewalk_to( self.r, point - #self.v )endfunction tree_traverse( self, point, cbfun )-- wv.log('debug','tree_traverse point=%d cbfun=%s', point, cbfun )if not self thenreturnendif self.l thenif point <= self.l.len thentree_traverse( self.l, point, cbfun )endpoint = point - self.l.lenendif point <= #self.v thencbfun( self.v, (point < 1 and 1 or point) )endpoint = point - #self.vreturn tree_traverse( self.r, point, cbfun )end--function Bbuf:lcol4point(point)-- local ndx = 1-- if not point or point < 1 then-- wv.log('abnorm','invalid point=%s',point)-- error('invalid point')-- end-- if point >= self.cachept and (point < (self.cachept+self.cachelen)) then-- return self.cachel, (point-self.cachept+1)-- end-- local startPoint = point-- while ndx <= #self.lines do-- local llen = #(self.lines[ndx])-- -- wv.log('debug','ndx=%d llen=%d point=%d line=%s',ndx,llen,point,self.lines[ndx])-- if llen >= point then-- self.cachept = startPoint - point + 1-- self.cachel = ndx-- self.cachelen = #self.lines[1]-- return ndx, point-- else-- point = point - llen-- ndx = ndx + 1-- end-- end-- wv.log('error','point=%d, max=%d #lines=%d',point,self.max,#self.lines)--end--function Bbuf:end_point()return self.maxendfunction Bbuf:char_at_point_dec( point )local l, col = treewalk_to( self.textavlroot, point )if not l thenwv.log('error','point maybe out of range? point=%d max=%d',point or -99,self:end_point())return string.byte(' ',1)elsereturn string.byte(l,col)endendfunction Bbuf:char_at_point( point )return string.char( self:char_at_point_dec(point) )endfunction Bbuf:append( text )-- this could probably be optimized with an optimized AVL-- "insert all the way to the right" operation that avoids-- all the un-needed comparisons...-- but for now, it is okayself:_insert_int( self.max + 1, text )endfunction Bbuf:walkFragments( point )local co = coroutine.create(function()tree_traverse( self.textavlroot, point,function(t,point)coroutine.yield(t,point)end )end)return function()--wv.log('debug','walkFragments resuming point=%d',point)local ok, t, point = coroutine.resume(co)-- wv.log('debug','walkFragments resumed=%s / %s / %s',ok, tostring(point), tostring(t))if ok thenreturn t, pointend-- local t = {}-- while point < self:end_point() do-- table.insert(t,self:char_at_point(point))-- point = point + 1-- if t[#t] == '\n' then-- break-- end-- end-- if #t > 0 then-- local str = table.concat(t)-- wv.log('debug','walkfrag, t=%d str=%s', #t,str)-- return str, 1, #t-- else-- wv.log('debug','walkfrag END, t=%d', #t)-- endendendfunction Bbuf:walkFragmentsEOL( point )local fn = self:walkFragments(point)local lastt,lasteolreturn function()local t, pointif lasteol and (lasteol+1) <= #lastt thent = lasttpoint = lasteol + 1lasteol = nilelset, point = fn()endif t thenlocal eol = string.find(t,'\n',point)-- wv.log('debug','walkFragmentsEOL, [b,e] = [%d,%d] t[]=%s',-- point, (eol or -1), t:sub(point,eol) )if eol thenlastt = tlasteol = eolreturn t, point, eolelsereturn t, pointendendend-- local ndx, col = self:lcol4point(point)-- return function()-- local rcl ,rccol = self.lines[ndx], col-- if not rcl then-- return-- end-- local nl = string.find( rcl, '\n', rccol )-- if nl then-- col = nl+1-- if col > #rcl then-- col = 1-- ndx = ndx + 1-- end-- return rcl,rccol,nl-- else -- no EOL, go to next fragment-- col = 1-- ndx = ndx + 1-- if self.lines[ndx] then-- return rcl,rccol-- else-- return rcl,rccol,(#rcl+1)-- end-- end-- endendfunction Bbuf:walkFragmentsEOL_orWidth( point, width )local fn = self:walkFragmentsEOL(point)local t, col, eollocal remain = widthreturn function()if not t thent,col,eol = fn()if not t thenreturnendendlocal w = (eol and eol or #t) - col + 1if w > remain thenlocal oldcol = collocal nextcol = col+remainif nextcol > #t thent = nilelsecol = nextcolendreturn t, oldcol, nextcolelselocal savet = tt = nilif eol thenremain = widthendreturn savet, col, eolendendendfunction Bbuf:searchPointsCharBackward(point)local ndx, col = self:lcol4point(point)return function()local rcl ,rccol = self.lines[ndx], colendendfunction tree_dump(self,path)path = path or '[root'if self.l thentree_dump( self.l, path .. ', left' )endprint( string.format('node %s %s] #=%d l=%s r=%s v="%s"',self, path, self.len, self.l, self.r, self.v ))if self.r thentree_dump( self.r, path .. ', right' )endendfunction Bbuf:dump()tree_dump( self.textavlroot )endfunction Bbuf:new()return setmetatable({-- lines = {},-- textavlroot = Node(''),max = 0,cachept = 9.0E99,operations = {},unmodified_operation_index = 0,}, { __index = self })endfunction Bbuf:sub( l, r )local substrleft = (r-l)local frags = {}for line, col in self:walkFragments(l) dolocal llen = #line-col+1if llen > substrleft thentable.insert(frags,line:sub(col,col+substrleft))breakelsetable.insert(frags,line:sub(col))substrleft = substrleft - llenendendreturn table.concat(frags)end-- returns some point near where the undone operation occurred or somethingfunction Bbuf:undo()if #self.operations < 1 thenreturnendlocal last = self.operations[#self.operations]table.remove( self.operations, #self.operations )if last.removed thenself:_insert_int( last.l, last.text )return last.lelseif last.inserted thenself:_remove_int( last.point, last.point + last.len - 1 )return last.pointendendfunction Bbuf:isModified()return self.unmodified_operation_index ~= #self.operationsendfunction Bbuf:setUnmodified()self.unmodified_operation_index = #self.operationsendfunction Bbuf:_addoperation( o )local last = self.operations[#self.operations]if last and o.inserted and last.inserted and(o.point == last.point + last.len) thenlast.len = last.len + o.lenelsetable.insert( self.operations, o )endendfunction Bbuf:remove( l, r )l = (l < 1) and 1 or lr = r or lr = (r > self.max) and self.max or rif r < l then -- possibly if self.max = 0returnendif self.textavlroot thenself:_addoperation{ removed = true, text = self:sub(l,r), l = l, r = r }self:_remove_int( l, r )end-- return self:_remove_int( l, r )endfunction Bbuf:_remove_int( l, r )local len = (r-l)+1tree_delete( self.textavlroot, l, len )self.max = self.max - lenendfunction Bbuf:_old_remove_int( l, r )local lndx, lcol = self:lcol4point(l)local rndx, rcol = self:lcol4point(r)-- wv.log( 'debug', 'remove [%d-%d] :: end=%d :: [%d,%d]->[%d,%d]',-- l, r, self:end_point(),-- lndx or -99,lcol or -99, rndx or -99, rcol or -99)if lndx == rndx thenif lndx thenlocal line = self.lines[lndx]self.lines[lndx] = line:sub(1,lcol-1) .. line:sub(rcol+1)self.max = self.max - (r-l+1)endelseif r > self:end_point() and lndx == #self.lines thenlocal line = self.lines[lndx]self.max = self.max - (#line-lcol+1)self.lines[lndx] = line:sub(1,lcol-1)elseerror( string.format('multi-line remove not implemented [%d,%d]->[%d,%d]',lndx or -99,lcol or -99, rndx or -99, rcol or -99))endendself:_invalidateOnEdit()endfunction Bbuf:insert( point, text )if point > self:end_point() + 1 and self:char_at_point_dec(self:end_point()) ~= 10 thenself:insert( self:end_point()+1, '\n' )endif #text > 0 thenself:_addoperation{ inserted = true, point = point, len = #text }endreturn self:_insert_int( point, text )endfunction Bbuf:_insert_int( point, text )-- if point > self:end_point() then-- return self:append(text)-- elselocal full = #text-- wv.log('debug','insert point=%d max=%d #text=%d', point or -99, self.max, #text)local off = 0while #text > kIdealStringLength doself.textavlroot = tree_insert( self.textavlroot, point+off, Node(text:sub(1,kIdealStringLength)) )off = off + kIdealStringLengthtext = text:sub(kIdealStringLength+1)endif #text > 0 thenself.textavlroot = tree_insert( self.textavlroot, point+off, Node(text:sub(1)) )endself.max = self.max + fullself:_invalidateOnEdit()-- endendfunction Bbuf:replace( ptl, ptr, text )-- making this a primitive because it is a good candidate for optimization,-- but for now it is just implemented as 'remove' + insertself:remove(ptl,ptr)self:insert(ptl,text)end-- local subbed = l:gsub('\t',' '):gsub('\n','')return Bbuf
local function stringsplit(str)if #str > 1 thenreturn str:sub(1,1), stringsplit(str:sub(2))elsereturn strendend-- this is shameful how long it took to write.-- I think a better general thing would be to write it as-- 1377 => { 1, 1, 1, 1 }-- 1378 => { 1, 1, 1, 2 } etc. where the table is {base #cstr, base #vstr, base #cstr}local cstr = 'bdfghjklmnprstvw'local vstr = 'aeiou'local cons = { stringsplit(cstr) }local vowel = { stringsplit(vstr) }-- four places is 1377 (abab) "lett"-- five places is 7777 (babab) "7777" hahahalocal function number2string( num, which )-- print( 'number2string', num, which == vowel and 'vowel' or 'cons' )if not which thenwhich = consnum = tonumber(num) -- protect against receiving number as string, e.g, "8192" rather than 8192elseif num <= 0 then return '' endendnum = num - 1local mod = num % #whichreturn ( number2string( math.floor(num/#which),which == cons and vowel or cons ) .. which[mod+1] )endlocal function string2number( str, prior, which )-- print('string2number', str, prior )if not str then return endif not string.find(cstr,str:sub(#str)) then return end -- last letter must be consonantprior = prior or 0local check = which or vstrlocal other = (check == vstr) and cstr or vstrlocal first = str:sub(1,1)local f = string.find(check,first)if f thenlocal acc = (prior*#check) + freturn (#str <= 1) and acc or string2number(str:sub(2), acc, other)elseif not which thenf = string.find(other,first)if not f thenreturn -- invalid stringendlocal acc = prior*#other + freturn (#str <= 1) and acc or string2number(str:sub(2),acc,check)endendlocal function isvalid( str )local len = #strif len < 1 thenreturn falseend-- last letter must be consonant.local which = cstrfor i = len, 1, -1 doif not string.find(which,str:sub(i,i)) thenreturn falseend-- must alternate consonant->vowel->consonant->vowel etc.which = (which == cstr) and vstr or cstrendreturn trueend--if string.find(arg[1],'%d') then-- local n = number2string(arg[1])-- print(n,string2number(n))--else-- local n = string2number(arg[1])-- print( n, number2string(n))--endreturn {to_s = number2string,to_i = string2number,isvalid = isvalid}
require 'clibs' -- for nylonolocal Keys = {}local WINDOWS = (NylonOs and NylonOs.IsWindows())--[[-- Free Ctrl keys:C-c (emacs this is a user prefix or something)C-l (i associate with window mgmt)C-o (emacs = "open line"? I've never used that)C-q (usally means insert literal)C-u (emacs = numbered prefix)C-w (emacs = kill-region)--]]----[[--useful keys[27] C-[ (esc)[28] C-\[29] C-][30] C-^[31] C-_ (underscore)[417] M-a (418=M-b, 419=M-c, 442=M-z, etc)[423] M-g for M-g n and M-g p to move through search results[428] M-l might be better for the app menu than C-lFor some reason, in windows, right alt key doesn't work,only left key. so I am inclined against Alt+# key combos now.--]]--Keys.control = {-- menu = 12, -- C-l -- conflicts with Ion WM-- menu = 17, -- C-q-- menu = 20, -- C-t -- conlicts with buffer mgmt, C-t k, C-t o (Keys.command_prefix)menu = 429, -- M-meditTitle = 436, -- M-t}Keys.edit = {[18] = 'isearch_backward', -- C-r[19] = 'isearch_forward', -- C-s[1] = 'move_beginning_of_line', -- C-a[5] = 'move_end_of_line', -- C-e[6] = 'forward_char', -- C-f[2] = 'backward_char', -- C-b[Pdcurses.key.right] = 'forward_char', -- C-f[Pdcurses.key.left] = 'backward_char', -- C-f[7] = 'ctrlg',[422] = 'forward_word', -- M-f[418] = 'backward_word', -- M-b[437] = 'upcase_word', -- M-u[428] = 'downcase_word', -- M-d[419] = 'capitalize_word', -- M-c[22] = 'scroll_up_command', -- C-v[Pdcurses.key.npage] = 'scroll_up_command', -- pgdn[438] = 'scroll_down_command', -- M-v[Pdcurses.key.ppage] = 'scroll_down_command', -- pgup[Pdcurses.key.dc] = 'delete_char', -- [Delete][4] = 'delete_char', -- C-d[8] = 'delete_backward_char', -- bksp-- 161217 not exactly sure why this is; on linux I get 263 for backspace key.[263] = 'delete_backward_char', -- C-z[420] = 'delete_word', -- M-d[11] = 'kill_line', -- C-k[441] = 'yank_pop', -- M-y[26] = 'undo', -- C-z[445] = 'scroll_right', -- C-pgup[446] = 'scroll_left', -- C-pgdb[9] = 'indent_for_tab_command', -- bksp[10] = 'newline', -- Enter[25] = 'yank', -- C-y[27] = 'escape'}Keys.everybody = {[Pdcurses.key.down] = 'next_line',[Pdcurses.key.up] = 'previous_line',[22] = 'scroll_up_command', -- C-v[Pdcurses.key.npage] = 'scroll_up_command', -- pgdn[438] = 'scroll_down_command', -- M-v[Pdcurses.key.ppage] = 'scroll_down_command', -- pgup[14] = 'next_line', -- C-n[15] = 'context_menu', -- C-o[16] = 'previous_line', -- C-p[23] = 'kill_region', -- C-w[431] = 'toggle_landscape', -- M-o-- [431] = 'toggle_fullscreen', -- M-o[439] = 'kill_ring_save', -- M-w}if WINDOWS thenlocal keymap_everybody_windows = {[Pdcurses.key.ctl_tab] = 'other_window',[Pdcurses.key.ctl_enter] = 'focused_window_to_primary', --[Pdcurses.key.ctl_home] = 'beginning_of_buffer', -- C-Home[Pdcurses.key.ctl_end] = 'end_of_buffer', -- C-end[Pdcurses.key.sdown] = 'xyz', -- Shift-down[Pdcurses.key.sup] = 'xyz', -- Shift-up[13] = 10,}for i,v in pairs(keymap_everybody_windows) doKeys.everybody[i] = vendend-- C-x prefix (or C-t, if you're me)Keys.command_prefix = {[3] = 'save_buffers_kill_terminal', -- C-x C-c[19] = 'save_buffer', -- C-x C-s[string.byte('k',1)] = 'kill_buffer', -- C-x k[string.byte('u',1)] = 'undo', -- C-x u[string.byte('o',1)] = 'other_window', -- C-x o[string.byte('i',1)] = 'insert_file', -- C-x o[string.byte('m',1)] = 'mail_record', -- C-x k[string.byte(' ',1)] = 'set_mark_command', -- C-x k}-- C-c prefixKeys.personal_prefix = {[string.byte('.',1)] = 'jumptotag', -- C-c .[3] = 'query_citations', -- C-c C-c[string.byte('>',1)] = 'extract_to_new_record',[string.byte('n',1)] = 'insert_ref_to_new_record',[string.byte('y',1)] = 'special_yank',}-- abandoned-- [string.byte('s',1)] = 'start_snipping_tool',Keys.esc_prefix = { -- Duplicate Pdcurses' M-[a-z] mappings[string.byte('a',1)] = 417,[string.byte('b',1)] = 418,[string.byte('c',1)] = 419,[string.byte('d',1)] = 420,[string.byte('e',1)] = 421,[string.byte('f',1)] = 422,[string.byte('g',1)] = 423,[string.byte('l',1)] = 428,[string.byte('o',1)] = 431,[string.byte('s',1)] = 435,[string.byte('t',1)] = 436,[string.byte('u',1)] = 437,[string.byte('w',1)] = 439,[string.byte('y',1)] = 441,}return Keys
-------------------------------------------------------------------- @todo:-- kill ring gc?local wv = require 'nylon.debug' { name = 'pls-ed' }local ok,e = pcall(function()require 'NylonOs'end)if not ok thenwv.log('abnorm','NylonOs not found')endlocal Nylon = require 'nylon.core'()local edopts = {replaceMark = true}local function wait_first_event( cord, handlers )local rclocal done = false-- setup event handlersfor nEvent, fHandler in pairs(handlers) do-- wv.log('debug','set event handler for nEvent=%s',nEvent)cord.event[nEvent] = function(...)rc = fHandler(...)done = trueendendwhile not done docord:yield_to_sleep()end-- remove event handlersfor k, v in pairs(handlers) docord.event[k] = nilendreturn rcendlocal killring = {}local killring_yankndx = 0local function killring_add( t )table.insert( killring, t )killring_yankndx = #killringif NylonOs thenNylonOs.Static.setclipboard(killring[#killring])endendlocal function killring_append(t)killring[#killring] = killring[#killring] .. tif NylonOs thenNylonOs.Static.setclipboard(killring[#killring])endendlocal function killring_getyanktext()if NylonOs thenreturn NylonOs.Static.getclipboard()elsereturn killring[killring_yankndx]endendlocal function watch_cursor_pos( ptTopLeft, thePoint, wdim )local charsInto = thePoint - ptTopLeftlocal ptRow, ptCollocal drow = 0local dcol = 0local charsDrawn = 0return function( l, col, eol )local toDraw = eol and (eol - col + 1) or (#l-col+1) -- was #lif (charsDrawn + toDraw) > charsInto thenreturn (dcol + charsInto-charsDrawn), drowelsecharsDrawn = charsDrawn + toDrawendif eol thendcol = 0drow = drow + 1elsedcol = dcol + toDrawendendendlocal Theme = require 'pls-theme'--# [[View]]--# A view is completely described by:--# The ncurses window (and dimensions), buffer, topleft, and point--# incremental search, e.g., is unique to a view.-- (and maybe some internal stuff? like "last_col", though that has to do-- only with _movement/key interaction_ and not display; same might be true-- of e.g., things like 'last i-search start point" or something).local function entryfn_edit( cord, env, buffer, opt )local wdim = opt.wdimlocal w = env.wenv.on = env.on or {} -- ensure table exists, to make test easierenv.report = env.report or function() endopt.plug = opt.plug or {}local wordWrap = falselocal walkFun = wordWrap and buffer.walkFragmentsEOL_orWidth or buffer.walkFragmentsEOL-- add refresh every 2sif true thenlocal refresh_cord = Nylon.cord('refreshedit', function(refcord)while true dorefcord:sleep(10)cord.event.refresh()endend )endlocal function cursorpos( ptTopLeft, thePoint, wdim )local f = watch_cursor_pos(ptTopLeft,thePoint,wdim)for l, col, eol in walkFun(buffer, ptTopLeft, wdim.w) dolocal cx, cy = f( l, col, eol )if cx thenreturn cx, cyendendendlocal ptTopLeft = 1local thePoint = 1local theMarklocal isearch_fstring = nillocal wasding -- true when last event was a "ding", should prevent status-- update from overriding ding messagelocal function get_marked()local markbeg = theMark and (thePoint < theMark) and thePoint or theMarklocal markend = theMark and (theMark < thePoint) and thePoint or theMarkreturn markbeg, markendendlocal function get_line_marked( drawPoint, drawPointAfterLine )local markbeg, markend = get_marked()local lineMarkBeg, lineMarkEndif markbeg and drawPointAfterLine > markbeg and drawPoint < markend thenlineMarkBeg = drawPoint >= markbeg and 1 or (markbeg-drawPoint+1)lineMarkEnd = drawPointAfterLine <= markend and (drawPointAfterLine-drawPoint) or (markend - drawPoint)endreturn lineMarkBeg, lineMarkEndendlocal function pdcur_draw_window( wdim )local charsInto = thePoint - ptTopLeftlocal c = opt and opt.theme and opt.theme.text and Theme[opt.theme.text] or Theme.normalw:attron( c )local function draw_line( drow, l, markBeg, markEnd )w:move(drow+wdim.y,wdim.x)local s, eif isearch_fstring and #isearch_fstring > 0 thenif isearch_fstring:find '[A-Z]' thens,e = l:find(isearch_fstring)elses,e = l:lower():find(isearch_fstring)endendlocal ndrawn = #lif s thenlocal Theme = require 'pls-theme'w:addstr( l:sub(1,s-1) )w:attroff( c )Theme.with.classicmenu( w, function()w:addstr( l:sub(s,e) )end )w:attron( c )w:addstr( l:sub(e+1,wdim.w) )elseif opt.plug.drawline thenndrawn = opt.plug.drawline( w, wdim.x, drow+wdim.y, l, markBeg, markEnd ) or ndrawnelseif not markBeg thenw:addstr((#l > wdim.w) and l:sub(1,wdim.w) or l)elselocal endPoint = markEnd > wdim.w and wdim.w or markEndw:addstr( l:sub(1,markBeg-1) )local function drawMarked() w:addstr( l:sub(markBeg,endPoint) ) endif opt and opt.theme and opt.theme.text == 'inverse' then-- 160518 bah, this is not working as i expectw:attroff(c)Theme.with.classicmenu( w, drawMarked )w:attron(c)else-- w:attroff(c)Theme.with.inverse( w, drawMarked )-- w:attron(c)endw:addstr( l:sub(endPoint+1,wdim.w) )endendendif ndrawn < wdim.w then-- wv.log('debug','eol drawn=%d w=%d', drawn, wdim.w)--w:mvaddstr( drow,drawn,string.rep(string.char(test1), (wdim.w-drawn)) )w:mvaddstr( drow+wdim.y,wdim.x+ndrawn,string.rep(' ', (wdim.w-ndrawn)) )endendlocal drow = 0local linefrags = {}local drawPoint = ptTopLeftfor l, col, eol in walkFun( buffer, ptTopLeft, wdim.w ) do-- wv.log('debug','pdcur_draw_window walk win=%dx%d@%d,%d #l=%d d=%d,%d text col=%d eol=%d', wdim.w, wdim.h, wdim.x, wdim.y, #l, drow, dcol, col or -99, eol or -99)if eol thentable.insert( linefrags, l:sub(col,eol-1) )local wholeLine = table.concat(linefrags)local drawPointAfterLine = drawPoint + #wholeLine + 1local lineMarkBeg, lineMarkEnd = get_line_marked( drawPoint, drawPointAfterLine )draw_line( drow, wholeLine, lineMarkBeg, lineMarkEnd )drawPoint = drawPointAfterLinedrow = drow + 1linefrags = {}elseif col > 1 thentable.insert( linefrags, l:sub(col) )elsetable.insert( linefrags, l )endif drow >= wdim.h thenbreakendendif #linefrags > 0 thendraw_line( drow, table.concat(linefrags) )drow = drow + 1endwhile drow < wdim.h dow:mvaddstr(drow+wdim.x,wdim.y,string.rep(' ',wdim.w))drow = drow + 1endlocal cx,cy=cursorpos(ptTopLeft,thePoint,wdim)-- if cx > wdim.w then -- hack!-- local exceeded = (cx-wdim.w)+5-- local str = w:mvgetstr( cy, exceeded )-- wv.log('debug','exceeded screen cx=%d wdim.w=%d str=%s', cx, wdim.w, str)-- w:mvaddstr( cy, 0, str:sub(1,wdim.w) )-- endw:attroff(c)if isearch_fstring thenenv.report('isearch','i-search: ' .. isearch_fstring)else--local ndx,col = buffer:lcol4point(thePoint)if not wasding thenenv.report( 'navstatus', string.format('sz=%d pt=%d top=%d x,y=%02d,%02d',buffer:end_point(), thePoint, ptTopLeft, cx or -1, cy or -1))endendendlocal keybindings = (require 'pls-keys').editlocal function ding(msg,p,...)Pdcurses.Static.beep()if msg thenwasding = trueenv.report('oops',p and string.format(msg,p,...) or msg)endendlocal function report(...)env.report(...)wasding = trueendlocal function next_isearch_forward( startPoint )local nextpoint = buffer:search_forward_to( startPoint, isearch_fstring )wv.log('debug','isearch got nextpoint=%s pt=%d', nextpoint, thePoint)if nextpoint thenthePoint = nextpointlocal cx, cy = cursorpos(ptTopLeft,thePoint,wdim)if cy >= wdim.h then -- ran off screenlocal adjust = math.floor(cy - (wdim.h / 3)) -- put next point at top 1/3 of screenwv.log('debug','isearch moved beyond screen! wdim.h=%d cy=%d adjust=%d', wdim.h, cy, adjust)local st = buffer:forward_lines(ptTopLeft, adjust)if st thenptTopLeft = stendendelseding 'i-search failed'endcord.event.refresh()end-- Warning!! This function does not communicate back with input handling framework.-- for the actual key handler look at the setup in [main.lua::make_editor()]cord.event.key = function(k)wv.log('debug','edit got key=%s => %s', k, keybindings[k] )if keybindings[k] thencord.event[ keybindings[k] ]()elseif type(k) == 'number' thenif k < 256 and k >= 32 then -- don't insert meta chars and ctrl charsif isearch_fstring thenisearch_fstring = isearch_fstring .. string.char(k)next_isearch_forward( thePoint )cord.event.refresh()elsecord.event.text(k)endelseif k == 27 then -- don't insert meta chars and ctrl charsif isearch_fstring thenisearch_fstring = nilcord.event.refresh()elsereturn kendelsereturn kendendendlocal baseline = 1local ccol = 1local crow = 1local function cursor_line()return buffer[crow+baseline-1] or ''endlocal function char_at_point()return buffer:char_at_point(thePoint)endlocal function insert_at_point(t)-- wv.log('debug','insert at pt=%d str=[%s]',thePoint, t)buffer:insert( thePoint, t )thePoint = thePoint + #tif env.on.modified thenenv.on.modified()end-- local l = cursor_line()-- if ccol > #l+1 then-- ccol = #l+1-- end-- buffer[crow+baseline-1] = l:sub(1,ccol-1) .. t .. l:sub(ccol)-- ccol = ccol + #tendfunction cord.event.getPoint(ret)ret(thePoint)endfunction cord.event.replace_marked(cbfun)if cbfun thenlocal mb, me = get_marked()if mb and me > mb thenlocal rc = cbfun( buffer:sub(mb,me-1) )if rc ~= false then -- false return code means abort operationbuffer:remove(mb,me-1)buffer:insert(mb,rc or '')theMark = nilcord.event.refresh()endelsecbfun() -- callback with no params to indicate no marked regionendendendfunction cord.event.kill_buffer()if env.on.kill_buffer thenenv.on.kill_buffer()elseding 'kill-buffer not implemented'endendfunction cord.event.undo()local undopoint = buffer:undo()if undopoint thenthePoint = undopointif env.on.modified thenenv.on.modified()endcord.event.refresh()elseding 'No further undo information'endendfunction cord.event.isearch_forward()-- ding 'isearch_forward not implemented'-- emacs search forward uses 'ctrl-g' to clear broken chars back to-- last match, and esc exits the search altogetherif isearch_fstring then -- search for next matchnext_isearch_forward( thePoint + 1 )else -- start new searchisearch_fstring = ''endcord.event.refresh()endfunction cord.event.ctrlg()isearch_fstring = niltheMark = nilcord.event.refresh()endfunction cord.event.isearch_backward()ding 'isearch_backward not implemented'endfunction cord.event.jumptotag()local w = buffer:word_at_point(thePoint > 1 and (thePoint-1) or thePoint)local w2 = buffer:word_at_point( buffer:end_of_line(thePoint)-1 )local _ = env.on.tagActivated and env.on.tagActivated( w, w2 )endfunction cord.event.query_citations()local _ = env.on.queryCitations and env.on.queryCitations()endfunction cord.event.save_buffer()if env.on.save thenreport('major','Saving buffer')local one = buffer:asOneString()if true then -- create temp record to save my sanity maybelocal f = io.open(string.format('/tmp/pls-r-%f.txt',Nylon.uptime()),"w")f:write(one)f:close()endenv.on.save( one )report('major','Buffer saved')elsereport('warn','No buffer save handler')endendfunction cord.event.insert_at_point( t )insert_at_point( t )cord.event.refresh()endlocal function do_yank( special )if env.on.yank and env.on.yank( special ) thenreturnendlocal yanktext = killring_getyanktext()insert_at_point( yanktext )cord.event.refresh()endfunction cord.event.yank()do_yank()endfunction cord.event.special_yank() -- typically allows paste of htmldo_yank(true)endfunction cord.event.text(t)if t < 256 thenlocal theString = string.char(t)if theMark and edopts.replaceMark thenlocal markbeg = get_marked()cord.event.replace_marked( function()thePoint = markbeg + 1return theStringend)elseinsert_at_point(theString)endsave_col = trueelseding("unrecognized key=%d",t)endcord.event.refresh()endfunction cord.event.indent_for_tab_command()insert_at_point(' ')cord.event.refresh()endfunction cord.event.delete_char()buffer:remove(thePoint)cord.event.refresh()endlocal lastkilllocal function killring_addorappend(t)wv.log('debug','killring_addorappend lastkill=%s',lastkill)if lastkill thenkillring_append(t)elsekillring_add(t)endendfunction cord.event.kill_line()if buffer:char_at_point(thePoint) == '\n' thenbuffer:remove(thePoint)killring_addorappend '\n'elselocal eolPoint = buffer:end_of_line(thePoint)if buffer:char_at_point(eolPoint) == '\n' theneolPoint = eolPoint - 1endif eolPoint ~= thePoint thenlocal deleted = buffer:sub( thePoint, eolPoint )buffer:remove( thePoint, eolPoint )killring_addorappend( deleted )endendcord.event.refresh_kill()endfunction cord.event.escape()if isearch_fstring thenisearch_fstring = nilcord.event.refresh()endif opt.oneLine thenwv.log 'oneLine edit got escape (cancel)'local _ = env.on.cancelled and env.on.cancelled()endendfunction cord.event.newline()if opt.oneLine thenif env.on.save thenenv.on.save( buffer:asOneString() )endelsebuffer:insert(thePoint,'\n')thePoint = thePoint+1local cx, cy = cursorpos( ptTopLeft, thePoint, wdim )if cy and (cy + 1 > wdim.h) thenlocal nextline = buffer:search_forward_to( ptTopLeft, '\n' )if nextline thenptTopLeft = nextline + 1endendcord.event.refresh()end-- if ccol <= #cursor_line() then-- local l = buffer[crow+baseline-1]-- buffer[crow+baseline-1] = l:sub(1,ccol-1)-- table.insert(buffer,crow+baseline,l:sub(ccol))-- else-- while (crow+baseline) > #buffer do-- table.insert(buffer,'')-- end-- table.insert(buffer,crow+baseline,'')-- end-- ccol = 1-- cord.event.next_line()endfunction cord.event.delete_backward_char()if isearch_fstring thenisearch_fstring = isearch_fstring:sub(1,#isearch_fstring-1)elseif thePoint > 1 thenthePoint = thePoint - 1buffer:remove(thePoint)elseding '(delete back) beginning of buffer'endendcord.event.refresh()endlocal last_collocal save_collocal function _go_if(newPoint)if newPoint and newPoint ~= thePoint thenthePoint = newPointsave_col = truereturn newPointendendlocal function lfn_backward_word()_go_if( buffer:backward_word(thePoint) )cord.event.refresh()endcord.event.backward_word = lfn_backward_wordlocal function lfn_delete_word()local endword = buffer:forward_word(thePoint) or (buffer:end_point()+1)if endword and endword > thePoint thenlocal removed = buffer:sub(thePoint,endword-1)killring_addorappend(removed)buffer:remove(thePoint,endword-1)cord.event.refresh_kill()elseding 'No more words (and no more promises)'endendcord.event.delete_word = lfn_delete_wordfunction cord.event.context_menu()local w = buffer:word_at_point(thePoint > 1 and (thePoint-1) or thePoint)wv.log('debug','context_menu word_at_point=%s',w)local function consume_word()lfn_backward_word()lfn_delete_word()endlocal _ = env.on.contextmenu and env.on.contextmenu(w,{ consume_word = consume_word })cord.event.refresh()endlocal function restore_col()-- wv.log('debug','last_col=%s',tostring(last_col))if last_col thenlocal eolpoint = buffer:end_of_line(thePoint)wv.log('debug','eolpoint=%d thePoint=%d last_col=%d',eolpoint,thePoint,last_col)if (eolpoint - thePoint) > last_col thenthePoint = thePoint + last_colelsethePoint = eolpointendendendfunction cord.event.set_mark_command()theMark = thePointcord.event.refresh()endlocal function _copy_marked_and(fn)local mb, me = get_marked()if me and me > mb thenlocal removed = buffer:sub(mb,me-1)killring_addorappend(removed)if fn thenfn(mb,me)endtheMark = nilcord.event.refresh()elseding 'No selection - use [C-x Spc] to mark'endendfunction cord.event.kill_ring_save()_copy_marked_and()endfunction cord.event.kill_region()_copy_marked_and( function(mb,me)buffer:remove(mb,me-1)thePoint = mbend)endfunction cord.event.next_line()local point = ptTopLeftlocal nextLineFromToplocal cx, cylocal thePointStart = thePointlocal curfun = watch_cursor_pos(ptTopLeft, thePoint, wdim)for l, col, nl in walkFun(buffer,ptTopLeft,wdim.w) doif not cx thencx, cy = curfun( l, col, nl )endlocal toDraw = nl and (nl - col + 1) or (#l-col+1)-- wv.log('debug','nextline #l=%d col=%d nl=%d toDraw=%d',#l, col, (nl or -1),toDraw)point = point + toDrawif nl thenif not nextLineFromTop thennextLineFromTop = point + 1-- wv.log('debug','got nextLineFromTop=%d',nextLineFromTop)endif point > thePoint thenif wordWrap and point >= (thePoint + wdim.w) thenthePoint = thePoint + wdim.wbreak -- don't restore_col, for nowelsethePoint = pointendwv.log('debug','point=%d cy=%d nextLineFromTop=%d',point,cy,nextLineFromTop)assert(cy) -- should have found cursorif cy + 1 >= wdim.h thenptTopLeft = nextLineFromTopend-- if not wordWrap thenrestore_col()-- endbreakendendendif thePoint == thePointStart thenthePoint = buffer:end_point() + 2endcord.event.refresh()endfunction cord.event.previous_line()if thePoint == 1 thending 'no previous line'returnendnextpoint = buffer:beginning_of_line(thePoint)if (nextpoint <= 2) and buffer:char_at_point_dec(1) == 10 then-- ugly special case for 1st char is '\n'thePoint = 1restore_col()elseif nextpoint > 1 thenlocal bol = buffer:beginning_of_line(nextpoint-1)if bol < ptTopLeft thenptTopLeft = bolendthePoint = bolrestore_col()elseding 'no previous line'endendcord.event.refresh()endlocal function _backward_char()if thePoint > 1 thenthePoint = thePoint - 1save_col = trueendendfunction cord.event.backward_char()if thePoint > 1 then_backward_char()cord.event.refresh()elseding 'At beginning of buffer'endendlocal function _forward_char()local ep = buffer:end_point()if thePoint <= ep thenthePoint = thePoint + 1save_col = trueelseif thePoint == (ep + 1) then-- when the buffer doesn't end in '\n', we allow point-- to move to buffer end + 2, to allow us to move forward-- to the next line.if buffer:char_at_point(ep) ~= '\n' thenthePoint = thePoint + 1save_col = trueendelseding 'End of buffer'endendfunction cord.event.forward_char()_forward_char()cord.event.refresh()endfunction cord.event.capitalize_word()-- hacky; emacs will search forward to the next word if-- not currently at a word.local w, bow, eow = buffer:word_at_point(thePoint)if bow < thePoint thenw = w:sub( thePoint-bow+1 )bow = thePointendif w thenbuffer:replace( bow, bow, w:sub(1,1):upper() )buffer:replace( bow+1, eow, w:sub(2):lower() )endcord.event.forward_word()endfunction cord.event.upcase_word()local w, bow, eow = buffer:word_at_point(thePoint)if w thenbuffer:replace( bow, eow, w:upper() )endcord.event.forward_word()endfunction cord.event.downcase_word()local w, bow, eow = buffer:word_at_point(thePoint)if w thenbuffer:replace( bow, eow, w:lower() )endcord.event.forward_word()endlocal function _forward_to(pat)return _go_if( buffer:search_forward_to(thePoint,pat) )endlocal function _forward_past(pat)return _go_if( buffer:search_forward_past(thePoint,pat) )endlocal function _backward_to(pat)return _go_if(buffer:search_backward_to(thePoint,pat))endlocal function _backward_past(pat)return _go_if(buffer:search_backward_past(thePoint,pat))endfunction cord.event.move_beginning_of_line()_go_if( buffer:beginning_of_line(thePoint) )cord.event.refresh()endfunction cord.event.move_end_of_line()local eol = buffer:end_of_line(thePoint)wv.log('debug','move_end_of_line eol=%d',eol)if eol == buffer:end_point() theneol = eol + 1end_go_if( eol )cord.event.refresh()endfunction cord.event.forward_word()local fw = buffer:forward_word(thePoint) or (buffer:end_point() + 1)-- wv.log('debug','forward_word: fw=%d thePoint=%d', fw, thePoint)_go_if( fw )cord.event.refresh()endfunction cord.event.scroll_down_command()local start = (thePoint < buffer:end_point()) and thePoint or (buffer:end_point()-1)local prior = buffer:backward_lines(start,wdim.h)if prior and prior ~= thePoint thenthePoint = priorrestore_col()local st = buffer:backward_lines(ptTopLeft,wdim.h)if st then ptTopLeft = st endelseding()endcord.event.refresh()endfunction cord.event.scroll_up_command()local s = NylonSysCore.uptime()local nextpoint = buffer:forward_lines(thePoint,wdim.h)-- env.report('dbg',string.format('nextpoint=%d',nextpoint))if nextpoint and nextpoint ~= startPoint thenthePoint = nextpointrestore_col()local st = buffer:forward_lines(ptTopLeft,wdim.h)if st thenptTopLeft = stendendlocal e = NylonSysCore.uptime()wv.log('debug','time to page down=%f',(e-s))cord.event.refresh()endfunction cord.event.beginning_of_buffer()ptTopLeft = 1thePoint = 1save_col = truecord.event.refresh()endfunction cord.event.end_of_buffer()local b = NylonSysCore.uptime()local ep = buffer:end_point()local top = buffer:backward_lines(ep,wdim.h-2)if top thenlocal prevline = buffer:backward_lines(top-1,1)wv.log('debug','eob top=%s prevl=%s',top,prevline)if prevline thenptTopLeft = prevlineelseptTopLeft = 1endelseptTopLeft = 1endthePoint = ep + (buffer:char_at_point_dec(buffer:end_point())==10 and 1 or 2)save_col = truelocal e = NylonSysCore.uptime()wv.log('debug','time for end_of_buffer=%f ptl=%d',(e-b),ptTopLeft)cord.event.refresh()endlocal function do_redraw()pdcur_draw_window( wdim )local curx,cury = cursorpos(ptTopLeft,thePoint,wdim)if curx thenw:move(wdim.y+cury, wdim.x+curx)elselocal ep = buffer:end_point()if thePoint > ep thenlocal curx, cury = cursorpos( ptTopLeft, ep, wdim)cury = cury or 0curx = curx or 0if (thePoint > ep+1) or buffer:char_at_point(ep)=='\n' thenw:move( wdim.y+cury+1, wdim.x )elsew:move( wdim.y+cury, wdim.x+curx+1 )endend-- curx = curx or 0-- cury = cury or 0-- local diff = thePoint-buffer:end_point()-- curx = curx + diff + (buffer:end_point()>0 and 1 or 0)-- w:move(wdim.y+cury,curx)env.report( 'warn', string.format('sz=%d pt=%d tl=%d (end of buffer)', ep, thePoint, ptTopLeft ))endif save_col thenlast_col = curxsave_col = falseendend--local showcur = ccol > (#cursor_line()+1) and (#cursor_line()+1) or ccol-- w:move( wdim.y + crow -1, wdim.x + showcur -1 )cord.event.redraw = function(ondone)wv.log('debug','window redraw, %dx%d@%d,%d', wdim.w, wdim.h, wdim.x, wdim.y)do_redraw()w:refresh()w:redraw()if ondone thenondone()endendwhile true dodo_redraw()w:refresh()local waskill = falsewasding = falsewait_first_event( cord, {refresh=function( force )if force thenw:redraw()endend,refresh_kill=function()waskill = trueend,})-- wv.log('debug','cord got event, lastkill=%s waskill=%s',lastkill,waskill)lastkill = waskillendendreturn {entryfn_edit = entryfn_edit}
local function Logwrite(...)endlocal function gen_search( text )Logwrite "Scratchpad.search text='#{text}'"local pretextrepeatpretext = texttext = text:gsub("%(%(", "( (")text = text:gsub("%)%)", ") )")until text == pretextprint(text)local qstr = ''local do_andlocal do_orlocal do_notlocal wordlist = {}local spcndx = 1while true dolocal b,e = text:find('%s+',spcndx)if b thentable.insert(wordlist,text:sub(spcndx,b-1))spcndx = e+1elsetable.insert(wordlist,text:sub(spcndx))breakendendfor _,v in ipairs(wordlist) do-- print(string.format('"%s"',v))end--print ' ------------'local function consume_logic()qstr = do_and and (qstr .. " and") or qstrqstr = do_or and (qstr .. " or") or qstrdo_and = truedo_or = falseend--[[--if word =~ /^\((.*)/ thenword = $1consume_logic.call()qstr += ' ( 'do_and = falsenext if word.length < 1end--]]--for _,word in ipairs(wordlist) dolocal b,e,match = word:find('^%((%a*)')if b then-- print('openparen',match)consume_logic()qstr = qstr .. ' ( 'do_and = falseword = matchendif #word > 0 thenif word == 'and' then;elseif word == 'not' thendo_not = trueelseif word == 'or' thendo_or = truedo_and = falseelselocal doit = truelocal closer = ''local b,e,match = word:find('(%a*)%)$' )if b thenword = matchcloser = ' ) 'if #word < 1 thenqstr = qstr .. closerdoit = falseendendif doit thenconsume_logic()if do_not thenqstr = qstr .. " (detail not like '%" .. word .. "%' and title not like '%" .. word .. "%')" .. closerdo_not = falseelseqstr = qstr .. " (detail like '%" .. word .. "%' or title like '%" .. word .. "%')" .. closerendendend -- not and, not, or orend --, #word > 0end -- for -loop-- print( 'qstr:', qstr )return qstrend--gen_search '(((mattm or miller) and (not discount)) or rcm) and reward'return {megasearch = gen_search}--[[--if word == 'and' thentrue # 'and' is assumed if not otherwise spec'edelsif word == 'not' thendo_not = trueelsif word == 'or' thendo_or = truedo_and = falseelsecloser = ''if word =~ /(.*)\)$/ thenword = $1closer = ' ) 'if word.length < 1 thenqstr += closernextendendconsume_logic.call()if do_not thenqstr += " (details not like '%#{word}%' and title not like '%#{word}%')" + closerdo_not = falseelseqstr += " (details like '%#{word}%' or title like '%#{word}%')" + closerendend}sql = "select * from #{tableName} where #{qstr} order by date_modified desc"Log.write "Searching: '#{qstr}' -- sql: '#{sql}' -- wordlist: #{wordlist.join(':')} text=#{text}"queryset = $db.execute( sql )queryset--]]----gen_search('foo and bar and gaba')--gen_search('(foo and bar) or zoobie')-- gen_search('(not (foo and bar)) or zoobie')--gen_search('((foo and bar) or zoobie) and gaba')
-- Buffer provides a set of higher level buffer operations to operate on the-- backing "textstore", of which Buffer is a subclass.--local NEWLINE=string.byte('\n',1)local wv = require 'nylon.debug'{ name = 'pls-buffer' }local Store = require 'pls-textstore'local Buffer = setmetatable({}, { __index = Store })function Buffer:new()return Store.new(Buffer)endfunction Buffer.search_forward_to( buffer, startPoint, pat )-- local okay, err = pcall( function() string.find('so',pat) end )-- if not okay then-- wv.log('error','bad pattern or something: %s', err)-- return-- else-- wv.log('debug','pattern checked okay, pat=%s', pat)-- endlocal point = startPointif #pat == 1 or (#pat==2 and pat:sub(1,1) == '%') thenwhile not buffer:char_at_point(point):find(pat) doif point < buffer:end_point() thenpoint = point + 1elsereturnendendreturn pointelse-- this is probably not real efficient, but it is quick and keeps patterns working (sort of)-- this could probably be done more efficiently with 'walkFragments'for i = startPoint, buffer:end_point() dolocal sub = buffer:sub( i, i+#pat-1 )wv.log('debug',"search-forward pt=%d pat='%s' sub='%s'", i, pat, sub)if sub:find(pat) thenreturn iendendendendfunction Buffer.search_forward_past( buffer, startPoint, pat )local point = startPointif #pat == 1 or (#pat==2 and pat:sub(1,1) == '%') thenwhile buffer:char_at_point(point):find(pat) andpoint <= buffer:end_point() dopoint = point + 1endreturn point <= buffer:end_point() and pointelse-- this is probably not real efficient, but it is quick and keeps patterns working (sort of)-- this could probably be done more efficiently with 'walkFragments'for i = startPoint, buffer:end_point() dolocal sub = buffer:sub( i, i+(#pat*2) )if not sub:find(pat) thenreturn pointendendendendfunction Buffer.search_backward_to( buffer, startPoint, pat )local point = startPointif #pat == 1 thenlocal c = string.byte(pat,1)while buffer:char_at_point_dec(point) ~= c doif point <= 1 thenreturnendpoint = point - 1endreturn pointelseif #pat==2 and pat:sub(1,1) == '%' thenwhile not buffer:char_at_point(point):find(pat) doif point <= 1 thenreturnendpoint = point - 1endreturn pointelseerror 'not implemented, multi-char search forward'endendfunction Buffer.search_backward_past( buffer, startPoint, pat )local point = startPointif #pat == 1 or (#pat==2 and pat:sub(1,1) == '%') thenwhile buffer:char_at_point(point):find(pat) doif point <= 1 thenreturnendpoint = point - 1endreturn pointelseerror 'not implemented, multi-char search forward'endendfunction Buffer:beginning_of_line( startPoint )local point = (self:char_at_point_dec(startPoint) == NEWLINE and startPoint > 1) and (startPoint-1) or startPointlocal found = self:search_backward_to(point,'\n')if found thenreturn (found + 1)elsereturn 1endendfunction Buffer.end_of_line( buffer, startPoint )local nexteol = buffer:search_forward_to(startPoint, '\n')return nexteol and nexteol or buffer:end_point() + 1end--- function Buffer.forward_lines( buffer, startPoint, nlines )--- local point = startPoint--- local lasteol--- for i = 1, nlines do--- local eol = Buffer.search_forward_to(buffer, point, '\n')--- if eol and eol < buffer:end_point() then--- point = eol + 1--- else--- ding()--- return point--- end--- end--- return pointfunction Buffer.backward_lines( buffer, startPoint, nlines )local pointif startPoint > buffer:end_point() +1 thenpoint = buffer:end_point()nlines = nlines - 1elsepoint = startPointendlocal linestart = pointwhile buffer:char_at_point_dec(point) == NEWLINE doif point ~= linestart thenif nlines > 0 thennlines = nlines - 1elsereturn pointendendpoint = point - 1endlocal lastfoundwhile nlines >= 0 dolocal found = Buffer.search_backward_to( buffer, point, '\n')if not found or found == point thenbreakendlastfound = foundif found <= 1 thenbreakendpoint = foundwhile (buffer:char_at_point_dec(point) == NEWLINE) and nlines >= 0 and point > 1 dopoint = point - 1nlines = nlines - 1endendif lastfound thenreturn lastfound + 1endendfunction Buffer.forward_word(buffer,point)if not buffer:char_at_point(point):find('%w') thenlocal p = buffer:search_forward_to(point,'%w')point = p or pointendreturn buffer:search_forward_past( point, '%w' )endfunction Buffer:backward_word(point)if point <= 2 thenreturn 1endif self:char_at_point(point):find('%w') thenif self:char_at_point(point-1):find('%w') thenlocal justBeforeWord = self:search_backward_past(point-1, '%w')return justBeforeWord and justBeforeWord + 1 or 1else-- we are at the first letter in a word.endendlocal lastLetterOfPreviousWord = self:search_backward_to( point-1, '%w' )if not lastLetterOfPreviousWord or lastLetterOfPreviousWord < 2 thenreturn 1 -- no more words, beginning of bufferendlocal bpw = self:search_backward_past( lastLetterOfPreviousWord-1, '%w' )return bpw and (bpw+1) or 1endfunction Buffer:word_at_point(point)local endOfWord = self:forward_word(point) or self:end_point()-- wv.log('debug','word_at_point endOfWord=%s', endOfWord)local beginningOfWord = self:backward_word(endOfWord)if endOfWord > beginningOfWord thenreturn self:sub(beginningOfWord,endOfWord-1), beginningOfWord, endOfWord-1endendfunction Buffer.asOneString( buffer )local all = {}for l in buffer:walkFragments(1) dotable.insert(all,l)endreturn table.concat(all)endfunction Buffer.forward_lines( buffer, startPoint, nlines )local point = startPointfor l, col, nl in buffer:walkFragmentsEOL(startPoint) doif nl thenpoint = point + (nl-col) + 1if nlines <= 1 thenreturn pointelsenlines = nlines - 1endelseif not l thenreturn (point ~= startPoint) and pointelsepoint = point + (#l - col) + 1endendendreturn pointendfunction Buffer:insertFileAtPoint( point, fname )local f = io.open(fname,'r')-- local buffer = Buffer:new()if f thenfor l in f:lines() dolocal fix = l:gsub('\t',' ') .. '\n'self:insert( point, fix )point = point + #fixendf:close()endendlocal function buffer_openFile( fname )local f = io.open(fname,'r')if f thenlocal buffer = Buffer:new()for l in f:lines() dobuffer:append(l:gsub('\t',' ') .. '\n')endf:close()endreturn bufferendlocal function buffer_withText(t)local buffer = Buffer:new()if t thenbuffer:append(t)endreturn bufferendreturn {openFile = buffer_openFile,withText = buffer_withText}
-- valid SRI patters-- :%w (id only, assumes PLS)-- %w+:%w+ (system / id-numword)-- %w+:%d+ (system / id-int)-- %w+:%w+:%w+ (org/system/id-numword)-- %w+:%w+:%d+ (org/system/id-int)local function on_save_scan_citations( db, id, text )wv.log('debug','got save for record id=%s #text=%d', id, #text)----- local srchstart = 1----- while true do----- local s, e, system, recid = text:find('(%w+):(%w+)', srchstart )----- if not s then----- break----- end----- wv.log('debug','found possible SRI, [%d,%d] "%s:%s"', s, e, system, recid)---------- -- @todo: run valiidity checks, ie, record id is all digits or else valid numword----- -- possibly enforce known systems etc----- table.insert( found, { s, e, sub } )----- srchstart = e + 1----- end-- 160613 start with pls-only idslocal found = {}local srchstart = 1while true dolocal s, e, plsid = text:find('%s:(%w+)', srchstart )if not s thenbreakendwv.log('debug','found possible SRI, [%d,%d] ":%s"', s, e, plsid)-- @todo: run valiidity checks, ie, record id is all digits or else valid numword-- possibly enforce known systems etctable.insert( found, { s, e, plsid } )srchstart = e + 1endif #found > 0 thenend-- @todo: compare list of fonud citations to current database table of citations.endreturn on_save_scan_citations
local ARGS = { ... }if not string.find(arg[-1],'lua523r') thentable.move(package.searchers, 1, #package.searchers, 2)package.searchers[1] = function(...)print('searching... ', ...)returnendendrequire 'clibs'-- require 'parseOpts'local Nylon = require 'nylon.core'()wv = require 'nylon.debug' { name = 'pls-main' }wv.log('debug', 'pls-main created; syscore.addCallback=%s', type(NylonSysCore.addCallback))local Sqlite = require 'sqlite'-- add path for nylabus servicespackage.path = '../nylabus/?.lua;' .. package.pathglOpts = {space = 'ncr'}if ARGS[1] thenglOpts.space = ARGS[1]endlocal Theme = require 'pls-theme'local plugin = {fixLine = function() end,onEditRecord = function() end,}pcall( function()plugin = loadfile( 'space/' .. glOpts.space .. '/plugin.lua' ){Theme = Theme}end)local VLINE = tonumber(Pdcurses.Lines.vline)local HLINE = string.byte('-',1) -- Some fonts don't have a good HLINE character, so use the dash. Suboptimal, but some fonts, like Monaco, are really nice except for this deficiency.local HLINE = tonumber(Pdcurses.Lines.hline)local WINDOWS = (NylonOs and NylonOs.IsWindows())wv.log('debug', 'color_pairs=%d', Pdcurses.Static.color_pairs() )local json = require 'JSON' -- for debugginglocal Winman = require 'pls-winman'local Numword = require 'pls-numword'local Servicelocal ok, err = pcall( function() Service = require 'nylaservice' end )if not ok thenwv.log('debug','could not load nylaservice, e=%s', err )endlocal gIdUser = 1 -- dmattp, defaultlocal cord_app -- this should be moved down to creation point by removing single use in make_editor()if arg[1] thenrecid = tonumber(arg[1])endlocal dbname = ( 'space/' .. (glOpts.space) .. '/notes.db' )-- print('db=', dbname)local db = Sqlite:new( dbname )if not db thenwv.log('error', 'db.a01 could not open name=%s', dbname)error 'no database'elsewv.log('debug', 'db open db=%s db.db = %s', tostring(db), type(db.db))end-- db.db:testVoid();-- db.db:testInt(123456);-- error 'done okay'-- db:exec('select count(*) from notes')-- open up the most recently edited record to startlocal lastnote = db:selectOne('select ROWID from note where dt_modified=(select MAX(dt_modified) from note)')if not lastnote thenlocal rc = db:exec('insert into note (detail,title,dt_created,dt_modified,id_user) values (?,?,DATETIME("NOW"),DATETIME("NOW"),?)','Welcome to PLS Notes', 'Welcome - First Record', gIdUser)lastnote = db:selectOne('select ROWID from note where dt_modified=(select MAX(dt_modified) from note)')endlocal recid = lastnote.ROWIDlocal Buffer = require 'pls-buffer'local keyhandlerlocal curses_startedlocal function start_curses()if curses_started thenPdcurses.Static.refresh()elsePdcurses.Static.noecho()Pdcurses.Static.start_color()Pdcurses.Static.raw()-- when mouse enabled, generates Pdcurses.key.mouse-- Pdcurses.Static.mouse_on(0x1fffffff) -- ALL_MOUSE_EVENTScurses_started = trueendendlocal function end_curses()if curses_started then -- and (not Pdcurses.Static.isendwin()) thenwv.log('debug', 'stopping curses end_curses()')Pdcurses.Static.endwin()endendlocal function curses_init()local screen = Pdcurses.Static.initscr()start_curses()local ncols, nrows = screen:getmaxx(), screen:getmaxy()return { screen = screen, ncols = ncols, nrows = nrows }endlocal env = { curses = curses_init() }local function shell_cmd( fmt, ... )Pdcurses.Static.endwin()-- -noprofile maybe nice, or not.os.execute( string.format('powershell -noninteractive /c "%s"',string.format(fmt,...) ) )Pdcurses.Static.noecho()env.curses.screen:redraw()env.curses.screen:refresh()Winman.Refresh()endlocal function simple_shell_cmd( fmt, ... )Pdcurses.Static.endwin()-- -noprofile maybe nice, or not.os.execute( string.format(fmt,...) )Pdcurses.Static.noecho()env.curses.screen:redraw()env.curses.screen:refresh()Winman.Refresh()endlocal function WindowDim( x, y, w, h )return { x = x, y = y, w = w, h = h }endfunction centerwindow(env,h,w)local x = (env.curses.ncols - w)/2local y = (env.curses.nrows - h)/2return h, w, y, xendlocal function withFocusAttrib( mw, fun )Theme.with.focuswinborder( Winman.isFocused(mw) and mw.win, fun )endfunction ui_yesno(cord,env,prompt,default)local wid = (#prompt)+2wid = wid < 14 and 14 or widlocal w = Pdcurses.Window(centerwindow(env,4,wid))local function showopt( hiliteyes )Theme.with.inverse( hiliteyes and w, function()w:mvaddstr(2,2,'[Y]es')end)Theme.with.inverse( (not hiliteyes) and w, function()w:mvaddstr(2,8,'[N]o')end )w:refresh()enddefault = default ~= nil and default or truelocal mwlocal function drawme()withFocusAttrib( mw, function() w:stdbox_() end )w:mvaddstr(1,1,prompt)showopt(default)endlocal removemelocal yes = Nylon.self:sleep_manual(function(wakefun)mw = Winman.win:new{ win = w, on = { key = function(k)local s = (type(k) == 'number' and k < 256 and string.char(k))wv.log('debug','yesno got key=%s/%s',k,s)if s == 'n' or s == 'N' thenwakefun(false)elseif s == 'y' or s == 'Y' thenwakefun(true)elseif k == 27 then -- escapewakefun(false)elseif default and k == 10 then -- newlinewakefun(default)endend,resized = function()drawme()end} }drawme()removeme = Winman.push_modal(mw)end)-- flash yes/no to provide feedbackshowopt( yes )w:refresh()Nylon.self:sleep(0.15)removeme()return yesendfunction ui_oneline(env,prompt,opt)opt = opt or {}local cord = Nylon.selflocal wid = type(opt)=='table' and opt.w or math.floor(env.curses.ncols * 0.8)local w = Pdcurses.Window(centerwindow(env,3,wid))local function draw()Theme.with.inverse( w, function()w:box( VLINE, HLINE )w:mvaddstr(0, 3, '[ ' .. prompt .. ': ]')w:mvaddstr(1, 1, string.rep(' ', wid-2))-- w:mvaddstr(2, 2, ' [E]nter ')end)w:refresh()endlocal ed = require 'pls-ed'local bbuffer = Buffer.withText( (type(opt)=='string') and opt or opt.text or '')local e = setmetatable( { w = w,on = {}}, { __index = env } )local editcord = Nylon.cord( 'editoneline', ed.entryfn_edit, e, bbuffer,{ wdim = WindowDim(1,1,wid-2,1),theme = { text = 'inverse' },oneLine = true } )if opt and opt.text and opt.replace thenif true theneditcord.event.move_end_of_line()Nylon.self:sleep(0.03)editcord.event.set_mark_command()Nylon.self:sleep(0.03)editcord.event.move_beginning_of_line()endendlocal mw = Winman.win:new{ win = w, on = { key = function(k)editcord.event.key(k)end,resized = function()draw()editcord.event.redraw()end} }local removeme = Winman.push_modal( mw )draw()local edited = Nylon.self:sleep_manual(function(wakefun)function e.on.save( text )wv.log('debug','got save oneline text=%s',text)wakefun( text )endfunction e.on.cancelled()wv.log('debug','cancelled oneline text edit')wakefun()endend)removeme()-- @todo: shutdown should be checked by the cord to break out of loop!editcord.event._shutdown()wv.log('debug','got edited text=%s',edited)return editedend-- global, meh--- function request_keys( cbfun )--- local old = keyhandler--- local enabled = true--- keyhandler = function(k)--- local handled = (enabled and cbfun(k)) or (old and old(k))--- wv.log('norm','keyhandled? %d=%s', k, handled and 'yes' or 'no')--- end--- return function()--- enabled = false--- end--- endlocal function picklist( title, query_sql, ... )local on_callbacks = {}if type(title) == 'table' thenif title.on thenon_callbacks = title.onendtitle = title.titleendlocal records = {}local function run_db_query(...)local ok, err = pcall( function(...)records = db:selectMany( query_sql, ... )end, ...)if not ok thenwv.log('error','error running SQL query=%s', tostring(err))endendrun_db_query(...)local width = math.floor(env.curses.ncols/3)local height = math.floor(env.curses.nrows*2/3)if height > #records + 2 thenheight = #records + 2endlocal dim = WindowDim(env.curses.ncols-width,0,width,height)local w = Pdcurses.Window(dim.h, dim.w, dim.y, dim.x)-- wv.log('debug','tostring=%s type=%s', tostring, type)-- wv.log('debug','created WINDOW w=%s', type(w))-- wv.log('debug','created WINDOW w.mvaddstr=%s,%s',tostring(w.mvaddstr),type(w.mvaddstr))-- wv.log('debug','created WINDOW w.box=%s,%s',tostring(w.box),type(w.box))local active = #records > 0 and 1 or 0local theTop = 0local mwlocal sstringlocal function draw()if (dim.w < 2) or (dim.h <2) thenreturnendlocal maxtextlen = dim.w - 2withFocusAttrib( mw, function()-- wv.log('debug','draw WINDOW w=%s,%s',tostring(w),type(w))-- wv.log('debug','draw WINDOW w.mvaddstr=%s,%s',tostring(w.mvaddstr),type(w.mvaddstr))-- wv.log('debug','draw WINDOW w.box=%s,%s',tostring(w.box),type(w.box))w:box(VLINE, HLINE)w:mvaddstr(0,3,title)end )for row = 1, dim.h-2 dolocal drawingRecord = row + theToplocal r = records[drawingRecord]if not r then break endlocal text = string.format("%.10s %5s %s", r.dt_modified, Numword.to_s(r.ROWID),r.title):sub(1,maxtextlen)Theme.with.inverse( drawingRecord == active and w, function()local s,eif sstring thens,e = text:lower():find(sstring)endif s thenw:mvaddstr(row,1,text:sub(1,s-1))Theme.with.classicmenu( w, function()w:addstr(text:sub(s,e))end)Theme.with.inverse( drawingRecord == active and w, function()w:addstr(text:sub(e+1))end)elsew:mvaddstr(row,1,text)endend )if #text < maxtextlen thenw:addstr( string.rep(' ',maxtextlen-#text) )endendif sstring thenTheme.with.classicmenu( w, function()w:mvaddstr(dim.h-1,4,'[i-search: ' .. sstring .. ' ]')end)endw:refresh()endlocal removewinlocal cord = Nylon.cord('picklist',function(cord,args)function cord.event.beginning_of_buffer()theTop = 0active = 1draw()endfunction cord.event.end_of_buffer()active = #recordstheTop = active - (dim.h-2)draw()endfunction cord.event.scroll_up_command()active = active + (dim.h-2)if active > #records thenactive = #recordsendif active > theTop + (dim.h-2) thentheTop = active - (dim.h-2)enddraw()endfunction cord.event.next_line()if active < #records thenactive = active + 1if active > theTop + (dim.h-2) thentheTop = active - (dim.h-2)enddraw()endendfunction cord.event.scroll_down_command()active = active - (dim.h-2)if active < 1 thenactive = 1endif (active-1) < theTop thentheTop = (active-1)enddraw()endfunction cord.event.previous_line()if active > 1 thenactive = active - 1if (active-1) < theTop thentheTop = (active-1)enddraw()endendlocal donefunction cord.event.kill_buffer()done = trueremovewin()wv.log('debug','close picklist, on_callbacks.closed=%s',on_callbacks.closed)local _ = on_callbacks.closed and on_callbacks.closed()end-- very simple and dumb refresh mechanismwhile true docord:sleep(20)if done then break end-- wv.log('debug','run db query #records=%d #args=%d',#records,#args)run_db_query(table.unpack(args))-- wv.log('debug','ran db query, #records=%d',#records)-- this is a minor problem that if a modal window is up,-- redrawing will clobber the modal windowdraw()endend, { ... } )local function isearch_search_from(sstring,from,dir)dir = dir or 1local nrecords = #recordsif nrecords < 1 then wv.log('error','no search records?'); return endfor j = from, from+(nrecords*dir), dir do-- wv.log('debug','isrch from=%d dir=%d j=%d #records=%d', from, dir, j, #records )local i = ((j-1) % nrecords)+1-- wv.log('debug','isrch from=%d j=%d i=%d dir=%d', from, j, i, dir )if string.find(records[i].title:lower(),sstring) orstring.find(Numword.to_s(records[i].ROWID):lower(),sstring) thenactive = ireturnendend-- ding()endlocal function isearch_find_next(str,dir)dir = dir or 1isearch_search_from(sstring,active+dir,dir)draw()endlocal function isearch_on_string_update(sstring)isearch_search_from(sstring,active)draw()endmw = Winman.win:new{ win = w, on = {resized = function(newdim)dim = (newdim or dim)wv.log('debug','picklist resized, %dx%d@%d,%d', dim.w, dim.h, dim.x, dim.y)draw()end,key = function(k)wv.log('debug','picklist got key=%s',k)if type(k) == 'string' and cord:has_event(k) thencord.event[k]()elseif k == 10 then -- newlinelocal _ = records and records[active] and cord_app.event.OpenRecord( records[active].ROWID )elseif k == 46 then -- '.'if not sstring thencord.event.kill_buffer()local _ = records and records[active] and cord_app.event.OpenRecord( records[active].ROWID )elsesstring = sstring .. string.char(k)isearch_on_string_update( sstring )endelseif k == 8 and sstring thensstring = sstring:sub(1,#sstring-1)isearch_on_string_update(sstring)elseif k == 19 or k == 18 then -- C-sif sstring thenisearch_find_next(sstring, (k == 19) and 1 or -1)-- go to next after currentelsesstring = '' -- start isearch forwarddraw()endelseif sstring thenif type(k) == 'number' and k >= 32 and k <= 128 thensstring = sstring .. string.char(k)isearch_on_string_update(sstring)elseif k == 27 or k == 7 then -- ESC or Ctrl+gsstring = nildraw()endelsereturn kendendend } }removewin = Winman.push( mw )endlocal function args_concat( ... )local t = { ... }return table.concat(t)endlocal function menuOptions( keys )menutext = {}for _,v in pairs(keys) dolocal key = type(v)=='string' and v:sub(1,1) or v[1]local str = type(v)=='string' and v:sub(2) or v[2]table.insert( menutext, args_concat( '[', key, ']', str ) )endreturn menutextendlocal function MakeMenu( keys )local menuText = menuOptions(keys)local text = args_concat( ' ', table.concat(menutext, ' '), ' ' )local menuwin = Pdcurses.Window(1,#text,0,0)local function draw()Theme.with.classicmenu( menuwin, function()menuwin:mvaddstr(0,0,text)end)menuwin:refresh()endreturn menuwin, drawendlocal function MakeMenuVert( keys )local menuText = menuOptions(keys)local maxlen = 0for i, v in ipairs(menuText) dov = args_concat(' ', v, ' ')menuText[i] = vmaxlen = (#v > maxlen) and #v or maxlenendwv.log('debug','MakeMenuVert: maxlen=%d #menuText=%d', maxlen, #menuText)local text = args_concat( ' ', table.concat(menutext, ' '), ' ' )-- local menuwin = Pdcurses.Window(#menuText,maxlen,0,0)local menuwin = Pdcurses.Window(#menuText,maxlen,1,0)local function draw()Theme.with.classicmenu( menuwin, function()for i, v in ipairs(menuText) do-- menuwin:mvaddstr( 1+i, 0, v )menuwin:mvaddstr(i-1,0,v)menuwin:mvaddstr(i-1,#v, (' '):sub(1,maxlen-#v))endend)menuwin:refresh()endreturn menuwin, drawendlocal function menuedFunctions( invokingCord, ftab, opt )local tOptions = {}for k, _ in pairs(ftab) dotable.insert( tOptions, k )endlocal menuwinlocal drawmenuif opt and opt.vert thenmenuwin, drawmenu = MakeMenuVert(tOptions)elsemenuwin, drawmenu = MakeMenu(tOptions)endlocal grabbed = {}for k, v in pairs(ftab) dolocal optkeyif type(k) == 'table' thenoptkey = string.byte(k[1],1)elseoptkey = string.byte(string.lower(string.sub(k,1,1)),1)endgrabbed[ optkey ] = vendlocal managed = Winman.win:new{ win = menuwin,on = { key = function(k) invokingCord.event.exitContextMenu(k) end,resized = function() drawmenu() end} }local winman_remove = Winman.push_modal( managed )local key = invokingCord:sleep_manual( function(wakefun)invokingCord.event.exitContextMenu = function(k)wv.log('debug','app key grab, k=%s',k)wakefun(k)endend )winman_remove()if grabbed[key] thenwv.log('debug','app grab key 2=%s',key)grabbed[key]()endendlocal function getrecid(recid)recid = recid and (type(recid)=='number' or recid:find('^%d')) and recid or Numword.to_i(recid)return recidendlocal function FindRecordById(recid1)local altrecidif type(recid1) == 'table' thenaltrecid = recid1[2]recid1 = recid1[1]wv.log('debug','got rec/alt=%s/%s',recid1, altrecid)endlocal recid = getrecid(recid1)wv.log('debug','getrecid=%s recid1=%s',recid, recid1)if not recid and altrecid thenrecid = getrecid(altrecid)endif not recid thenif not recid1 thenrecid = ui_oneline(env,'Enter record id', {w=30})if not recid then -- cancelledreturnendrecid1 = recidlocal r2 = getrecid(recid)wv.log('debug','getrecid oneline=%s r2=%s',recid, r2)recid = r2endendlocal function selectBestMatch(set,id)id = id:lower()local strmatch = ':' .. id .. '%W'local bestfor _, r in pairs(set) dolocal downtitle = r.title:lower()local hasExact = string.find( downtitle, ':' .. id .. '$')wv.log('debug','selectBestMatch match=/%s/ hasExact=%s title="%s" r=%s best=%s', strmatch, hasExact, r.title, r, best)if hasExact then -- always prefer a record whose title is exactly the tag!best = relsebest = best or (string.find( downtitle, strmatch ) and r) -- secondly prefer a record which has the :tag followed by non-word (space,punctuation,etc; not 's' for plural!)endendreturn best or set[1]endlocal recordif recid then -- check for keywordrecord = db:selectOne('select rowid, title, detail, dt_created, dt_modified from note where rowid=?', tonumber(recid) )endif record thenreturn record, false, recid1elsewv.log('debug','looking for title like=%s',recid1)-- prefer records with a leading ":" as a tag, with the exact string first followed by a prefix/stem matchrecords = db:selectMany('select rowid, title, detail, dt_created, dt_modified from note where title like ?', '%:' .. recid1 .. '%' )record = selectBestMatch( records, recid1 )record = record or db:selectOne('select rowid, title, detail, dt_created, dt_modified from note where title like ?', '%' .. recid1 .. '%' )return record, true, recid1endendlocal Keys = require 'pls-keys'local on_save_scan_citations = require 'on-save-scan-citations'local gOpenRecords = {}local function make_editor( env, record )local ed = require 'pls-ed'if record.ROWID and gOpenRecords[record.ROWID] thenwv.log('abnorm','record[%s] already open', Numword.to_s(record.ROWID))local win = gOpenRecords[record.ROWID]Winman.this_window_to_primary(win)returnendlocal nrows, ncols = env.curses.nrows, env.curses.ncolslocal dim = WindowDim( 0, 0, math.floor(ncols/2), env.curses.nrows )local currentRev = 0local function setCurrentRev()if record.ROWID thenlocal found = db:selectOne('SELECT COUNT(*) as rev from patch where id_note=?',record.ROWID)currentRev = found and tonumber(found.rev) or currentRevendendsetCurrentRev()---------------------------------------------------------------------- Border-- local wb = Pdcurses.Window(h+2,w+2,y-1,x-1)-- wb:stdbox_()-- wb:refresh()---------------------------------------------------------------------- Edit windowlocal win = Pdcurses.Window( dim.h, dim.w, dim.y, dim.x )local mw = { win = win } -- Winman managed window, set at the bottom of this functionlocal function drawbox()withFocusAttrib( mw, function()-- win:stdbox_()win:box( tonumber(VLINE), HLINE ) --5VLINE, '-' )end )endlocal bbuffer---------------------------------------------------------------------- Titlelocal function settitle()local recwordif record.ROWID thenrecword = Numword.to_s(record.ROWID)elserecword = "*NEW*"endlocal ttext = ' ' .. record.title:sub(1,dim.w-16) .. ' [ ' ..recword .. '.' .. currentRev .. (bbuffer and bbuffer:isModified() and '* ] ' or ' ] ')win:move(0,1)withFocusAttrib( mw, function()win:hline(HLINE,dim.w-2)win:mvaddstr(0,(dim.w-2-#ttext),ttext)end )endsettitle()local cord = Nylon.selflocal function editTitle()wv.log('debug','got call to editTitle')local starttext = record.title == 'new record' and '' or record.titlelocal title = ui_oneline(env,'Edit Title', { text=starttext })if title thenrecord.title = titlesettitle()win:refresh()if record.ROWID thendb:retryexec('update note set title=?,dt_modified=DATETIME("NOW") where rowid=?', title, record.ROWID)endendreturn trueend---------------------------------------------------------------------- Status bar-- local statuswin = Pdcurses.Window(1,w,y+h+1,x)local laststatuslocal function winstatus( sttype, data )-- statuswin:hline(HLINE,w+2)local prevx, prevy = win:getx(), win:gety()local statusrow = dim.y + dim.h - 1laststatus = datalocal crdate = '???'local update = '???'if record and record.dt_created thenlocal d = record.dt_createdcrdate = d:sub(3,4) .. d:sub(6,7) .. d:sub(9,10)endif record and record.dt_modified thenlocal d = record.dt_modifiedupdate = d:sub(3,4) .. d:sub(6,7) .. d:sub(9,10)enddata = string.format('date=%s/%s %s', crdate, update, data)win:move(statusrow,1)withFocusAttrib( mw, function()win:hline(HLINE,dim.w-2)end )-- statuswin:clear()-- statuswin:move(0,0)win:mvaddstr(statusrow,3,'[')win:addstr(data)win:addstr(']')--local left = w - 6 - #data + 2--statuswin:addstr(string.rep('_',left))win:move(prevy, prevx)win:refresh()endlocal titlecache = {}local editcordrefresh = nillocal function plsed_drawline( win, x, y, text, markBeg, markEnd )if #text > (dim.w - 2) thenwin:mvaddstr( y, x, text:sub(1,dim.w-2) )return dim.w-2end-- markBeg, markEnd indicate the text in the section should-- be highlightedif not markBeg thenlocal b,e,hdr,htext = text:find("^(h%d%.)(.*)") -- textile specific highlighting!! DANGER!! DANGER!!if not b thenwin:mvaddstr( y, x, text )elsewin:mvaddstr( y, x, hdr )Theme.with.heading( win, function()win:addstr(htext)end)endelselocal endPoint = markEnd > (dim.w-2) and (dim.w-2) or markEndwin:move(y,x)win:addstr( text:sub(1,markBeg-1) )Theme.with.inverse( win, function()win:addstr( text:sub(markBeg,endPoint) )end )win:addstr( text:sub(endPoint+1,dim.w-2) )endlocal drawn = #textlocal remain = (dim.w -2) - drawnlocal s,e,tag = string.find(text,':(%w+)$')if s thenif Numword.isvalid( tag ) then-- win:move(y,x)if not titlecache[tag] thenlocal rec = db:selectOne( 'select title from note where rowid=?',Numword.to_i(tag) )titlecache[tag] = rec and rec.titleendlocal append = titlecache[tag] or '???'local todraw = string.sub( (' ' .. append), 1, remain)Theme.with.autotext( win, function()win:addstr( todraw ) -- text.sub(s, e) )end)return drawn + #todrawendend-- here we look for codings, e.g., cross-system references that may be-- unique to the active spaceplugin.fixLine(text, win)endlocal e = setmetatable( { w = win,report = winstatus,on = {}}, { __index = env } )-- for simple testingbbuffer = Buffer.withText( record.detail )local editordim = WindowDim(1,1,dim.w-2,dim.h-2)local cord_edit = Nylon.cord( 'edit', ed.entryfn_edit, e, bbuffer,{ wdim = editordim,plug = {drawline = plsed_drawline}} )editcordrefresh = function() cord_edit.event.refresh() endlocal removewin -- callback set when Winman.push is called-- kill_buffer() callback from editor cordfunction e.on.kill_buffer()-- cord.event._shutdown()wv.log('debug','got kill buffer removewin=%s',removewin)if record.ROWID thengOpenRecords[record.ROWID] = nilendremovewin()endfunction e.on.contextmenu(wordAtPoint, opt)wv.log 'got on.contextmenu callback'local function show_insert_menu()local function insert_link()local record, makenew, recid1 = FindRecordById(wordAtPoint)wv.log('debug', 'frbid record=%s makenew=%s recid1=%s', record, makenew, recid1 )if record thenlocal id = record.ROWIDlocal nw = Numword.to_s(id)wv.log( 'debug', 'got record, id=%s / nw=%s', id, nw )local text = string.format(':%s\n', nw)local _ = opt and opt.consume_word and opt.consume_word()cord_edit.event.insert_at_point( text )endendmenuedFunctions( cord_edit, {XML = function() end,Nonsense = function() end,Link = insert_link}, { vert = true })end-- @todo: check buffer for newlineslocal function insert_yank_with_tag( tag )-- if buffer:char_at_point(thePoint) ~= '\n' then cord_edit.event.insert_at_point( '\n' ) endcord_edit.event.insert_at_point( '\n<' .. tag .. '>\n' )cord_edit.event.yank()Nylon.self:sleep(0.01) -- give yank process time to complete; hate this, but yank i think depends on events propogating back to main cord-- if buffer:char_at_point(thePoint) ~= '\n' then cord_edit.event.insert_at_point( '\n' ) endcord_edit.event.insert_at_point( '</' .. tag .. '>\n' )endmenuedFunctions( cord_edit,{Insert = show_insert_menu,Paste = function()menuedFunctions( cord_edit, {Pre = function() insert_yank_with_tag 'pre' end,Quote = function() insert_yank_with_tag 'blockquote' end}, { vert = true })end})endlocal function possibly_transform_clipboard_text( text )wv.log('debug', 'transform clibpboard=[[%s]]', text)if string.find(text,'^(http://.*)') or string.find(text,'^(https://.*)') thenreturn string.gsub(text, ' ', '%%20')elsereturn textendendfunction e.on.yank(special)if not NylonOs thenreturnendlocal puthtm-- wv.log('debug','got yank, special=%s',special)local cbrc = {}NylonOs.Static.getclipboard_ext(function(tyyp, content)cbrc = { tyyp = tyyp, content = content }end )if cbrc.tyyp thenlocal tyyp = cbrc.tyyplocal content = cbrc.content-- wv.log('debug','clipboard has data of type=%s',tyyp)-- this is sort of unique to windows; see internet docs on on CF_HTML-- clipboard content type.if special and tyyp == 'html' thenlocal _, _, m = content:find '<!%-%-StartFragment%-%->(.*)<!%-%-EndFragment%-%->'if m thencord_edit.event.insert_at_point( m ) -- :sub(s,e)puthtm = trueendendif tyyp == 'text' and (not puthtm) thencord_edit.event.insert_at_point( possibly_transform_clipboard_text(content) )endif tyyp == 'CF_HDROP' then(function()local File = require 'filelib'local Table = require 'extable'local fileURLsRaw = Service.archiver.archiveFileList( content )local fileURLs = Table.map( function(url)local base = File.leaf(url)return string.format('"%s":%s', base, url)end, fileURLsRaw )local itextif #content > 1 thenitext = '* ' .. table.concat( fileURLs, '\n* ' )elseitext = fileURLs[1]endcord_edit.event.insert_at_point( itext )end)()endif tyyp == 'image/png' thendbimg = dbimg or (Sqlite:new 'images.db')dbimg:exec('insert into image (raw) values (?)', content)local rowid = dbimg:lastRowId()cord_edit.event.insert_at_point(string.format("!:img:%s!\n", Numword.to_s(rowid) ) )local copy = dbimg:selectOne('select raw from image where ROWID=?', rowid);if copy thenwv.log('debug','Got image out, sz=%d', #copy.raw)elsewv.log('error','cant read saved image??')end-- shell_cmd 'get-process snippingtool | stop-process"'endendreturn true;end-- save() callback from editor cordfunction e.on.save( text )wv.log('debug','save new record=%d text=%s',record.ROWID,text:sub(1,40))local prevText = (currentRev > 0) and record.detail or ''local Diff = require 'diff_match_patch'-- create patch to turn text into previous textlocal patches = Diff.patch_make(text,prevText)local patchtext = Diff.patch_toText(patches)-- create patch to turn previous text into text-- hate doing seperate fwd/reverse patchtext, but there's not an easy algo provided with diffmatchpatch for reversinglocal rpatches = Diff.patch_make(prevText,text)local rpatchtext = Diff.patch_toText(rpatches)if #patchtext > 0 thenwv.log('debug','edit, patch is this (retryexec=%s):\n%s', tostring(db.retryexec), patchtext)db:retryexec('insert into patch (id_note,dt_created,content,rcontent,id_user,revision) values (?,DATETIME("NOW"),?,?,?,(SELECT COUNT(*) from patch where id_note=?))',record.ROWID, patchtext, rpatchtext,gIdUser, record.ROWID)endon_save_scan_citations( db, record.ROWID, text )local isNew = falseif record.ROWID thendb:retryexec('update note set detail=?,dt_modified=DATETIME("NOW") where rowid=?', text, record.ROWID )elselocal rc = db:retryexec('insert into note (detail,title,dt_created,dt_modified,id_user) values (?,?,DATETIME("NOW"),DATETIME("NOW"),?)',text, record.title, gIdUser)record.ROWID = db:lastRowId()gOpenRecords[record.ROWID] = mwisNew = trueendplugin.onEditRecord( isNew, record )bbuffer:setUnmodified()record.detail = textsetCurrentRev()settitle()end -- end e.on.save()function e.on.modified()settitle() -- reset the "modified" indicatorendfunction e.on.tagActivated( tag, alttag )wv.log('debug','tagActivated, tag=%s (alt=%s)',tag, alttag)cord_app.event.OpenRecord{ tag, alttag }endfunction e.on.queryCitations( tag, alttag )local nw = Numword.to_s(record.ROWID)wv.log('debug','queryCitations, id=%s/%s', record.ROWID,nw)picklist( ('Citations of :' .. nw .. ' ' .. record.title),'select ROWID,dt_modified,title from note where detail like ? order by dt_modified desc limit 50', ("%:" .. nw .. "%") )end-- callback when winman passes a key to melocal function winman_keyhandler(k)if k == Keys.control.editTitle thencord_app:add_pending(editTitle)elseif type(k) == 'string' thenif cord_edit:has_event(k) thencord_edit.event[k]()elseif k == 'mail_record' thenlocal recname = Numword.to_s(record.ROWID)wv.log('debug','mailing record=%s',recname)shell_cmd( 'mpnote-sendmail %s', recname )-- elseif k == 'start_snipping_tool' then-- simple_shell_cmd 'c:\\windows\\system32\\SnippingTool.exe'elseif k == 'extract_to_new_record' thenNylon.cord('extract_to_new_record',function(cord)cord:sleep_manual(function(wake)cord_edit.event.replace_marked(function(marked_text)if not marked_text thenwake()returnendlocal proposedTitle = 'New Record from: ' .. record.titlelocal newtitle = ui_oneline( env, 'Extracted Record Title', { text = proposedTitle })if not newtitle thenreturn falseendlocal r = { title = newtitle, detail = marked_text }local rc = db:retryexec('insert into note (detail,title,dt_created,dt_modified) values (?,?,DATETIME("NOW"),DATETIME("NOW"))', r.detail, r.title)r.ROWID = db:lastRowId()-- r.ROWID = 1379make_editor(env,r)wake()return ':' .. Numword.to_s(r.ROWID) .. '\n'end)end )end )elseif k == 'insert_ref_to_new_record' thenNylon.cord(k, function(cord)local function doit(wakefun)local proposedTitle = 'New Record from: ' .. record.titlelocal newtitle = ui_oneline( env, 'New Record Title', { text = proposedTitle })if not newtitle thenwakefun()returnendlocal r = { title = newtitle, detail = '\n\n' }local rc = db:retryexec('insert into note (detail,title,dt_created,dt_modified) values (?,?,DATETIME("NOW"),DATETIME("NOW"))', r.detail, r.title)r.ROWID = db:lastRowId()-- r.ROWID = 1379make_editor(env,r)cord_edit.event.insert_at_point( ':' .. Numword.to_s(r.ROWID) )endcord:sleep_manual( doit )end )elseif k == 'insert_file' thenNylon.cord('insert_file_cord',function(cord)cord:sleep_manual( function(wake)local fname = ui_oneline( env, 'Insert file' )if fname thenwv.log('debug','insert_file=%s',fname)cord_edit.event.getPoint(function(thePoint)wv.log('debug','insert_file point=%d',thePoint)bbuffer:insertFileAtPoint( thePoint, fname )cord_edit.event.refresh()wake()end)endend )end )elsereturn kendendelsecord_edit.event.key(k)endendend-- callback when winman resizes mefunction winman_resized( newdim )if newdim thendim.x, dim.y, dim.w, dim.h = newdim.x, newdim.y, newdim.w, newdim.hend-- editor x,y is always offset 1,1 of the windoweditordim.w, editordim.h = dim.w-2, dim.h-2drawbox()settitle()winstatus('resized', laststatus or '-resized-')wv.log('debug','winman_resized win@%d,%d , ed, %dx%d@%d,%d', dim.x,dim.y,editordim.w, editordim.h, editordim.x, editordim.y)if newdim then -- don't redraw if we're just changing focusNylon.self:sleep_manual(function(wakefun)cord_edit.event.redraw( wakefun )end)endwv.log('debug','winman_resize done')endmw = Winman.win:new{win = win,on = { resized = winman_resized,key = winman_keyhandler,}}removewin = Winman.push( mw )if (record.ROWID) thengOpenRecords[record.ROWID] = mwendend -- function make_editor()local function entryfn_app( cord, env )cord.event.quit = function()local yes = ui_yesno(cord,env,'Exit now?')wv.log('debug','exit=%d',yes and 1 or 0)if yes thencollectgarbage();Pdcurses.Static.clear()Pdcurses.Static.refresh()Pdcurses.Static.endwin()os.exit(1)end-- dmp this was hear to hide window when i was less familiar with ncurses-- window refreshes and before we had a window manager. it may not be needed-- any longercollectgarbage()Pdcurses.Static.refresh()endfunction cord.event.save_buffers_kill_terminal()cord.event.quit()endfunction cord.event.other_window()Winman.other_window()endfunction cord.event.focused_window_to_primary()Winman.focused_window_to_primary()endcord.event.SwapBuffers = function()wv.log 'app cord.event.SwapBuffers'Winman.swap_tiled()endcord.event.NewRecord = function( opt )local record = { title = opt and opt.title or 'new record', detail = '\n\n' }make_editor( env, record )endcord.event.OpenRecord = function(recid1)local record, makenew, recid1 = FindRecordById(recid1)wv.log('debug', 'frbid record=%s makenew=%s recid1=%s', record, makenew, recid1 )if (not record) and makenew and recid1 thenlocal newt = tostring(recid1)if newt:sub(1,1) == ':' thennewt = newt:sub(1,1) .. newt:sub(2,2):upper() .. newt:sub(3)elsenewt = ':' .. newt:sub(1,1):upper() .. newt:sub(2)endcord.event.NewRecord{ title = (newt .. ' new record') }returnendif record thenwv.log('debug','got record=%s',json:encode(record))make_editor( env, record )-- warn / status??-- 'could not find record for id=%s', tostring(recid)endendlocal lastsearch = ''cord.event.Search = function()local sstr = ui_oneline( env, 'Search For', { text = lastsearch, replace = true } )if not sstr then return endlocal S = require 'pls-db'local fancysearch = S.megasearch( sstr )wv.log('debug','fancysearch=%s',fancysearch)lastsearch = sstrpicklist( ('Search: ' .. sstr),'select ROWID,dt_modified,title from note where ' .. fancysearch .. string.format(' order by dt_modified desc limit %d',((env.curses.nrows-2)*6)) )endwhile true docord.event.grab.wait()wv.log('debug','main window got grab key')menuedFunctions( cord, {New = function() cord.event.NewRecord() end,Goto = function() cord.event.OpenRecord() end,PushFS = function() Winman.toggle_fullscreen() end,Quit = function() cord.event.quit() end,[{ '/', 'Search' }] = function() cord.event.Search() end,})endend -- end, function entryfn_applocal function ding(x,p,...)end-- state variables to track whether a prefix is activelocal command_prefix = falselocal personal_prefix = falselocal esc_prefix = false-- Curses on unix systems generally don't support unique "Alt" sequences-- like M-f, M-b, M-d which are so key to the emacs editing experience.-- instead these are sent as escape sequences, ie, ESC followd by "f".-- This means our main keyhandler has to "trap" the key immediately following-- the escape and convert it to a unique "M-letter" style event. But this-- means that when escape is pressed we are _waiting_ for the next-- keystroke and won't send the escape key immediately, which makes the escape-- key almost useless. This cord allows the escape key to be sent on by-- itself if no follow-on key is received within a certain amount of time.local cord_eschandler=Nylon.cord('escsender',function( cord )local count = 1local esccountcord.event.gotesc = function(k)esccount = countcord.event.sendesc()endcord.event.gotnonesc = function(k)count = count + 1endwhile true docord.event.sendesc.wait()cord:sleep(0.1) -- wait 100 msif esccount == count then -- no keys received since escwv.log('debug','should send escape here')Winman.inject_key( 9027 ) -- ctrl+g-- Winman.inject_key( 27 ) -- ctrl+gendendend)local keymap_everybody = Keys.everybodylocal keymap_command_prefix = Keys.command_prefixlocal keymap_personal_prefix = Keys.personal_prefixlocal keymap_esc_prefix = Keys.esc_prefixlocal function app_keymapper( k )local function handle_if_app_event_or_return( k )if cord_app:has_event(k) thencord_app.event[k]()elsereturn kendendlocal function unknown(c)if k >= 32 and k < 128 thending( 'Unknown prefix command Ctrl-%s + "%c"', c, k )elseif k < 27 thending( 'Unknown prefix command Ctrl-%s + Ctrl-%c', c, (k+64) )elseding( 'Unknown prefix command Ctrl-%s + %d', c, k )endendendif k == 27 then -- escesc_prefix = truecord_eschandler.event.gotesc()returnelsecord_eschandler.event.gotnonesc()endif k == 9027 then -- real escape; see cord_eschandlerk = 27endif esc_prefix thenesc_prefix = falseif keymap_esc_prefix[k] thenreturn app_keymapper( keymap_esc_prefix[k] )endendif command_prefix thencommand_prefix = falseif keymap_command_prefix[k] then-- Map the sequence of keys (C-x s, C-x k, etc) to a named event. The named event-- string is then forwarded to key handlers just as an integer key value would bewv.log('debug','got cmd prefix mapped [%d=>%s]',k,keymap_command_prefix[k])return handle_if_app_event_or_return( keymap_command_prefix[k] )elseunknown 'X'endelseif personal_prefix thenpersonal_prefix = falsereturn handle_if_app_event_or_return( keymap_personal_prefix[k] )elseif keymap_everybody[k] thenreturn handle_if_app_event_or_return( keymap_everybody[k] )elseif k == 3 then -- begin personal prefixpersonal_prefix = truewv.log 'start personal prefix'elseif k == 435 then -- M-swv.log 'got swap / M-s key'cord_app.event.SwapBuffers()elseif k == Keys.control.menu then -- C-lcord_app.event.grab()elseif k == 20 or k == 24 then -- C-t or C-xcommand_prefix = trueelsereturn k -- not handled or translated; pass on the original keyendendlocal function app_unhandledkeys(k)wv.log('debug','app unhandled key handler key=%s',k)if k == 'jumptotag' thencord_app.event.OpenRecord()elseif k == 'toggle_landscape' thenWinman.toggle_landscape()elsewv.log('debug','unhandled key/input event=%s',k)endend------------------------------------------------------------------------------------------------------------------------------------local function cordfn_followup( cord )while true dolocal today = os.date("%y%m%d",os.time())local fupdate = string.format('%%followup%s%%', today)local followupCount = db:selectOne('select count(*) as thecount from note where title like ? or detail like ?', fupdate, fupdate)wv.log('debug','followup search [%s] rc=%s', fupdate, json:encode(followupCount))if followupCount and tonumber(followupCount.thecount) > 0 thenlocal manualClose = falselocal done = falsepicklist({ title = string.format('Follow Up %s', today),on = {closed = function()wv.log('debug','followup picklist manually closed')manualClose = trueif done thendone()enddone = function() endend} },'select ROWID, dt_modified, title from note where title like ? or detail like ?', fupdate, fupdate)while (not done) and (today == os.date("%y%m%d", os.time())) dowv.log('debug', 'followup picklist open; wait until closed or new day')cord:sleep_manual( function(wakefn)done = function()done = falsewakefn()endNylonSysCore.addOneShot( 120*1000, function() if done then done() end end) -- wake up every 2minutes to check for new dayend)endif manualClose thenwv.log('debug', 'followup picklist closed manually, wait 15min')cord:sleep(15*60*1000) -- 15 minutesendelsecord:sleep(20) -- as long as no follow up items are found, check every 20s (to detect new ones)endendendNylon.cord('followup', cordfn_followup)------------------------------------------------------------------------------------------------------------------------------------Winman.create( env )picklist(' Recent Edits ',-- string.format('select ROWID,dt_modified,title from note order by dt_modified desc limit %d', env.curses.nrows-2))string.format('select ROWID,dt_modified,title from note order by dt_modified desc limit %d', (env.curses.nrows*6)))Winman.set_input( app_keymapper, app_unhandledkeys )cord_app = Nylon.cord('app', entryfn_app, env)cord_app.event.OpenRecord( recid )------------------------------------------------------------------------------------------------------------------------------------Nylon.run()
--[[--Demonstrates application of diffs to obtain prior versions of record textMaybe useful for calling from external program, ie, ruby script--]]--require 'site'local Nylon = require 'nylon.core'()--local wv = require 'nylon.debug' { name = 'pls-main' }local Sqlite = require 'sqlite'local Numword = require 'pls-numword'local JSON = require 'JSON' -- for debugginglocal db = Sqlite:new 'plstest.db'local nwid = arg[1]local recid = string.match(nwid,'^%d') and tonumber(nwid) or Numword.to_i(nwid)local record = db:selectOne('select rowid, title, detail from note where rowid=?', tonumber(recid) )if record thenif arg[2] thenlocal Diff = require 'diff_match_patch'local rev = tonumber(arg[2])local patches = db:selectMany('select content from patch where id_note=? and revision >= ? order by revision desc', recid, rev)for _, patch in ipairs(patches) dolocal gdiff = Diff.patch_fromText( patch.content )record.detail = Diff.patch_apply( gdiff, record.detail )endendprint( record.detail )end
require 'site'local Nylon = require 'nylon.core'()local wv = require 'nylon.debug' { name = 'pls-main' }local filename = '/tmp/scratch'if arg[1] thenfilename = arg[1]endrequire 'LbindPdcurses'local Buffer = require 'pls-buffer'local bbuffer = Buffer.openFile( filename )local gl_keybindings = {[3] = 'quit', -- ctrl+c[27] = 'quit', -- esc}local keyhandler-- global, mehfunction request_keys( cbfun )local old = keyhandlerlocal enabled = truekeyhandler = function(k)local handled = (enabled and cbfun(k)) or (old and old(k))wv.log('norm','keyhandled? %d=%s', k, handled and 'yes' or 'no')endreturn function()enabled = falseendendlocal function entryfn_input( cord, controller )wv.log '001 entryfn_input'Pdcurses.Static.keypad(true);wv.log '002 entryfn_input'cord:cthreaded_multi( Pdcurses.Static.cthread_getch_loop(),function( k )wv.log('norm','entryfn_input got key=%d',k)-- controller:msg{ key = k }if gl_keybindings[k] == 'quit' thenPdcurses.Static.endwin()os.exit(1)elsewv.log('norm','keyhandler[%d]=%s',k,keyhandler)if keyhandler thenkeyhandler(k)endendend )endlocal function curses_init()local screen = Pdcurses.Static.initscr()Pdcurses.Static.noecho()Pdcurses.Static.start_color()local ncols, nrows = screen:getmaxx(), screen:getmaxy()Pdcurses.Static.refresh()return { screen = screen, ncols = ncols, nrows = nrows }endlocal function WindowDim( x, y, w, h )return { x = x, y = y, w = w, h = h }endlocal function make_editor( env )local ed = require 'pls-ed'local nrows, ncols = env.curses.nrows, env.curses.ncolslocal x, y, w, h = math.floor(ncols/4), math.floor(nrows/4), math.floor(ncols/2), math.floor(nrows/2)local win = Pdcurses.Window( h, w, y, x )local wb = Pdcurses.Window(h+2,w+2,y-1,x-1)wb:stdbox_()wb:refresh()--local win = env.curses.screenlocal statuswin = Pdcurses.Window(1,w+2,y+h+1,x-1)local function winstatus( sttype, data )statuswin:mvaddstr(0,0,'[__')statuswin:addstr(data)statuswin:addstr('__]')local left = w - 6 - #data + 2statuswin:addstr(string.rep('_',left))statuswin:refresh()endlocal e = setmetatable( { w = win,report = winstatus}, { __index = env } )local cord_edit = Nylon.cord( 'edit', ed.entryfn_edit, WindowDim(0,0,w,h), bbuffer, e )endlocal e = { curses = curses_init() }local cord_input = Nylon.cord( 'input', entryfn_input, cord_keycontroller )make_editor( e )Nylon.run()
--[[* Diff Match and Patch** Copyright 2006 Google Inc.* http://code.google.com/p/google-diff-match-patch/** Based on the JavaScript implementation by Neil Fraser.* Ported to Lua by Duncan Cross.** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.--]]--[[-- Lua 5.1 and earlier requires the external BitOp library.-- This library is built-in from Lua 5.2 and later as 'bit32'.require 'bit' -- <http://bitop.luajit.org/>local band, bor, lshift= bit.band, bit.bor, bit.lshift--]]local band, bor, lshift= bit32.band, bit32.bor, bit32.lshiftlocal type, setmetatable, ipairs, select= type, setmetatable, ipairs, selectlocal unpack, tonumber, error= unpack, tonumber, errorlocal strsub, strbyte, strchar, gmatch, gsub= string.sub, string.byte, string.char, string.gmatch, string.gsublocal strmatch, strfind, strformat= string.match, string.find, string.formatlocal tinsert, tremove, tconcat= table.insert, table.remove, table.concatlocal max, min, floor, ceil, abs= math.max, math.min, math.floor, math.ceil, math.abslocal clock = os.clock-- Utility functions.local percentEncode_pattern = '[^A-Za-z0-9%-=;\',./~!@#$%&*%(%)_%+ %?]'local function percentEncode_replace(v)return strformat('%%%02X', strbyte(v))endlocal function tsplice(t, idx, deletions, ...)local insertions = select('#', ...)for i = 1, deletions dotremove(t, idx)endfor i = insertions, 1, -1 do-- do not remove parentheses around selecttinsert(t, idx, (select(i, ...)))endendlocal function strelement(str, i)return strsub(str, i, i)endlocal function indexOf(a, b, start)if (#b == 0) thenreturn nilendreturn strfind(a, b, start, true)endlocal htmlEncode_pattern = '[&<>\n]'local htmlEncode_replace = {['&'] = '&', ['<'] = '<', ['>'] = '>', ['\n'] = '¶<br>'}-- Public API Functions-- (Exported at the end of the script)local diff_main,diff_cleanupSemantic,diff_cleanupEfficiency,diff_levenshtein,diff_prettyHtmllocal match_mainlocal patch_make,patch_toText,patch_fromText,patch_apply--[[* The data structure representing a diff is an array of tuples:* {{DIFF_DELETE, 'Hello'}, {DIFF_INSERT, 'Goodbye'}, {DIFF_EQUAL, ' world.'}}* which means: delete 'Hello', add 'Goodbye' and keep ' world.'--]]local DIFF_DELETE = -1local DIFF_INSERT = 1local DIFF_EQUAL = 0-- Number of seconds to map a diff before giving up (0 for infinity).local Diff_Timeout = 1.0-- Cost of an empty edit operation in terms of edit characters.local Diff_EditCost = 4-- At what point is no match declared (0.0 = perfection, 1.0 = very loose).local Match_Threshold = 0.5-- How far to search for a match (0 = exact location, 1000+ = broad match).-- A match this many characters away from the expected location will add-- 1.0 to the score (0.0 is a perfect match).local Match_Distance = 1000-- When deleting a large block of text (over ~64 characters), how close do-- the contents have to be to match the expected contents. (0.0 = perfection,-- 1.0 = very loose). Note that Match_Threshold controls how closely the-- end points of a delete need to match.local Patch_DeleteThreshold = 0.5-- Chunk size for context length.local Patch_Margin = 4-- The number of bits in an int.local Match_MaxBits = 32function settings(new)if new thenDiff_Timeout = new.Diff_Timeout or Diff_TimeoutDiff_EditCost = new.Diff_EditCost or Diff_EditCostMatch_Threshold = new.Match_Threshold or Match_ThresholdMatch_Distance = new.Match_Distance or Match_DistancePatch_DeleteThreshold = new.Patch_DeleteThreshold or Patch_DeleteThresholdPatch_Margin = new.Patch_Margin or Patch_MarginMatch_MaxBits = new.Match_MaxBits or Match_MaxBitselsereturn {Diff_Timeout = Diff_Timeout;Diff_EditCost = Diff_EditCost;Match_Threshold = Match_Threshold;Match_Distance = Match_Distance;Patch_DeleteThreshold = Patch_DeleteThreshold;Patch_Margin = Patch_Margin;Match_MaxBits = Match_MaxBits;}endend-- ----------------------------------------------------------------------------- DIFF API-- ----------------------------------------------------------------------------- The private diff functionslocal _diff_compute,_diff_bisect,_diff_halfMatchI,_diff_halfMatch,_diff_cleanupSemanticScore,_diff_cleanupSemanticLossless,_diff_cleanupMerge,_diff_commonPrefix,_diff_commonSuffix,_diff_commonOverlap,_diff_xIndex,_diff_text1,_diff_text2,_diff_toDelta,_diff_fromDelta--[[* Find the differences between two texts. Simplifies the problem by stripping* any common prefix or suffix off the texts before diffing.* @param {string} text1 Old string to be diffed.* @param {string} text2 New string to be diffed.* @param {boolean} opt_checklines Has no effect in Lua.* @param {number} opt_deadline Optional time when the diff should be complete* by. Used internally for recursive calls. Users should set DiffTimeout* instead.* @return {Array.<Array.<number|string>>} Array of diff tuples.--]]function diff_main(text1, text2, opt_checklines, opt_deadline)-- Set a deadline by which time the diff must be complete.if opt_deadline == nil thenif Diff_Timeout <= 0 thenopt_deadline = 2 ^ 31elseopt_deadline = clock() + Diff_Timeoutendendlocal deadline = opt_deadline-- Check for null inputs.if text1 == nil or text1 == nil thenerror('Null inputs. (diff_main)')end-- Check for equality (speedup).if text1 == text2 thenif #text1 > 0 thenreturn {{DIFF_EQUAL, text1}}endreturn {}end-- LUANOTE: Due to the lack of Unicode support, Lua is incapable of-- implementing the line-mode speedup.local checklines = false-- Trim off common prefix (speedup).local commonlength = _diff_commonPrefix(text1, text2)local commonprefixif commonlength > 0 thencommonprefix = strsub(text1, 1, commonlength)text1 = strsub(text1, commonlength + 1)text2 = strsub(text2, commonlength + 1)end-- Trim off common suffix (speedup).commonlength = _diff_commonSuffix(text1, text2)local commonsuffixif commonlength > 0 thencommonsuffix = strsub(text1, -commonlength)text1 = strsub(text1, 1, -commonlength - 1)text2 = strsub(text2, 1, -commonlength - 1)end-- Compute the diff on the middle block.local diffs = _diff_compute(text1, text2, checklines, deadline)-- Restore the prefix and suffix.if commonprefix thentinsert(diffs, 1, {DIFF_EQUAL, commonprefix})endif commonsuffix thendiffs[#diffs + 1] = {DIFF_EQUAL, commonsuffix}end_diff_cleanupMerge(diffs)return diffsend--[[* Reduce the number of edits by eliminating semantically trivial equalities.* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.--]]function diff_cleanupSemantic(diffs)local changes = falselocal equalities = {} -- Stack of indices where equalities are found.local equalitiesLength = 0 -- Keeping our own length var is faster.local lastequality = nil-- Always equal to diffs[equalities[equalitiesLength]][2]local pointer = 1 -- Index of current position.-- Number of characters that changed prior to the equality.local length_insertions1 = 0local length_deletions1 = 0-- Number of characters that changed after the equality.local length_insertions2 = 0local length_deletions2 = 0while diffs[pointer] doif diffs[pointer][1] == DIFF_EQUAL then -- Equality found.equalitiesLength = equalitiesLength + 1equalities[equalitiesLength] = pointerlength_insertions1 = length_insertions2length_deletions1 = length_deletions2length_insertions2 = 0length_deletions2 = 0lastequality = diffs[pointer][2]else -- An insertion or deletion.if diffs[pointer][1] == DIFF_INSERT thenlength_insertions2 = length_insertions2 + #(diffs[pointer][2])elselength_deletions2 = length_deletions2 + #(diffs[pointer][2])end-- Eliminate an equality that is smaller or equal to the edits on both-- sides of it.if lastequalityand (#lastequality <= max(length_insertions1, length_deletions1))and (#lastequality <= max(length_insertions2, length_deletions2)) then-- Duplicate record.tinsert(diffs, equalities[equalitiesLength],{DIFF_DELETE, lastequality})-- Change second copy to insert.diffs[equalities[equalitiesLength] + 1][1] = DIFF_INSERT-- Throw away the equality we just deleted.equalitiesLength = equalitiesLength - 1-- Throw away the previous equality (it needs to be reevaluated).equalitiesLength = equalitiesLength - 1pointer = (equalitiesLength > 0) and equalities[equalitiesLength] or 0length_insertions1, length_deletions1 = 0, 0 -- Reset the counters.length_insertions2, length_deletions2 = 0, 0lastequality = nilchanges = trueendendpointer = pointer + 1end-- Normalize the diff.if changes then_diff_cleanupMerge(diffs)end_diff_cleanupSemanticLossless(diffs)-- Find any overlaps between deletions and insertions.-- e.g: <del>abcxxx</del><ins>xxxdef</ins>-- -> <del>abc</del>xxx<ins>def</ins>-- e.g: <del>xxxabc</del><ins>defxxx</ins>-- -> <ins>def</ins>xxx<del>abc</del>-- Only extract an overlap if it is as big as the edit ahead or behind it.pointer = 2while diffs[pointer] doif (diffs[pointer - 1][1] == DIFF_DELETE anddiffs[pointer][1] == DIFF_INSERT) thenlocal deletion = diffs[pointer - 1][2]local insertion = diffs[pointer][2]local overlap_length1 = _diff_commonOverlap(deletion, insertion)local overlap_length2 = _diff_commonOverlap(insertion, deletion)if (overlap_length1 >= overlap_length2) thenif (overlap_length1 >= #deletion / 2 oroverlap_length1 >= #insertion / 2) then-- Overlap found. Insert an equality and trim the surrounding edits.tinsert(diffs, pointer,{DIFF_EQUAL, strsub(insertion, 1, overlap_length1)})diffs[pointer - 1][2] =strsub(deletion, 1, #deletion - overlap_length1)diffs[pointer + 1][2] = strsub(insertion, overlap_length1 + 1)pointer = pointer + 1endelseif (overlap_length2 >= #deletion / 2 oroverlap_length2 >= #insertion / 2) then-- Reverse overlap found.-- Insert an equality and swap and trim the surrounding edits.tinsert(diffs, pointer,{DIFF_EQUAL, strsub(deletion, 1, overlap_length2)})diffs[pointer - 1] = {DIFF_INSERT,strsub(insertion, 1, #insertion - overlap_length2)}diffs[pointer + 1] = {DIFF_DELETE,strsub(deletion, overlap_length2 + 1)}pointer = pointer + 1endendpointer = pointer + 1endpointer = pointer + 1endend--[[* Reduce the number of edits by eliminating operationally trivial equalities.* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.--]]function diff_cleanupEfficiency(diffs)local changes = false-- Stack of indices where equalities are found.local equalities = {}-- Keeping our own length var is faster.local equalitiesLength = 0-- Always equal to diffs[equalities[equalitiesLength]][2]local lastequality = nil-- Index of current position.local pointer = 1-- The following four are really booleans but are stored as numbers because-- they are used at one point like this:---- (pre_ins + pre_del + post_ins + post_del) == 3---- ...i.e. checking that 3 of them are true and 1 of them is false.-- Is there an insertion operation before the last equality.local pre_ins = 0-- Is there a deletion operation before the last equality.local pre_del = 0-- Is there an insertion operation after the last equality.local post_ins = 0-- Is there a deletion operation after the last equality.local post_del = 0while diffs[pointer] doif diffs[pointer][1] == DIFF_EQUAL then -- Equality found.local diffText = diffs[pointer][2]if (#diffText < Diff_EditCost) and (post_ins == 1 or post_del == 1) then-- Candidate found.equalitiesLength = equalitiesLength + 1equalities[equalitiesLength] = pointerpre_ins, pre_del = post_ins, post_dellastequality = diffTextelse-- Not a candidate, and can never become one.equalitiesLength = 0lastequality = nilendpost_ins, post_del = 0, 0else -- An insertion or deletion.if diffs[pointer][1] == DIFF_DELETE thenpost_del = 1elsepost_ins = 1end--[[* Five types to be split:* <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>* <ins>A</ins>X<ins>C</ins><del>D</del>* <ins>A</ins><del>B</del>X<ins>C</ins>* <ins>A</del>X<ins>C</ins><del>D</del>* <ins>A</ins><del>B</del>X<del>C</del>--]]if lastequality and ((pre_ins+pre_del+post_ins+post_del == 4)or((#lastequality < Diff_EditCost / 2)and(pre_ins+pre_del+post_ins+post_del == 3))) then-- Duplicate record.tinsert(diffs, equalities[equalitiesLength],{DIFF_DELETE, lastequality})-- Change second copy to insert.diffs[equalities[equalitiesLength] + 1][1] = DIFF_INSERT-- Throw away the equality we just deleted.equalitiesLength = equalitiesLength - 1lastequality = nilif (pre_ins == 1) and (pre_del == 1) then-- No changes made which could affect previous entry, keep going.post_ins, post_del = 1, 1equalitiesLength = 0else-- Throw away the previous equality.equalitiesLength = equalitiesLength - 1pointer = (equalitiesLength > 0) and equalities[equalitiesLength] or 0post_ins, post_del = 0, 0endchanges = trueendendpointer = pointer + 1endif changes then_diff_cleanupMerge(diffs)endend--[[* Compute the Levenshtein distance; the number of inserted, deleted or* substituted characters.* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.* @return {number} Number of changes.--]]function diff_levenshtein(diffs)local levenshtein = 0local insertions, deletions = 0, 0for x, diff in ipairs(diffs) dolocal op, data = diff[1], diff[2]if (op == DIFF_INSERT) theninsertions = insertions + #dataelseif (op == DIFF_DELETE) thendeletions = deletions + #dataelseif (op == DIFF_EQUAL) then-- A deletion and an insertion is one substitution.levenshtein = levenshtein + max(insertions, deletions)insertions = 0deletions = 0endendlevenshtein = levenshtein + max(insertions, deletions)return levenshteinend--[[* Convert a diff array into a pretty HTML report.* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.* @return {string} HTML representation.--]]function diff_prettyHtml(diffs)local html = {}for x, diff in ipairs(diffs) dolocal op = diff[1] -- Operation (insert, delete, equal)local data = diff[2] -- Text of change.local text = gsub(data, htmlEncode_pattern, htmlEncode_replace)if op == DIFF_INSERT thenhtml[x] = '<ins style="background:#e6ffe6;">' .. text .. '</ins>'elseif op == DIFF_DELETE thenhtml[x] = '<del style="background:#ffe6e6;">' .. text .. '</del>'elseif op == DIFF_EQUAL thenhtml[x] = '<span>' .. text .. '</span>'endendreturn tconcat(html)end-- ----------------------------------------------------------------------------- UNOFFICIAL/PRIVATE DIFF FUNCTIONS-- -----------------------------------------------------------------------------[[* Find the differences between two texts. Assumes that the texts do not* have any common prefix or suffix.* @param {string} text1 Old string to be diffed.* @param {string} text2 New string to be diffed.* @param {boolean} checklines Has no effect in Lua.* @param {number} deadline Time when the diff should be complete by.* @return {Array.<Array.<number|string>>} Array of diff tuples.* @private--]]function _diff_compute(text1, text2, checklines, deadline)if #text1 == 0 then-- Just add some text (speedup).return {{DIFF_INSERT, text2}}endif #text2 == 0 then-- Just delete some text (speedup).return {{DIFF_DELETE, text1}}endlocal diffslocal longtext = (#text1 > #text2) and text1 or text2local shorttext = (#text1 > #text2) and text2 or text1local i = indexOf(longtext, shorttext)if i ~= nil then-- Shorter text is inside the longer text (speedup).diffs = {{DIFF_INSERT, strsub(longtext, 1, i - 1)},{DIFF_EQUAL, shorttext},{DIFF_INSERT, strsub(longtext, i + #shorttext)}}-- Swap insertions for deletions if diff is reversed.if #text1 > #text2 thendiffs[1][1], diffs[3][1] = DIFF_DELETE, DIFF_DELETEendreturn diffsendif #shorttext == 1 then-- Single character string.-- After the previous speedup, the character can't be an equality.return {{DIFF_DELETE, text1}, {DIFF_INSERT, text2}}end-- Check to see if the problem can be split in two.dolocaltext1_a, text1_b,text2_a, text2_b,mid_common = _diff_halfMatch(text1, text2)if text1_a then-- A half-match was found, sort out the return data.-- Send both pairs off for separate processing.local diffs_a = diff_main(text1_a, text2_a, checklines, deadline)local diffs_b = diff_main(text1_b, text2_b, checklines, deadline)-- Merge the results.local diffs_a_len = #diffs_adiffs = diffs_adiffs[diffs_a_len + 1] = {DIFF_EQUAL, mid_common}for i, b_diff in ipairs(diffs_b) dodiffs[diffs_a_len + 1 + i] = b_diffendreturn diffsendendreturn _diff_bisect(text1, text2, deadline)end--[[* Find the 'middle snake' of a diff, split the problem in two* and return the recursively constructed diff.* See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.* @param {string} text1 Old string to be diffed.* @param {string} text2 New string to be diffed.* @param {number} deadline Time at which to bail if not yet complete.* @return {Array.<Array.<number|string>>} Array of diff tuples.* @private--]]function _diff_bisect(text1, text2, deadline)-- Cache the text lengths to prevent multiple calls.local text1_length = #text1local text2_length = #text2local _sub, _elementlocal max_d = ceil((text1_length + text2_length) / 2)local v_offset = max_dlocal v_length = 2 * max_dlocal v1 = {}local v2 = {}-- Setting all elements to -1 is faster in Lua than mixing integers and nil.for x = 0, v_length - 1 dov1[x] = -1v2[x] = -1endv1[v_offset + 1] = 0v2[v_offset + 1] = 0local delta = text1_length - text2_length-- If the total number of characters is odd, then-- the front path will collide with the reverse path.local front = (delta % 2 ~= 0)-- Offsets for start and end of k loop.-- Prevents mapping of space beyond the grid.local k1start = 0local k1end = 0local k2start = 0local k2end = 0for d = 0, max_d - 1 do-- Bail out if deadline is reached.if clock() > deadline thenbreakend-- Walk the front path one step.for k1 = -d + k1start, d - k1end, 2 dolocal k1_offset = v_offset + k1local x1if (k1 == -d) or ((k1 ~= d) and(v1[k1_offset - 1] < v1[k1_offset + 1])) thenx1 = v1[k1_offset + 1]elsex1 = v1[k1_offset - 1] + 1endlocal y1 = x1 - k1while (x1 <= text1_length) and (y1 <= text2_length)and (strelement(text1, x1) == strelement(text2, y1)) dox1 = x1 + 1y1 = y1 + 1endv1[k1_offset] = x1if x1 > text1_length + 1 then-- Ran off the right of the graph.k1end = k1end + 2elseif y1 > text2_length + 1 then-- Ran off the bottom of the graph.k1start = k1start + 2elseif front thenlocal k2_offset = v_offset + delta - k1if k2_offset >= 0 and k2_offset < v_length and v2[k2_offset] ~= -1 then-- Mirror x2 onto top-left coordinate system.local x2 = text1_length - v2[k2_offset] + 1if x1 > x2 then-- Overlap detected.return _diff_bisectSplit(text1, text2, x1, y1, deadline)endendendend-- Walk the reverse path one step.for k2 = -d + k2start, d - k2end, 2 dolocal k2_offset = v_offset + k2local x2if (k2 == -d) or ((k2 ~= d) and(v2[k2_offset - 1] < v2[k2_offset + 1])) thenx2 = v2[k2_offset + 1]elsex2 = v2[k2_offset - 1] + 1endlocal y2 = x2 - k2while (x2 <= text1_length) and (y2 <= text2_length)and (strelement(text1, -x2) == strelement(text2, -y2)) dox2 = x2 + 1y2 = y2 + 1endv2[k2_offset] = x2if x2 > text1_length + 1 then-- Ran off the left of the graph.k2end = k2end + 2elseif y2 > text2_length + 1 then-- Ran off the top of the graph.k2start = k2start + 2elseif not front thenlocal k1_offset = v_offset + delta - k2if k1_offset >= 0 and k1_offset < v_length and v1[k1_offset] ~= -1 thenlocal x1 = v1[k1_offset]local y1 = v_offset + x1 - k1_offset-- Mirror x2 onto top-left coordinate system.x2 = text1_length - x2 + 1if x1 > x2 then-- Overlap detected.return _diff_bisectSplit(text1, text2, x1, y1, deadline)endendendendend-- Diff took too long and hit the deadline or-- number of diffs equals number of characters, no commonality at all.return {{DIFF_DELETE, text1}, {DIFF_INSERT, text2}}end--[[* Given the location of the 'middle snake', split the diff in two parts* and recurse.* @param {string} text1 Old string to be diffed.* @param {string} text2 New string to be diffed.* @param {number} x Index of split point in text1.* @param {number} y Index of split point in text2.* @param {number} deadline Time at which to bail if not yet complete.* @return {Array.<Array.<number|string>>} Array of diff tuples.* @private--]]function _diff_bisectSplit(text1, text2, x, y, deadline)local text1a = strsub(text1, 1, x - 1)local text2a = strsub(text2, 1, y - 1)local text1b = strsub(text1, x)local text2b = strsub(text2, y)-- Compute both diffs serially.local diffs = diff_main(text1a, text2a, false, deadline)local diffsb = diff_main(text1b, text2b, false, deadline)local diffs_len = #diffsfor i, v in ipairs(diffsb) dodiffs[diffs_len + i] = vendreturn diffsend--[[* Determine the common prefix of two strings.* @param {string} text1 First string.* @param {string} text2 Second string.* @return {number} The number of characters common to the start of each* string.--]]function _diff_commonPrefix(text1, text2)-- Quick check for common null cases.if (#text1 == 0) or (#text2 == 0) or (strbyte(text1, 1) ~= strbyte(text2, 1))thenreturn 0end-- Binary search.-- Performance analysis: http://neil.fraser.name/news/2007/10/09/local pointermin = 1local pointermax = min(#text1, #text2)local pointermid = pointermaxlocal pointerstart = 1while (pointermin < pointermid) doif (strsub(text1, pointerstart, pointermid)== strsub(text2, pointerstart, pointermid)) thenpointermin = pointermidpointerstart = pointerminelsepointermax = pointermidendpointermid = floor(pointermin + (pointermax - pointermin) / 2)endreturn pointermidend--[[* Determine the common suffix of two strings.* @param {string} text1 First string.* @param {string} text2 Second string.* @return {number} The number of characters common to the end of each string.--]]function _diff_commonSuffix(text1, text2)-- Quick check for common null cases.if (#text1 == 0) or (#text2 == 0)or (strbyte(text1, -1) ~= strbyte(text2, -1)) thenreturn 0end-- Binary search.-- Performance analysis: http://neil.fraser.name/news/2007/10/09/local pointermin = 1local pointermax = min(#text1, #text2)local pointermid = pointermaxlocal pointerend = 1while (pointermin < pointermid) doif (strsub(text1, -pointermid, -pointerend)== strsub(text2, -pointermid, -pointerend)) thenpointermin = pointermidpointerend = pointerminelsepointermax = pointermidendpointermid = floor(pointermin + (pointermax - pointermin) / 2)endreturn pointermidend--[[* Determine if the suffix of one string is the prefix of another.* @param {string} text1 First string.* @param {string} text2 Second string.* @return {number} The number of characters common to the end of the first* string and the start of the second string.* @private--]]function _diff_commonOverlap(text1, text2)-- Cache the text lengths to prevent multiple calls.local text1_length = #text1local text2_length = #text2-- Eliminate the null case.if text1_length == 0 or text2_length == 0 thenreturn 0end-- Truncate the longer string.if text1_length > text2_length thentext1 = strsub(text1, text1_length - text2_length + 1)elseif text1_length < text2_length thentext2 = strsub(text2, 1, text1_length)endlocal text_length = min(text1_length, text2_length)-- Quick check for the worst case.if text1 == text2 thenreturn text_lengthend-- Start by looking for a single character match-- and increase length until no match is found.-- Performance analysis: http://neil.fraser.name/news/2010/11/04/local best = 0local length = 1while true dolocal pattern = strsub(text1, text_length - length + 1)local found = strfind(text2, pattern, 1, true)if found == nil thenreturn bestendlength = length + found - 1if found == 1 or strsub(text1, text_length - length + 1) ==strsub(text2, 1, length) thenbest = lengthlength = length + 1endendend--[[* Does a substring of shorttext exist within longtext such that the substring* is at least half the length of longtext?* This speedup can produce non-minimal diffs.* Closure, but does not reference any external variables.* @param {string} longtext Longer string.* @param {string} shorttext Shorter string.* @param {number} i Start index of quarter length substring within longtext.* @return {?Array.<string>} Five element Array, containing the prefix of* longtext, the suffix of longtext, the prefix of shorttext, the suffix* of shorttext and the common middle. Or nil if there was no match.* @private--]]function _diff_halfMatchI(longtext, shorttext, i)-- Start with a 1/4 length substring at position i as a seed.local seed = strsub(longtext, i, i + floor(#longtext / 4))local j = 0 -- LUANOTE: do not change to 1, was originally -1local best_common = ''local best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_bwhile true doj = indexOf(shorttext, seed, j + 1)if (j == nil) thenbreakendlocal prefixLength = _diff_commonPrefix(strsub(longtext, i),strsub(shorttext, j))local suffixLength = _diff_commonSuffix(strsub(longtext, 1, i - 1),strsub(shorttext, 1, j - 1))if #best_common < suffixLength + prefixLength thenbest_common = strsub(shorttext, j - suffixLength, j - 1).. strsub(shorttext, j, j + prefixLength - 1)best_longtext_a = strsub(longtext, 1, i - suffixLength - 1)best_longtext_b = strsub(longtext, i + prefixLength)best_shorttext_a = strsub(shorttext, 1, j - suffixLength - 1)best_shorttext_b = strsub(shorttext, j + prefixLength)endendif #best_common * 2 >= #longtext thenreturn {best_longtext_a, best_longtext_b,best_shorttext_a, best_shorttext_b, best_common}elsereturn nilendend--[[* Do the two texts share a substring which is at least half the length of the* longer text?* @param {string} text1 First string.* @param {string} text2 Second string.* @return {?Array.<string>} Five element Array, containing the prefix of* text1, the suffix of text1, the prefix of text2, the suffix of* text2 and the common middle. Or nil if there was no match.* @private--]]function _diff_halfMatch(text1, text2)if Diff_Timeout <= 0 then-- Don't risk returning a non-optimal diff if we have unlimited time.return nilendlocal longtext = (#text1 > #text2) and text1 or text2local shorttext = (#text1 > #text2) and text2 or text1if (#longtext < 4) or (#shorttext * 2 < #longtext) thenreturn nil -- Pointless.end-- First check if the second quarter is the seed for a half-match.local hm1 = _diff_halfMatchI(longtext, shorttext, ceil(#longtext / 4))-- Check again based on the third quarter.local hm2 = _diff_halfMatchI(longtext, shorttext, ceil(#longtext / 2))local hmif not hm1 and not hm2 thenreturn nilelseif not hm2 thenhm = hm1elseif not hm1 thenhm = hm2else-- Both matched. Select the longest.hm = (#hm1[5] > #hm2[5]) and hm1 or hm2end-- A half-match was found, sort out the return data.local text1_a, text1_b, text2_a, text2_bif (#text1 > #text2) thentext1_a, text1_b = hm[1], hm[2]text2_a, text2_b = hm[3], hm[4]elsetext2_a, text2_b = hm[1], hm[2]text1_a, text1_b = hm[3], hm[4]endlocal mid_common = hm[5]return text1_a, text1_b, text2_a, text2_b, mid_commonend--[[* Given two strings, compute a score representing whether the internal* boundary falls on logical boundaries.* Scores range from 6 (best) to 0 (worst).* @param {string} one First string.* @param {string} two Second string.* @return {number} The score.* @private--]]function _diff_cleanupSemanticScore(one, two)if (#one == 0) or (#two == 0) then-- Edges are the best.return 6end-- Each port of this function behaves slightly differently due to-- subtle differences in each language's definition of things like-- 'whitespace'. Since this function's purpose is largely cosmetic,-- the choice has been made to use each language's native features-- rather than force total conformity.local char1 = strsub(one, -1)local char2 = strsub(two, 1, 1)local nonAlphaNumeric1 = strmatch(char1, '%W')local nonAlphaNumeric2 = strmatch(char2, '%W')local whitespace1 = nonAlphaNumeric1 and strmatch(char1, '%s')local whitespace2 = nonAlphaNumeric2 and strmatch(char2, '%s')local lineBreak1 = whitespace1 and strmatch(char1, '%c')local lineBreak2 = whitespace2 and strmatch(char2, '%c')local blankLine1 = lineBreak1 and strmatch(one, '\n\r?\n$')local blankLine2 = lineBreak2 and strmatch(two, '^\r?\n\r?\n')if blankLine1 or blankLine2 then-- Five points for blank lines.return 5elseif lineBreak1 or lineBreak2 then-- Four points for line breaks.return 4elseif nonAlphaNumeric1 and not whitespace1 and whitespace2 then-- Three points for end of sentences.return 3elseif whitespace1 or whitespace2 then-- Two points for whitespace.return 2elseif nonAlphaNumeric1 or nonAlphaNumeric2 then-- One point for non-alphanumeric.return 1endreturn 0end--[[* Look for single edits surrounded on both sides by equalities* which can be shifted sideways to align the edit to a word boundary.* e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came.* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.--]]function _diff_cleanupSemanticLossless(diffs)local pointer = 2-- Intentionally ignore the first and last element (don't need checking).while diffs[pointer + 1] dolocal prevDiff, nextDiff = diffs[pointer - 1], diffs[pointer + 1]if (prevDiff[1] == DIFF_EQUAL) and (nextDiff[1] == DIFF_EQUAL) then-- This is a single edit surrounded by equalities.local diff = diffs[pointer]local equality1 = prevDiff[2]local edit = diff[2]local equality2 = nextDiff[2]-- First, shift the edit as far left as possible.local commonOffset = _diff_commonSuffix(equality1, edit)if commonOffset > 0 thenlocal commonString = strsub(edit, -commonOffset)equality1 = strsub(equality1, 1, -commonOffset - 1)edit = commonString .. strsub(edit, 1, -commonOffset - 1)equality2 = commonString .. equality2end-- Second, step character by character right, looking for the best fit.local bestEquality1 = equality1local bestEdit = editlocal bestEquality2 = equality2local bestScore = _diff_cleanupSemanticScore(equality1, edit)+ _diff_cleanupSemanticScore(edit, equality2)while strbyte(edit, 1) == strbyte(equality2, 1) doequality1 = equality1 .. strsub(edit, 1, 1)edit = strsub(edit, 2) .. strsub(equality2, 1, 1)equality2 = strsub(equality2, 2)local score = _diff_cleanupSemanticScore(equality1, edit)+ _diff_cleanupSemanticScore(edit, equality2)-- The >= encourages trailing rather than leading whitespace on edits.if score >= bestScore thenbestScore = scorebestEquality1 = equality1bestEdit = editbestEquality2 = equality2endendif prevDiff[2] ~= bestEquality1 then-- We have an improvement, save it back to the diff.if #bestEquality1 > 0 thendiffs[pointer - 1][2] = bestEquality1elsetremove(diffs, pointer - 1)pointer = pointer - 1enddiffs[pointer][2] = bestEditif #bestEquality2 > 0 thendiffs[pointer + 1][2] = bestEquality2elsetremove(diffs, pointer + 1, 1)pointer = pointer - 1endendendpointer = pointer + 1endend--[[* Reorder and merge like edit sections. Merge equalities.* Any edit section can move as long as it doesn't cross an equality.* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.--]]function _diff_cleanupMerge(diffs)diffs[#diffs + 1] = {DIFF_EQUAL, ''} -- Add a dummy entry at the end.local pointer = 1local count_delete, count_insert = 0, 0local text_delete, text_insert = '', ''local commonlengthwhile diffs[pointer] dolocal diff_type = diffs[pointer][1]if diff_type == DIFF_INSERT thencount_insert = count_insert + 1text_insert = text_insert .. diffs[pointer][2]pointer = pointer + 1elseif diff_type == DIFF_DELETE thencount_delete = count_delete + 1text_delete = text_delete .. diffs[pointer][2]pointer = pointer + 1elseif diff_type == DIFF_EQUAL then-- Upon reaching an equality, check for prior redundancies.if count_delete + count_insert > 1 thenif (count_delete > 0) and (count_insert > 0) then-- Factor out any common prefixies.commonlength = _diff_commonPrefix(text_insert, text_delete)if commonlength > 0 thenlocal back_pointer = pointer - count_delete - count_insertif (back_pointer > 1) and (diffs[back_pointer - 1][1] == DIFF_EQUAL)thendiffs[back_pointer - 1][2] = diffs[back_pointer - 1][2].. strsub(text_insert, 1, commonlength)elsetinsert(diffs, 1,{DIFF_EQUAL, strsub(text_insert, 1, commonlength)})pointer = pointer + 1endtext_insert = strsub(text_insert, commonlength + 1)text_delete = strsub(text_delete, commonlength + 1)end-- Factor out any common suffixies.commonlength = _diff_commonSuffix(text_insert, text_delete)if commonlength ~= 0 thendiffs[pointer][2] =strsub(text_insert, -commonlength) .. diffs[pointer][2]text_insert = strsub(text_insert, 1, -commonlength - 1)text_delete = strsub(text_delete, 1, -commonlength - 1)endend-- Delete the offending records and add the merged ones.if count_delete == 0 thentsplice(diffs, pointer - count_insert,count_insert, {DIFF_INSERT, text_insert})elseif count_insert == 0 thentsplice(diffs, pointer - count_delete,count_delete, {DIFF_DELETE, text_delete})elsetsplice(diffs, pointer - count_delete - count_insert,count_delete + count_insert,{DIFF_DELETE, text_delete}, {DIFF_INSERT, text_insert})endpointer = pointer - count_delete - count_insert+ (count_delete>0 and 1 or 0) + (count_insert>0 and 1 or 0) + 1elseif (pointer > 1) and (diffs[pointer - 1][1] == DIFF_EQUAL) then-- Merge this equality with the previous one.diffs[pointer - 1][2] = diffs[pointer - 1][2] .. diffs[pointer][2]tremove(diffs, pointer)elsepointer = pointer + 1endcount_insert, count_delete = 0, 0text_delete, text_insert = '', ''endendif diffs[#diffs][2] == '' thendiffs[#diffs] = nil -- Remove the dummy entry at the end.end-- Second pass: look for single edits surrounded on both sides by equalities-- which can be shifted sideways to eliminate an equality.-- e.g: A<ins>BA</ins>C -> <ins>AB</ins>AClocal changes = falsepointer = 2-- Intentionally ignore the first and last element (don't need checking).while pointer < #diffs dolocal prevDiff, nextDiff = diffs[pointer - 1], diffs[pointer + 1]if (prevDiff[1] == DIFF_EQUAL) and (nextDiff[1] == DIFF_EQUAL) then-- This is a single edit surrounded by equalities.local diff = diffs[pointer]local currentText = diff[2]local prevText = prevDiff[2]local nextText = nextDiff[2]if strsub(currentText, -#prevText) == prevText then-- Shift the edit over the previous equality.diff[2] = prevText .. strsub(currentText, 1, -#prevText - 1)nextDiff[2] = prevText .. nextDiff[2]tremove(diffs, pointer - 1)changes = trueelseif strsub(currentText, 1, #nextText) == nextText then-- Shift the edit over the next equality.prevDiff[2] = prevText .. nextTextdiff[2] = strsub(currentText, #nextText + 1) .. nextTexttremove(diffs, pointer + 1)changes = trueendendpointer = pointer + 1end-- If shifts were made, the diff needs reordering and another shift sweep.if changes then-- LUANOTE: no return value, but necessary to use 'return' to get-- tail calls.return _diff_cleanupMerge(diffs)endend--[[* loc is a location in text1, compute and return the equivalent location in* text2.* e.g. 'The cat' vs 'The big cat', 1->1, 5->8* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.* @param {number} loc Location within text1.* @return {number} Location within text2.--]]function _diff_xIndex(diffs, loc)local chars1 = 1local chars2 = 1local last_chars1 = 1local last_chars2 = 1local xfor _x, diff in ipairs(diffs) dox = _xif diff[1] ~= DIFF_INSERT then -- Equality or deletion.chars1 = chars1 + #diff[2]endif diff[1] ~= DIFF_DELETE then -- Equality or insertion.chars2 = chars2 + #diff[2]endif chars1 > loc then -- Overshot the location.breakendlast_chars1 = chars1last_chars2 = chars2end-- Was the location deleted?if diffs[x + 1] and (diffs[x][1] == DIFF_DELETE) thenreturn last_chars2end-- Add the remaining character length.return last_chars2 + (loc - last_chars1)end--[[* Compute and return the source text (all equalities and deletions).* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.* @return {string} Source text.--]]function _diff_text1(diffs)local text = {}for x, diff in ipairs(diffs) doif diff[1] ~= DIFF_INSERT thentext[#text + 1] = diff[2]endendreturn tconcat(text)end--[[* Compute and return the destination text (all equalities and insertions).* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.* @return {string} Destination text.--]]function _diff_text2(diffs)local text = {}for x, diff in ipairs(diffs) doif diff[1] ~= DIFF_DELETE thentext[#text + 1] = diff[2]endendreturn tconcat(text)end--[[* Crush the diff into an encoded string which describes the operations* required to transform text1 into text2.* E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'.* Operations are tab-separated. Inserted text is escaped using %xx notation.* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.* @return {string} Delta text.--]]function _diff_toDelta(diffs)local text = {}for x, diff in ipairs(diffs) dolocal op, data = diff[1], diff[2]if op == DIFF_INSERT thentext[x] = '+' .. gsub(data, percentEncode_pattern, percentEncode_replace)elseif op == DIFF_DELETE thentext[x] = '-' .. #dataelseif op == DIFF_EQUAL thentext[x] = '=' .. #dataendendreturn tconcat(text, '\t')end--[[* Given the original text1, and an encoded string which describes the* operations required to transform text1 into text2, compute the full diff.* @param {string} text1 Source string for the diff.* @param {string} delta Delta text.* @return {Array.<Array.<number|string>>} Array of diff tuples.* @throws {Errorend If invalid input.--]]function _diff_fromDelta(text1, delta)local diffs = {}local diffsLength = 0 -- Keeping our own length var is fasterlocal pointer = 1 -- Cursor in text1for token in gmatch(delta, '[^\t]+') do-- Each token begins with a one character parameter which specifies the-- operation of this token (delete, insert, equality).local tokenchar, param = strsub(token, 1, 1), strsub(token, 2)if (tokenchar == '+') thenlocal invalidDecode = falselocal decoded = gsub(param, '%%(.?.?)',function(c)local n = tonumber(c, 16)if (#c ~= 2) or (n == nil) theninvalidDecode = truereturn ''endreturn strchar(n)end)if invalidDecode then-- Malformed URI sequence.error('Illegal escape in _diff_fromDelta: ' .. param)enddiffsLength = diffsLength + 1diffs[diffsLength] = {DIFF_INSERT, decoded}elseif (tokenchar == '-') or (tokenchar == '=') thenlocal n = tonumber(param)if (n == nil) or (n < 0) thenerror('Invalid number in _diff_fromDelta: ' .. param)endlocal text = strsub(text1, pointer, pointer + n - 1)pointer = pointer + nif (tokenchar == '=') thendiffsLength = diffsLength + 1diffs[diffsLength] = {DIFF_EQUAL, text}elsediffsLength = diffsLength + 1diffs[diffsLength] = {DIFF_DELETE, text}endelseerror('Invalid diff operation in _diff_fromDelta: ' .. token)endendif (pointer ~= #text1 + 1) thenerror('Delta length (' .. (pointer - 1).. ') does not equal source text length (' .. #text1 .. ').')endreturn diffsend-- ----------------------------------------------------------------------------- MATCH API-- ---------------------------------------------------------------------------local _match_bitap, _match_alphabet--[[* Locate the best instance of 'pattern' in 'text' near 'loc'.* @param {string} text The text to search.* @param {string} pattern The pattern to search for.* @param {number} loc The location to search around.* @return {number} Best match index or -1.--]]function match_main(text, pattern, loc)-- Check for null inputs.if text == nil or pattern == nil or loc == nil thenerror('Null inputs. (match_main)')endif text == pattern then-- Shortcut (potentially not guaranteed by the algorithm)return 1elseif #text == 0 then-- Nothing to match.return -1endloc = max(1, min(loc, #text))if strsub(text, loc, loc + #pattern - 1) == pattern then-- Perfect match at the perfect spot! (Includes case of null pattern)return locelse-- Do a fuzzy compare.return _match_bitap(text, pattern, loc)endend-- ----------------------------------------------------------------------------- UNOFFICIAL/PRIVATE MATCH FUNCTIONS-- -----------------------------------------------------------------------------[[* Initialise the alphabet for the Bitap algorithm.* @param {string} pattern The text to encode.* @return {Object} Hash of character locations.* @private--]]function _match_alphabet(pattern)local s = {}local i = 0for c in gmatch(pattern, '.') dos[c] = bor(s[c] or 0, lshift(1, #pattern - i - 1))i = i + 1endreturn send--[[* Locate the best instance of 'pattern' in 'text' near 'loc' using the* Bitap algorithm.* @param {string} text The text to search.* @param {string} pattern The pattern to search for.* @param {number} loc The location to search around.* @return {number} Best match index or -1.* @private--]]function _match_bitap(text, pattern, loc)if #pattern > Match_MaxBits thenerror('Pattern too long.')end-- Initialise the alphabet.local s = _match_alphabet(pattern)--[[* Compute and return the score for a match with e errors and x location.* Accesses loc and pattern through being a closure.* @param {number} e Number of errors in match.* @param {number} x Location of match.* @return {number} Overall score for match (0.0 = good, 1.0 = bad).* @private--]]local function _match_bitapScore(e, x)local accuracy = e / #patternlocal proximity = abs(loc - x)if (Match_Distance == 0) then-- Dodge divide by zero error.return (proximity == 0) and 1 or accuracyendreturn accuracy + (proximity / Match_Distance)end-- Highest score beyond which we give up.local score_threshold = Match_Threshold-- Is there a nearby exact match? (speedup)local best_loc = indexOf(text, pattern, loc)if best_loc thenscore_threshold = min(_match_bitapScore(0, best_loc), score_threshold)-- LUANOTE: Ideally we'd also check from the other direction, but Lua-- doesn't have an efficent lastIndexOf function.end-- Initialise the bit arrays.local matchmask = lshift(1, #pattern - 1)best_loc = -1local bin_min, bin_midlocal bin_max = #pattern + #textlocal last_rdfor d = 0, #pattern - 1, 1 do-- Scan for the best match; each iteration allows for one more error.-- Run a binary search to determine how far from 'loc' we can stray at this-- error level.bin_min = 0bin_mid = bin_maxwhile (bin_min < bin_mid) doif (_match_bitapScore(d, loc + bin_mid) <= score_threshold) thenbin_min = bin_midelsebin_max = bin_midendbin_mid = floor(bin_min + (bin_max - bin_min) / 2)end-- Use the result from this iteration as the maximum for the next.bin_max = bin_midlocal start = max(1, loc - bin_mid + 1)local finish = min(loc + bin_mid, #text) + #patternlocal rd = {}for j = start, finish dord[j] = 0endrd[finish + 1] = lshift(1, d) - 1for j = finish, start, -1 dolocal charMatch = s[strsub(text, j - 1, j - 1)] or 0if (d == 0) then -- First pass: exact match.rd[j] = band(bor((rd[j + 1] * 2), 1), charMatch)else-- Subsequent passes: fuzzy match.-- Functions instead of operators make this hella messy.rd[j] = bor(band(bor(lshift(rd[j + 1], 1),1),charMatch),bor(bor(lshift(bor(last_rd[j + 1], last_rd[j]), 1),1),last_rd[j + 1]))endif (band(rd[j], matchmask) ~= 0) thenlocal score = _match_bitapScore(d, j - 1)-- This match will almost certainly be better than any existing match.-- But check anyway.if (score <= score_threshold) then-- Told you so.score_threshold = scorebest_loc = j - 1if (best_loc > loc) then-- When passing loc, don't exceed our current distance from loc.start = max(1, loc * 2 - best_loc)else-- Already passed loc, downhill from here on in.breakendendendend-- No hope for a (better) match at greater error levels.if (_match_bitapScore(d + 1, loc) > score_threshold) thenbreakendlast_rd = rdendreturn best_locend-- ------------------------------------------------------------------------------- PATCH API-- -----------------------------------------------------------------------------local _patch_addContext,_patch_deepCopy,_patch_addPadding,_patch_splitMax,_patch_appendText,_new_patch_obj--[[* Compute a list of patches to turn text1 into text2.* Use diffs if provided, otherwise compute it ourselves.* There are four ways to call this function, depending on what data is* available to the caller:* Method 1:* a = text1, b = text2* Method 2:* a = diffs* Method 3 (optimal):* a = text1, b = diffs* Method 4 (deprecated, use method 3):* a = text1, b = text2, c = diffs** @param {string|Array.<Array.<number|string>>} a text1 (methods 1,3,4) or* Array of diff tuples for text1 to text2 (method 2).* @param {string|Array.<Array.<number|string>>} opt_b text2 (methods 1,4) or* Array of diff tuples for text1 to text2 (method 3) or undefined (method 2).* @param {string|Array.<Array.<number|string>>} opt_c Array of diff tuples for* text1 to text2 (method 4) or undefined (methods 1,2,3).* @return {Array.<_new_patch_obj>} Array of patch objects.--]]function patch_make(a, opt_b, opt_c)local text1, diffslocal type_a, type_b, type_c = type(a), type(opt_b), type(opt_c)if (type_a == 'string') and (type_b == 'string') and (type_c == 'nil') then-- Method 1: text1, text2-- Compute diffs from text1 and text2.text1 = adiffs = diff_main(text1, opt_b, true)if (#diffs > 2) thendiff_cleanupSemantic(diffs)diff_cleanupEfficiency(diffs)endelseif (type_a == 'table') and (type_b == 'nil') and (type_c == 'nil') then-- Method 2: diffs-- Compute text1 from diffs.diffs = atext1 = _diff_text1(diffs)elseif (type_a == 'string') and (type_b == 'table') and (type_c == 'nil') then-- Method 3: text1, diffstext1 = adiffs = opt_belseif (type_a == 'string') and (type_b == 'string') and (type_c == 'table')then-- Method 4: text1, text2, diffs-- text2 is not used.text1 = adiffs = opt_celseerror('Unknown call format to patch_make.')endif (diffs[1] == nil) thenreturn {} -- Get rid of the null case.endlocal patches = {}local patch = _new_patch_obj()local patchDiffLength = 0 -- Keeping our own length var is faster.local char_count1 = 0 -- Number of characters into the text1 string.local char_count2 = 0 -- Number of characters into the text2 string.-- Start with text1 (prepatch_text) and apply the diffs until we arrive at-- text2 (postpatch_text). We recreate the patches one by one to determine-- context info.local prepatch_text, postpatch_text = text1, text1for x, diff in ipairs(diffs) dolocal diff_type, diff_text = diff[1], diff[2]if (patchDiffLength == 0) and (diff_type ~= DIFF_EQUAL) then-- A new patch starts here.patch.start1 = char_count1 + 1patch.start2 = char_count2 + 1endif (diff_type == DIFF_INSERT) thenpatchDiffLength = patchDiffLength + 1patch.diffs[patchDiffLength] = diffpatch.length2 = patch.length2 + #diff_textpostpatch_text = strsub(postpatch_text, 1, char_count2).. diff_text .. strsub(postpatch_text, char_count2 + 1)elseif (diff_type == DIFF_DELETE) thenpatch.length1 = patch.length1 + #diff_textpatchDiffLength = patchDiffLength + 1patch.diffs[patchDiffLength] = diffpostpatch_text = strsub(postpatch_text, 1, char_count2).. strsub(postpatch_text, char_count2 + #diff_text + 1)elseif (diff_type == DIFF_EQUAL) thenif (#diff_text <= Patch_Margin * 2)and (patchDiffLength ~= 0) and (#diffs ~= x) then-- Small equality inside a patch.patchDiffLength = patchDiffLength + 1patch.diffs[patchDiffLength] = diffpatch.length1 = patch.length1 + #diff_textpatch.length2 = patch.length2 + #diff_textelseif (#diff_text >= Patch_Margin * 2) then-- Time for a new patch.if (patchDiffLength ~= 0) then_patch_addContext(patch, prepatch_text)patches[#patches + 1] = patchpatch = _new_patch_obj()patchDiffLength = 0-- Unlike Unidiff, our patch lists have a rolling context.-- http://code.google.com/p/google-diff-match-patch/wiki/Unidiff-- Update prepatch text & pos to reflect the application of the-- just completed patch.prepatch_text = postpatch_textchar_count1 = char_count2endendend-- Update the current character count.if (diff_type ~= DIFF_INSERT) thenchar_count1 = char_count1 + #diff_textendif (diff_type ~= DIFF_DELETE) thenchar_count2 = char_count2 + #diff_textendend-- Pick up the leftover patch if not empty.if (patchDiffLength > 0) then_patch_addContext(patch, prepatch_text)patches[#patches + 1] = patchendreturn patchesend--[[* Merge a set of patches onto the text. Return a patched text, as well* as a list of true/false values indicating which patches were applied.* @param {Array.<_new_patch_obj>} patches Array of patch objects.* @param {string} text Old text.* @return {Array.<string|Array.<boolean>>} Two return values, the* new text and an array of boolean values.--]]function patch_apply(patches, text)if patches[1] == nil thenreturn text, {}end-- Deep copy the patches so that no changes are made to originals.patches = _patch_deepCopy(patches)local nullPadding = _patch_addPadding(patches)text = nullPadding .. text .. nullPadding_patch_splitMax(patches)-- delta keeps track of the offset between the expected and actual location-- of the previous patch. If there are patches expected at positions 10 and-- 20, but the first patch was found at 12, delta is 2 and the second patch-- has an effective expected position of 22.local delta = 0local results = {}for x, patch in ipairs(patches) dolocal expected_loc = patch.start2 + deltalocal text1 = _diff_text1(patch.diffs)local start_loclocal end_loc = -1if #text1 > Match_MaxBits then-- _patch_splitMax will only provide an oversized pattern in-- the case of a monster delete.start_loc = match_main(text,strsub(text1, 1, Match_MaxBits), expected_loc)if start_loc ~= -1 thenend_loc = match_main(text, strsub(text1, -Match_MaxBits),expected_loc + #text1 - Match_MaxBits)if end_loc == -1 or start_loc >= end_loc then-- Can't find valid trailing context. Drop this patch.start_loc = -1endendelsestart_loc = match_main(text, text1, expected_loc)endif start_loc == -1 then-- No match found. :(results[x] = false-- Subtract the delta for this failed patch from subsequent patches.delta = delta - patch.length2 - patch.length1else-- Found a match. :)results[x] = truedelta = start_loc - expected_loclocal text2if end_loc == -1 thentext2 = strsub(text, start_loc, start_loc + #text1 - 1)elsetext2 = strsub(text, start_loc, end_loc + Match_MaxBits - 1)endif text1 == text2 then-- Perfect match, just shove the replacement text in.text = strsub(text, 1, start_loc - 1) .. _diff_text2(patch.diffs).. strsub(text, start_loc + #text1)else-- Imperfect match. Run a diff to get a framework of equivalent-- indices.local diffs = diff_main(text1, text2, false)if (#text1 > Match_MaxBits)and (diff_levenshtein(diffs) / #text1 > Patch_DeleteThreshold) then-- The end points match, but the content is unacceptably bad.results[x] = falseelse_diff_cleanupSemanticLossless(diffs)local index1 = 1local index2for y, mod in ipairs(patch.diffs) doif mod[1] ~= DIFF_EQUAL thenindex2 = _diff_xIndex(diffs, index1)endif mod[1] == DIFF_INSERT thentext = strsub(text, 1, start_loc + index2 - 2).. mod[2] .. strsub(text, start_loc + index2 - 1)elseif mod[1] == DIFF_DELETE thentext = strsub(text, 1, start_loc + index2 - 2) .. strsub(text,start_loc + _diff_xIndex(diffs, index1 + #mod[2] - 1))endif mod[1] ~= DIFF_DELETE thenindex1 = index1 + #mod[2]endendendendendend-- Strip the padding off.text = strsub(text, #nullPadding + 1, -#nullPadding - 1)return text, resultsend--[[* Take a list of patches and return a textual representation.* @param {Array.<_new_patch_obj>} patches Array of patch objects.* @return {string} Text representation of patches.--]]function patch_toText(patches)local text = {}for x, patch in ipairs(patches) do_patch_appendText(patch, text)endreturn tconcat(text)end--[[* Parse a textual representation of patches and return a list of patch objects.* @param {string} textline Text representation of patches.* @return {Array.<_new_patch_obj>} Array of patch objects.* @throws {Error} If invalid input.--]]function patch_fromText(textline)local patches = {}if (#textline == 0) thenreturn patchesendlocal text = {}for line in gmatch(textline, '([^\n]*)') dotext[#text + 1] = lineendlocal textPointer = 1while (textPointer <= #text) dolocal start1, length1, start2, length2= strmatch(text[textPointer], '^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@$')if (start1 == nil) thenerror('Invalid patch string: "' .. text[textPointer] .. '"')endlocal patch = _new_patch_obj()patches[#patches + 1] = patchstart1 = tonumber(start1)length1 = tonumber(length1) or 1if (length1 == 0) thenstart1 = start1 + 1endpatch.start1 = start1patch.length1 = length1start2 = tonumber(start2)length2 = tonumber(length2) or 1if (length2 == 0) thenstart2 = start2 + 1endpatch.start2 = start2patch.length2 = length2textPointer = textPointer + 1while true dolocal line = text[textPointer]if (line == nil) thenbreakendlocal sign; sign, line = strsub(line, 1, 1), strsub(line, 2)local invalidDecode = falselocal decoded = gsub(line, '%%(.?.?)',function(c)local n = tonumber(c, 16)if (#c ~= 2) or (n == nil) theninvalidDecode = truereturn ''endreturn strchar(n)end)if invalidDecode then-- Malformed URI sequence.error('Illegal escape in patch_fromText: ' .. line)endline = decodedif (sign == '-') then-- Deletion.patch.diffs[#patch.diffs + 1] = {DIFF_DELETE, line}elseif (sign == '+') then-- Insertion.patch.diffs[#patch.diffs + 1] = {DIFF_INSERT, line}elseif (sign == ' ') then-- Minor equality.patch.diffs[#patch.diffs + 1] = {DIFF_EQUAL, line}elseif (sign == '@') then-- Start of next patch.breakelseif (sign == '') then-- Blank line? Whatever.else-- WTF?error('Invalid patch mode "' .. sign .. '" in: ' .. line)endtextPointer = textPointer + 1endendreturn patchesend-- ----------------------------------------------------------------------------- UNOFFICIAL/PRIVATE PATCH FUNCTIONS-- ---------------------------------------------------------------------------local patch_meta = {__tostring = function(patch)local buf = {}_patch_appendText(patch, buf)return tconcat(buf)end}--[[* Class representing one patch operation.* @constructor--]]function _new_patch_obj()return setmetatable({--[[ @type {Array.<Array.<number|string>>} ]]diffs = {};--[[ @type {?number} ]]start1 = 1; -- nil;--[[ @type {?number} ]]start2 = 1; -- nil;--[[ @type {number} ]]length1 = 0;--[[ @type {number} ]]length2 = 0;}, patch_meta)end--[[* Increase the context until it is unique,* but don't let the pattern expand beyond Match_MaxBits.* @param {_new_patch_obj} patch The patch to grow.* @param {string} text Source text.* @private--]]function _patch_addContext(patch, text)if (#text == 0) thenreturnendlocal pattern = strsub(text, patch.start2, patch.start2 + patch.length1 - 1)local padding = 0-- LUANOTE: Lua's lack of a lastIndexOf function results in slightly-- different logic here than in other language ports.-- Look for the first two matches of pattern in text. If two are found,-- increase the pattern length.local firstMatch = indexOf(text, pattern)local secondMatch = nilif (firstMatch ~= nil) thensecondMatch = indexOf(text, pattern, firstMatch + 1)endwhile (#pattern == 0 or secondMatch ~= nil)and (#pattern < Match_MaxBits - Patch_Margin - Patch_Margin) dopadding = padding + Patch_Marginpattern = strsub(text, max(1, patch.start2 - padding),patch.start2 + patch.length1 - 1 + padding)firstMatch = indexOf(text, pattern)if (firstMatch ~= nil) thensecondMatch = indexOf(text, pattern, firstMatch + 1)elsesecondMatch = nilendend-- Add one chunk for good luck.padding = padding + Patch_Margin-- Add the prefix.local prefix = strsub(text, max(1, patch.start2 - padding), patch.start2 - 1)if (#prefix > 0) thentinsert(patch.diffs, 1, {DIFF_EQUAL, prefix})end-- Add the suffix.local suffix = strsub(text, patch.start2 + patch.length1,patch.start2 + patch.length1 - 1 + padding)if (#suffix > 0) thenpatch.diffs[#patch.diffs + 1] = {DIFF_EQUAL, suffix}end-- Roll back the start points.patch.start1 = patch.start1 - #prefixpatch.start2 = patch.start2 - #prefix-- Extend the lengths.patch.length1 = patch.length1 + #prefix + #suffixpatch.length2 = patch.length2 + #prefix + #suffixend--[[* Given an array of patches, return another array that is identical.* @param {Array.<_new_patch_obj>} patches Array of patch objects.* @return {Array.<_new_patch_obj>} Array of patch objects.--]]function _patch_deepCopy(patches)local patchesCopy = {}for x, patch in ipairs(patches) dolocal patchCopy = _new_patch_obj()local diffsCopy = {}for i, diff in ipairs(patch.diffs) dodiffsCopy[i] = {diff[1], diff[2]}endpatchCopy.diffs = diffsCopypatchCopy.start1 = patch.start1patchCopy.start2 = patch.start2patchCopy.length1 = patch.length1patchCopy.length2 = patch.length2patchesCopy[x] = patchCopyendreturn patchesCopyend--[[* Add some padding on text start and end so that edges can match something.* Intended to be called only from within patch_apply.* @param {Array.<_new_patch_obj>} patches Array of patch objects.* @return {string} The padding string added to each side.--]]function _patch_addPadding(patches)local paddingLength = Patch_Marginlocal nullPadding = ''for x = 1, paddingLength donullPadding = nullPadding .. strchar(x)end-- Bump all the patches forward.for x, patch in ipairs(patches) dopatch.start1 = patch.start1 + paddingLengthpatch.start2 = patch.start2 + paddingLengthend-- Add some padding on start of first diff.local patch = patches[1]local diffs = patch.diffslocal firstDiff = diffs[1]if (firstDiff == nil) or (firstDiff[1] ~= DIFF_EQUAL) then-- Add nullPadding equality.tinsert(diffs, 1, {DIFF_EQUAL, nullPadding})patch.start1 = patch.start1 - paddingLength -- Should be 0.patch.start2 = patch.start2 - paddingLength -- Should be 0.patch.length1 = patch.length1 + paddingLengthpatch.length2 = patch.length2 + paddingLengthelseif (paddingLength > #firstDiff[2]) then-- Grow first equality.local extraLength = paddingLength - #firstDiff[2]firstDiff[2] = strsub(nullPadding, #firstDiff[2] + 1) .. firstDiff[2]patch.start1 = patch.start1 - extraLengthpatch.start2 = patch.start2 - extraLengthpatch.length1 = patch.length1 + extraLengthpatch.length2 = patch.length2 + extraLengthend-- Add some padding on end of last diff.patch = patches[#patches]diffs = patch.diffslocal lastDiff = diffs[#diffs]if (lastDiff == nil) or (lastDiff[1] ~= DIFF_EQUAL) then-- Add nullPadding equality.diffs[#diffs + 1] = {DIFF_EQUAL, nullPadding}patch.length1 = patch.length1 + paddingLengthpatch.length2 = patch.length2 + paddingLengthelseif (paddingLength > #lastDiff[2]) then-- Grow last equality.local extraLength = paddingLength - #lastDiff[2]lastDiff[2] = lastDiff[2] .. strsub(nullPadding, 1, extraLength)patch.length1 = patch.length1 + extraLengthpatch.length2 = patch.length2 + extraLengthendreturn nullPaddingend--[[* Look through the patches and break up any which are longer than the maximum* limit of the match algorithm.* Intended to be called only from within patch_apply.* @param {Array.<_new_patch_obj>} patches Array of patch objects.--]]function _patch_splitMax(patches)local patch_size = Match_MaxBitslocal x = 1while true dolocal patch = patches[x]if patch == nil thenreturnendif patch.length1 > patch_size thenlocal bigpatch = patch-- Remove the big old patch.tremove(patches, x)x = x - 1local start1 = bigpatch.start1local start2 = bigpatch.start2local precontext = ''while bigpatch.diffs[1] do-- Create one of several smaller patches.local patch = _new_patch_obj()local empty = truepatch.start1 = start1 - #precontextpatch.start2 = start2 - #precontextif precontext ~= '' thenpatch.length1, patch.length2 = #precontext, #precontextpatch.diffs[#patch.diffs + 1] = {DIFF_EQUAL, precontext}endwhile bigpatch.diffs[1] and (patch.length1 < patch_size-Patch_Margin) dolocal diff_type = bigpatch.diffs[1][1]local diff_text = bigpatch.diffs[1][2]if (diff_type == DIFF_INSERT) then-- Insertions are harmless.patch.length2 = patch.length2 + #diff_textstart2 = start2 + #diff_textpatch.diffs[#(patch.diffs) + 1] = bigpatch.diffs[1]tremove(bigpatch.diffs, 1)empty = falseelseif (diff_type == DIFF_DELETE) and (#patch.diffs == 1)and (patch.diffs[1][1] == DIFF_EQUAL)and (#diff_text > 2 * patch_size) then-- This is a large deletion. Let it pass in one chunk.patch.length1 = patch.length1 + #diff_textstart1 = start1 + #diff_textempty = falsepatch.diffs[#patch.diffs + 1] = {diff_type, diff_text}tremove(bigpatch.diffs, 1)else-- Deletion or equality.-- Only take as much as we can stomach.diff_text = strsub(diff_text, 1,patch_size - patch.length1 - Patch_Margin)patch.length1 = patch.length1 + #diff_textstart1 = start1 + #diff_textif (diff_type == DIFF_EQUAL) thenpatch.length2 = patch.length2 + #diff_textstart2 = start2 + #diff_textelseempty = falseendpatch.diffs[#patch.diffs + 1] = {diff_type, diff_text}if (diff_text == bigpatch.diffs[1][2]) thentremove(bigpatch.diffs, 1)elsebigpatch.diffs[1][2]= strsub(bigpatch.diffs[1][2], #diff_text + 1)endendend-- Compute the head context for the next patch.precontext = _diff_text2(patch.diffs)precontext = strsub(precontext, -Patch_Margin)-- Append the end context for this patch.local postcontext = strsub(_diff_text1(bigpatch.diffs), 1, Patch_Margin)if postcontext ~= '' thenpatch.length1 = patch.length1 + #postcontextpatch.length2 = patch.length2 + #postcontextif patch.diffs[1]and (patch.diffs[#patch.diffs][1] == DIFF_EQUAL) thenpatch.diffs[#patch.diffs][2] = patch.diffs[#patch.diffs][2].. postcontextelsepatch.diffs[#patch.diffs + 1] = {DIFF_EQUAL, postcontext}endendif not empty thenx = x + 1tinsert(patches, x, patch)endendendx = x + 1endend--[[* Emulate GNU diff's format.* Header: @@ -382,8 +481,9 @@* @return {string} The GNU diff string.--]]function _patch_appendText(patch, text)local coords1, coords2local length1, length2 = patch.length1, patch.length2local start1, start2 = patch.start1, patch.start2local diffs = patch.diffsif length1 == 1 thencoords1 = start1elsecoords1 = ((length1 == 0) and (start1 - 1) or start1) .. ',' .. length1endif length2 == 1 thencoords2 = start2elsecoords2 = ((length2 == 0) and (start2 - 1) or start2) .. ',' .. length2endtext[#text + 1] = '@@ -' .. coords1 .. ' +' .. coords2 .. ' @@\n'local op-- Escape the body of the patch with %xx notation.for x, diff in ipairs(patch.diffs) dolocal diff_type = diff[1]if diff_type == DIFF_INSERT thenop = '+'elseif diff_type == DIFF_DELETE thenop = '-'elseif diff_type == DIFF_EQUAL thenop = ' 'endtext[#text + 1] = op.. gsub(diffs[x][2], percentEncode_pattern, percentEncode_replace).. '\n'endreturn textend-- Expose the APIlocal _M = {}_M.DIFF_DELETE = DIFF_DELETE_M.DIFF_INSERT = DIFF_INSERT_M.DIFF_EQUAL = DIFF_EQUAL_M.diff_main = diff_main_M.diff_cleanupSemantic = diff_cleanupSemantic_M.diff_cleanupEfficiency = diff_cleanupEfficiency_M.diff_levenshtein = diff_levenshtein_M.diff_prettyHtml = diff_prettyHtml_M.match_main = match_main_M.patch_make = patch_make_M.patch_toText = patch_toText_M.patch_fromText = patch_fromText_M.patch_apply = patch_apply-- Expose some non-API functions as well, for testing purposes etc._M.diff_commonPrefix = _diff_commonPrefix_M.diff_commonSuffix = _diff_commonSuffix_M.diff_commonOverlap = _diff_commonOverlap_M.diff_halfMatch = _diff_halfMatch_M.diff_bisect = _diff_bisect_M.diff_cleanupMerge = _diff_cleanupMerge_M.diff_cleanupSemanticLossless = _diff_cleanupSemanticLossless_M.diff_text1 = _diff_text1_M.diff_text2 = _diff_text2_M.diff_toDelta = _diff_toDelta_M.diff_fromDelta = _diff_fromDelta_M.diff_xIndex = _diff_xIndex_M.match_alphabet = _match_alphabet_M.match_bitap = _match_bitap_M.new_patch_obj = _new_patch_obj_M.patch_addContext = _patch_addContext_M.patch_splitMax = _patch_splitMax_M.patch_addPadding = _patch_addPadding_M.settings = settingsreturn _M
require 'site'if not NylonSysCore thenrequire 'nylon.core'()endif not Pdcurses thenrequire 'LbindPdcurses'endif not NylonSqlite thenrequire 'NylonSqlite'endif not NylonOs thenpcall( function() require 'NylonOs' end )end--print( 'Pdcurses=', Pdcurses )--print( 'NylonSqlite=', NylonSqlite )--print( 'NylonSysCore=', NylonSysCore )--print( 'NylonOs=', NylonOs )
-- -*- coding: utf-8 -*----- Simple JSON encoding and decoding in pure Lua.---- Copyright 2010-2013 Jeffrey Friedl-- http://regex.info/blog/---- Latest version: http://regex.info/blog/lua/json---- This code is released under a Creative Commons CC-BY "Attribution" License:-- http://creativecommons.org/licenses/by/3.0/deed.en_US---- It can be used for any purpose so long as the copyright notice and-- web-page links above are maintained. Enjoy.--local VERSION = 20131118.9 -- version history at end of filelocal OBJDEF = { VERSION = VERSION }---- Simple JSON encoding and decoding in pure Lua.-- http://www.json.org/------ JSON = (loadfile "JSON.lua")() -- one-time load of the routines---- local lua_value = JSON:decode(raw_json_text)---- local raw_json_text = JSON:encode(lua_table_or_value)-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability------ DECODING---- JSON = (loadfile "JSON.lua")() -- one-time load of the routines---- local lua_value = JSON:decode(raw_json_text)---- If the JSON text is for an object or an array, e.g.-- { "what": "books", "count": 3 }-- or-- [ "Larry", "Curly", "Moe" ]---- the result is a Lua table, e.g.-- { what = "books", count = 3 }-- or-- { "Larry", "Curly", "Moe" }------ The encode and decode routines accept an optional second argument, "etc", which is not used-- during encoding or decoding, but upon error is passed along to error handlers. It can be of any-- type (including nil).---- With most errors during decoding, this code calls---- JSON:onDecodeError(message, text, location, etc)---- with a message about the error, and if known, the JSON text being parsed and the byte count-- where the problem was discovered. You can replace the default JSON:onDecodeError() with your-- own function.---- The default onDecodeError() merely augments the message with data about the text and the-- location if known (and if a second 'etc' argument had been provided to decode(), its value is-- tacked onto the message as well), and then calls JSON.assert(), which itself defaults to Lua's-- built-in assert(), and can also be overridden.---- For example, in an Adobe Lightroom plugin, you might use something like---- function JSON:onDecodeError(message, text, location, etc)-- LrErrors.throwUserError("Internal Error: invalid JSON data")-- end---- or even just---- function JSON.assert(message)-- LrErrors.throwUserError("Internal Error: " .. message)-- end---- If JSON:decode() is passed a nil, this is called instead:---- JSON:onDecodeOfNilError(message, nil, nil, etc)---- and if JSON:decode() is passed HTML instead of JSON, this is called:---- JSON:onDecodeOfHTMLError(message, text, nil, etc)---- The use of the fourth 'etc' argument allows stronger coordination between decoding and error-- reporting, especially when you provide your own error-handling routines. Continuing with the-- the Adobe Lightroom plugin example:---- function JSON:onDecodeError(message, text, location, etc)-- local note = "Internal Error: invalid JSON data"-- if type(etc) = 'table' and etc.photo then-- note = note .. " while processing for " .. etc.photo:getFormattedMetadata('fileName')-- end-- LrErrors.throwUserError(note)-- end---- :-- :---- for i, photo in ipairs(photosToProcess) do-- :-- :-- local data = JSON:decode(someJsonText, { photo = photo })-- :-- :-- end---------- DECODING AND STRICT TYPES---- Because both JSON objects and JSON arrays are converted to Lua tables, it's not normally-- possible to tell which a JSON type a particular Lua table was derived from, or guarantee-- decode-encode round-trip equivalency.---- However, if you enable strictTypes, e.g.---- JSON = (loadfile "JSON.lua")() --load the routines-- JSON.strictTypes = true---- then the Lua table resulting from the decoding of a JSON object or JSON array is marked via Lua-- metatable, so that when re-encoded with JSON:encode() it ends up as the appropriate JSON type.---- (This is not the default because other routines may not work well with tables that have a-- metatable set, for example, Lightroom API calls.)------ ENCODING---- JSON = (loadfile "JSON.lua")() -- one-time load of the routines---- local raw_json_text = JSON:encode(lua_table_or_value)-- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability-- On error during encoding, this code calls:---- JSON:onEncodeError(message, etc)---- which you can override in your local JSON object.---- If the Lua table contains both string and numeric keys, it fits neither JSON's-- idea of an object, nor its idea of an array. To get around this, when any string-- key exists (or when non-positive numeric keys exist), numeric keys are converted-- to strings.---- For example,-- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" }))-- produces the JSON object-- {"1":"one","2":"two","3":"three","SOMESTRING":"some string"}---- To prohibit this conversion and instead make it an error condition, set-- JSON.noKeyConversion = true---- SUMMARY OF METHODS YOU CAN OVERRIDE IN YOUR LOCAL LUA JSON OBJECT---- assert-- onDecodeError-- onDecodeOfNilError-- onDecodeOfHTMLError-- onEncodeError---- If you want to create a separate Lua JSON object with its own error handlers,-- you can reload JSON.lua or use the :new() method.-----------------------------------------------------------------------------local author = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json), version " .. tostring(VERSION) .. " ]-"local isArray = { __tostring = function() return "JSON array" end } isArray.__index = isArraylocal isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObjectfunction OBJDEF:newArray(tbl)return setmetatable(tbl or {}, isArray)endfunction OBJDEF:newObject(tbl)return setmetatable(tbl or {}, isObject)endlocal function unicode_codepoint_as_utf8(codepoint)---- codepoint is a number--if codepoint <= 127 thenreturn string.char(codepoint)elseif codepoint <= 2047 then---- 110yyyxx 10xxxxxx <-- useful notation from http://en.wikipedia.org/wiki/Utf8--local highpart = math.floor(codepoint / 0x40)local lowpart = codepoint - (0x40 * highpart)return string.char(0xC0 + highpart,0x80 + lowpart)elseif codepoint <= 65535 then---- 1110yyyy 10yyyyxx 10xxxxxx--local highpart = math.floor(codepoint / 0x1000)local remainder = codepoint - 0x1000 * highpartlocal midpart = math.floor(remainder / 0x40)local lowpart = remainder - 0x40 * midparthighpart = 0xE0 + highpartmidpart = 0x80 + midpartlowpart = 0x80 + lowpart---- Check for an invalid character (thanks Andy R. at Adobe).-- See table 3.7, page 93, in http://www.unicode.org/versions/Unicode5.2.0/ch03.pdf#G28070--if ( highpart == 0xE0 and midpart < 0xA0 ) or( highpart == 0xED and midpart > 0x9F ) or( highpart == 0xF0 and midpart < 0x90 ) or( highpart == 0xF4 and midpart > 0x8F )thenreturn "?"elsereturn string.char(highpart,midpart,lowpart)endelse---- 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx--local highpart = math.floor(codepoint / 0x40000)local remainder = codepoint - 0x40000 * highpartlocal midA = math.floor(remainder / 0x1000)remainder = remainder - 0x1000 * midAlocal midB = math.floor(remainder / 0x40)local lowpart = remainder - 0x40 * midBreturn string.char(0xF0 + highpart,0x80 + midA,0x80 + midB,0x80 + lowpart)endendfunction OBJDEF:onDecodeError(message, text, location, etc)if text thenif location thenmessage = string.format("%s at char %d of: %s", message, location, text)elsemessage = string.format("%s: %s", message, text)endendif etc ~= nil thenmessage = message .. " (" .. OBJDEF:encode(etc) .. ")"endif self.assert thenself.assert(false, message)elseassert(false, message)endendOBJDEF.onDecodeOfNilError = OBJDEF.onDecodeErrorOBJDEF.onDecodeOfHTMLError = OBJDEF.onDecodeErrorfunction OBJDEF:onEncodeError(message, etc)if etc ~= nil thenmessage = message .. " (" .. OBJDEF:encode(etc) .. ")"endif self.assert thenself.assert(false, message)elseassert(false, message)endendlocal function grok_number(self, text, start, etc)---- Grab the integer part--local integer_part = text:match('^-?[1-9]%d*', start)or text:match("^-?0", start)if not integer_part thenself:onDecodeError("expected number", text, start, etc)endlocal i = start + integer_part:len()---- Grab an optional decimal part--local decimal_part = text:match('^%.%d+', i) or ""i = i + decimal_part:len()---- Grab an optional exponential part--local exponent_part = text:match('^[eE][-+]?%d+', i) or ""i = i + exponent_part:len()local full_number_text = integer_part .. decimal_part .. exponent_partlocal as_number = tonumber(full_number_text)if not as_number thenself:onDecodeError("bad number", text, start, etc)endreturn as_number, iendlocal function grok_string(self, text, start, etc)if text:sub(start,start) ~= '"' thenself:onDecodeError("expected string's opening quote", text, start, etc)endlocal i = start + 1 -- +1 to bypass the initial quotelocal text_len = text:len()local VALUE = ""while i <= text_len dolocal c = text:sub(i,i)if c == '"' thenreturn VALUE, i + 1endif c ~= '\\' thenVALUE = VALUE .. ci = i + 1elseif text:match('^\\b', i) thenVALUE = VALUE .. "\b"i = i + 2elseif text:match('^\\f', i) thenVALUE = VALUE .. "\f"i = i + 2elseif text:match('^\\n', i) thenVALUE = VALUE .. "\n"i = i + 2elseif text:match('^\\r', i) thenVALUE = VALUE .. "\r"i = i + 2elseif text:match('^\\t', i) thenVALUE = VALUE .. "\t"i = i + 2elselocal hex = text:match('^\\u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i)if hex theni = i + 6 -- bypass what we just read-- We have a Unicode codepoint. It could be standalone, or if in the proper range and-- followed by another in a specific range, it'll be a two-code surrogate pair.local codepoint = tonumber(hex, 16)if codepoint >= 0xD800 and codepoint <= 0xDBFF then-- it's a hi surrogate... see whether we have a following lowlocal lo_surrogate = text:match('^\\u([dD][cdefCDEF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i)if lo_surrogate theni = i + 6 -- bypass the low surrogate we just readcodepoint = 0x2400 + (codepoint - 0xD800) * 0x400 + tonumber(lo_surrogate, 16)else-- not a proper low, so we'll just leave the first codepoint as is and spit it out.endendVALUE = VALUE .. unicode_codepoint_as_utf8(codepoint)else-- just pass through what's escapedVALUE = VALUE .. text:match('^\\(.)', i)i = i + 2endendendself:onDecodeError("unclosed string", text, start, etc)endlocal function skip_whitespace(text, start)local match_start, match_end = text:find("^[ \n\r\t]+", start) -- [http://www.ietf.org/rfc/rfc4627.txt] Section 2if match_end thenreturn match_end + 1elsereturn startendendlocal grok_one -- assigned laterlocal function grok_object(self, text, start, etc)if not text:sub(start,start) == '{' thenself:onDecodeError("expected '{'", text, start, etc)endlocal i = skip_whitespace(text, start + 1) -- +1 to skip the '{'local VALUE = self.strictTypes and self:newObject { } or { }if text:sub(i,i) == '}' thenreturn VALUE, i + 1endlocal text_len = text:len()while i <= text_len dolocal key, new_i = grok_string(self, text, i, etc)i = skip_whitespace(text, new_i)if text:sub(i, i) ~= ':' thenself:onDecodeError("expected colon", text, i, etc)endi = skip_whitespace(text, i + 1)local val, new_i = grok_one(self, text, i)VALUE[key] = val---- Expect now either '}' to end things, or a ',' to allow us to continue.--i = skip_whitespace(text, new_i)local c = text:sub(i,i)if c == '}' thenreturn VALUE, i + 1endif text:sub(i, i) ~= ',' thenself:onDecodeError("expected comma or '}'", text, i, etc)endi = skip_whitespace(text, i + 1)endself:onDecodeError("unclosed '{'", text, start, etc)endlocal function grok_array(self, text, start, etc)if not text:sub(start,start) == '[' thenself:onDecodeError("expected '['", text, start, etc)endlocal i = skip_whitespace(text, start + 1) -- +1 to skip the '['local VALUE = self.strictTypes and self:newArray { } or { }if text:sub(i,i) == ']' thenreturn VALUE, i + 1endlocal text_len = text:len()while i <= text_len dolocal val, new_i = grok_one(self, text, i)table.insert(VALUE, val)i = skip_whitespace(text, new_i)---- Expect now either ']' to end things, or a ',' to allow us to continue.--local c = text:sub(i,i)if c == ']' thenreturn VALUE, i + 1endif text:sub(i, i) ~= ',' thenself:onDecodeError("expected comma or '['", text, i, etc)endi = skip_whitespace(text, i + 1)endself:onDecodeError("unclosed '['", text, start, etc)endgrok_one = function(self, text, start, etc)-- Skip any whitespacestart = skip_whitespace(text, start)if start > text:len() thenself:onDecodeError("unexpected end of string", text, nil, etc)endif text:find('^"', start) thenreturn grok_string(self, text, start, etc)elseif text:find('^[-0123456789 ]', start) thenreturn grok_number(self, text, start, etc)elseif text:find('^%{', start) thenreturn grok_object(self, text, start, etc)elseif text:find('^%[', start) thenreturn grok_array(self, text, start, etc)elseif text:find('^true', start) thenreturn true, start + 4elseif text:find('^false', start) thenreturn false, start + 5elseif text:find('^null', start) thenreturn nil, start + 4elseself:onDecodeError("can't parse JSON", text, start, etc)endendfunction OBJDEF:decode(text, etc)if type(self) ~= 'table' or self.__index ~= OBJDEF thenOBJDEF:onDecodeError("JSON:decode must be called in method format", nil, nil, etc)endif text == nil thenself:onDecodeOfNilError(string.format("nil passed to JSON:decode()"), nil, nil, etc)elseif type(text) ~= 'string' thenself:onDecodeError(string.format("expected string argument to JSON:decode(), got %s", type(text)), nil, nil, etc)endif text:match('^%s*$') thenreturn nilendif text:match('^%s*<') then-- Can't be JSON... we'll assume it's HTMLself:onDecodeOfHTMLError(string.format("html passed to JSON:decode()"), text, nil, etc)end---- Ensure that it's not UTF-32 or UTF-16.-- Those are perfectly valid encodings for JSON (as per RFC 4627 section 3),-- but this package can't handle them.--if text:sub(1,1):byte() == 0 or (text:len() >= 2 and text:sub(2,2):byte() == 0) thenself:onDecodeError("JSON package groks only UTF-8, sorry", text, nil, etc)endlocal success, value = pcall(grok_one, self, text, 1, etc)if success thenreturn valueelse-- should never get here... JSON parse errors should have been caught earlierassert(false, value)return nilendendlocal function backslash_replacement_function(c)if c == "\n" thenreturn "\\n"elseif c == "\r" thenreturn "\\r"elseif c == "\t" thenreturn "\\t"elseif c == "\b" thenreturn "\\b"elseif c == "\f" thenreturn "\\f"elseif c == '"' thenreturn '\\"'elseif c == '\\' thenreturn '\\\\'elsereturn string.format("\\u%04x", c:byte())endendlocal chars_to_be_escaped_in_JSON_string= '['.. '"' -- class sub-pattern to match a double quote.. '%\\' -- class sub-pattern to match a backslash.. '%z' -- class sub-pattern to match a null.. '\001' .. '-' .. '\031' -- class sub-pattern to match control characters.. ']'local function json_string_literal(value)local newval = value:gsub(chars_to_be_escaped_in_JSON_string, backslash_replacement_function)return '"' .. newval .. '"'endlocal function object_or_array(self, T, etc)---- We need to inspect all the keys... if there are any strings, we'll convert to a JSON-- object. If there are only numbers, it's a JSON array.---- If we'll be converting to a JSON object, we'll want to sort the keys so that the-- end result is deterministic.--local string_keys = { }local number_keys = { }local number_keys_must_be_strings = falselocal maximum_number_keyfor key in pairs(T) doif type(key) == 'string' thentable.insert(string_keys, key)elseif type(key) == 'number' thentable.insert(number_keys, key)if key <= 0 or key >= math.huge thennumber_keys_must_be_strings = trueelseif not maximum_number_key or key > maximum_number_key thenmaximum_number_key = keyendelseself:onEncodeError("can't encode table with a key of type " .. type(key), etc)endendif #string_keys == 0 and not number_keys_must_be_strings then---- An empty table, or a numeric-only array--if #number_keys > 0 thenreturn nil, maximum_number_key -- an arrayelseif tostring(T) == "JSON array" thenreturn nilelseif tostring(T) == "JSON object" thenreturn { }else-- have to guess, so we'll pick array, since empty arrays are likely more common than empty objectsreturn nilendendtable.sort(string_keys)local mapif #number_keys > 0 then---- If we're here then we have either mixed string/number keys, or numbers inappropriate for a JSON array-- It's not ideal, but we'll turn the numbers into strings so that we can at least create a JSON object.--if JSON and JSON.noKeyConversion thenself:onEncodeError("a table with both numeric and string keys could be an object or array; aborting", etc)end---- Have to make a shallow copy of the source table so we can remap the numeric keys to be strings--map = { }for key, val in pairs(T) domap[key] = valendtable.sort(number_keys)---- Throw numeric keys in there as strings--for _, number_key in ipairs(number_keys) dolocal string_key = tostring(number_key)if map[string_key] == nil thentable.insert(string_keys , string_key)map[string_key] = T[number_key]elseself:onEncodeError("conflict converting table with mixed-type keys into a JSON object: key " .. number_key .. " exists both as a string and a number.", etc)endendendreturn string_keys, nil, mapend---- Encode--local encode_value -- must predeclare because it calls itselffunction encode_value(self, value, parents, etc, indent) -- non-nil indent means pretty-printingif value == nil thenreturn 'null'elseif type(value) == 'string' thenreturn json_string_literal(value)elseif type(value) == 'number' thenif value ~= value then---- NaN (Not a Number).-- JSON has no NaN, so we have to fudge the best we can. This should really be a package option.--return "null"elseif value >= math.huge then---- Positive infinity. JSON has no INF, so we have to fudge the best we can. This should-- really be a package option. Note: at least with some implementations, positive infinity-- is both ">= math.huge" and "<= -math.huge", which makes no sense but that's how it is.-- Negative infinity is properly "<= -math.huge". So, we must be sure to check the ">="-- case first.--return "1e+9999"elseif value <= -math.huge then---- Negative infinity.-- JSON has no INF, so we have to fudge the best we can. This should really be a package option.--return "-1e+9999"elsereturn tostring(value)endelseif type(value) == 'boolean' thenreturn tostring(value)elseif type(value) ~= 'table' thenself:onEncodeError("can't convert " .. type(value) .. " to JSON", etc)else---- A table to be converted to either a JSON object or array.--local T = valueif parents[T] thenself:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc)elseparents[T] = trueendlocal result_valuelocal object_keys, maximum_number_key, map = object_or_array(self, T, etc)if maximum_number_key then---- An array...--local ITEMS = { }for i = 1, maximum_number_key dotable.insert(ITEMS, encode_value(self, T[i], parents, etc, indent))endif indent thenresult_value = "[ " .. table.concat(ITEMS, ", ") .. " ]"elseresult_value = "[" .. table.concat(ITEMS, ",") .. "]"endelseif object_keys then---- An object--local TT = map or Tif indent thenlocal KEYS = { }local max_key_length = 0for _, key in ipairs(object_keys) dolocal encoded = encode_value(self, tostring(key), parents, etc, "")max_key_length = math.max(max_key_length, #encoded)table.insert(KEYS, encoded)endlocal key_indent = indent .. " "local subtable_indent = indent .. string.rep(" ", max_key_length + 2 + 4)local FORMAT = "%s%" .. string.format("%d", max_key_length) .. "s: %s"local COMBINED_PARTS = { }for i, key in ipairs(object_keys) dolocal encoded_val = encode_value(self, TT[key], parents, etc, subtable_indent)table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val))endresult_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}"elselocal PARTS = { }for _, key in ipairs(object_keys) dolocal encoded_val = encode_value(self, TT[key], parents, etc, indent)local encoded_key = encode_value(self, tostring(key), parents, etc, indent)table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val))endresult_value = "{" .. table.concat(PARTS, ",") .. "}"endelse---- An empty array/object... we'll treat it as an array, though it should really be an option--result_value = "[]"endparents[T] = falsereturn result_valueendendfunction OBJDEF:encode(value, etc)if type(self) ~= 'table' or self.__index ~= OBJDEF thenOBJDEF:onEncodeError("JSON:encode must be called in method format", etc)endreturn encode_value(self, value, {}, etc, nil)endfunction OBJDEF:encode_pretty(value, etc)if type(self) ~= 'table' or self.__index ~= OBJDEF thenOBJDEF:onEncodeError("JSON:encode_pretty must be called in method format", etc)endreturn encode_value(self, value, {}, etc, "")endfunction OBJDEF.__tostring()return "JSON encode/decode package"endOBJDEF.__index = OBJDEFfunction OBJDEF:new(args)local new = { }if args thenfor key, val in pairs(args) donew[key] = valendendreturn setmetatable(new, OBJDEF)endreturn OBJDEF:new()---- Version history:---- 20131118.9 Update for Lua 5.3... it seems that tostring(2/1) produces "2.0" instead of "2",-- and this caused some problems.---- 20131031.8 Unified the code for encode() and encode_pretty(); they had been stupidly separate,-- and had of course diverged (encode_pretty didn't get the fixes that encode got, so-- sometimes produced incorrect results; thanks to Mattie for the heads up).---- Handle encoding tables with non-positive numeric keys (unlikely, but possible).---- If a table has both numeric and string keys, or its numeric keys are inappropriate-- (such as being non-positive or infinite), the numeric keys are turned into-- string keys appropriate for a JSON object. So, as before,-- JSON:encode({ "one", "two", "three" })-- produces the array-- ["one","two","three"]-- but now something with mixed key types like-- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" }))-- instead of throwing an error produces an object:-- {"1":"one","2":"two","3":"three","SOMESTRING":"some string"}---- To maintain the prior throw-an-error semantics, set-- JSON.noKeyConversion = true---- 20131004.7 Release under a Creative Commons CC-BY license, which I should have done from day one, sorry.---- 20130120.6 Comment update: added a link to the specific page on my blog where this code can-- be found, so that folks who come across the code outside of my blog can find updates-- more easily.---- 20111207.5 Added support for the 'etc' arguments, for better error reporting.---- 20110731.4 More feedback from David Kolf on how to make the tests for Nan/Infinity system independent.---- 20110730.3 Incorporated feedback from David Kolf at http://lua-users.org/wiki/JsonModules:---- * When encoding lua for JSON, Sparse numeric arrays are now handled by-- spitting out full arrays, such that-- JSON:encode({"one", "two", [10] = "ten"})-- returns-- ["one","two",null,null,null,null,null,null,null,"ten"]---- In 20100810.2 and earlier, only up to the first non-null value would have been retained.---- * When encoding lua for JSON, numeric value NaN gets spit out as null, and infinity as "1+e9999".-- Version 20100810.2 and earlier created invalid JSON in both cases.---- * Unicode surrogate pairs are now detected when decoding JSON.---- 20100810.2 added some checking to ensure that an invalid Unicode character couldn't leak in to the UTF-8 encoding---- 20100731.1 initial public release--