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 then
require 'nylon.core'()
end
if not Pdcurses then
require 'LbindPdcurses'
end
local 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 - ptTopLeft
local ptRow, ptCol
local drow = 0
local dcol = 0
local charsDrawn = 0
-- local nextiseol
for l, col, eol in buffer:walkFragmentsEOL( ptTopLeft ) do
local toDraw = eol and (eol - col) or #l
-- if nextiseol then drow = drow + 1; nextiseol = false end
if ((charsDrawn + toDraw) >= charsInto) then
return (dcol + charsInto-charsDrawn), drow
else
charsDrawn = charsDrawn + toDraw
end
if eol then
charsDrawn = charsDrawn + 1
dcol = 0
drow = drow + 1
-- nextiseol = true
else
dcol = dcol + toDraw
end
end
end
local function mysert_eq( v, shouldbe )
if v ~= shouldbe then
print(string.format('expected value=%d got=%d',shouldbe,v))
assert(false)
end
end
local 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 )
-- end
local 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) do
table.insert(rcvd, {col,l,eol} )
table.insert(re, l:sub(col,eol))
end
local JSON = require 'JSON'
if true or ( table.concat(re) ~= t ) then
for i, v in ipairs(re) do
print(i,JSON:encode(v))
print(i,JSON:encode(rcvd[i]) )
end
end
-- b:dump()
assert(table.concat(re)==t)
return b
end
if true then
local 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)
--- fail
local 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://') then
print('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)
end
end
local function cord_clipboard_monitor( cord )
require 'NylonOs'
local lastClip
while true do
-- wv.log('debug', 'clipboard monitor running')
local clip = NylonOs.Static.getclipboard()
if clip ~= lastClip then
lastClip = clip
addNewClipText( clip )
end
cord:sleep(0.3)
end
end
local cb_cord = Nylon.cord('clipboard-mon', cord_clipboard_monitor )
local Table = require 'extable'
function cleanupctxrecord( record )
if record.extern_id then
record.extern_id = tonumber(record.extern_id)
end
if record.context_t then
record.context_t = tonumber(record.context_t)
end
return record
end
local 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 )
end
function RPC.getRecentContext( N )
N = N or 20
wv.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 )
end
function 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)
end
function 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)
end
return RPC
--[[--
Provides file archiving services for PLS system; send a list of file names, they will be copied to
an 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 results
end
local function cordfn_bg_copy( cord )
while true do
collectgarbage()
local msg = cord:getmsg()
local shell = Psbind.Shell() -- shell is created here so that it will be closed while idle; these things suck memory
for _, v in ipairs(msg) do
local store = v.archpath
local item = v.item
wv.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 ) )
end
end
end
local 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 .. '/' .. rand
local store = File.joinpath( config.archiveDir, subdir )
table.insert( docopies, { item = item, archpath = store } )
local url = table.concat( { config.archiveUrl, subdir, base }, '/' )
return url
end, fileList )
cord_bg_copy:msg( docopies )
return results
end
return 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.ROWID
recid.ROWID = nil
return recid
end
local function getFullRecordForId( oneId )
local recid = (db:selectOne('select * from note where ROWID=?', oneId))
recid.id = recid.ROWID
recid.ROWID = nil
return recid
end
local Table = require 'extable'
function RPC.getRecordMetadata( id )
if type(id) == 'number' then -- single id
return getMetadataForId( id )
elseif type(id) == 'table' then -- todo: to optimize, use one SQL query
if type(id[1]) == 'number' then -- found a list of ids
return Table.map( getMetadataForId, id )
end
end
end
function RPC.getFullRecord( id )
if type(id) == 'number' then -- single id
return getFullRecordForId( id )
elseif type(id) == 'table' then
if type(id[1]) == 'number' then -- found a list of ids
return Table.map( getFullRecordForId, id )
end
end
end
local 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 then
wv.log('error', 'db could not open name=%s', dbname)
error 'no database'
else
wv.log('debug', 'db opened with success name=%s', dbname)
end
return setmetatable({ db = db }, { __index = Sqlite })
-- -- print(string.format("Sqlite:new db=%s", dbname))
-- return setmetatable({
-- db = NylonSqlite(dbname)
-- }, { __index = Sqlite })
end
function Sqlite:selectOne(sql,...)
local rc
if not self.db then
wv.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 then
rc.ROWID = tonumber(rc.ROWID)
end
return rc
end
function Sqlite:selectMany(sql,...)
local results = {}
self.db:selectMany( function(row)
table.insert(results,row)
end, sql, {...} )
return results
end
function Sqlite:exec(sql,...)
return self.db:exec( sql, {...} )
end
function Sqlite:retryexec(sql,...)
local dbrc
local args = { ... }
for i = 1, 7 do
local rc, err = pcall(function()
dbrc = self.db:exec( sql, args )
end)
if rc then
break
end
wv.log('error','could not exec [try %d, sql=%s], e=%s', i, sql, err)
Nylon.self:sleep( 0.05 * (2 ^ i) )
end
return dbrc
end
function Sqlite:lastRowId()
return self.db:lastRowId()
end
return 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 then
loadfile '../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 debugging
local 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) do
local gdiff = Diff.patch_fromText( patch.rcontent )
inittext = Diff.patch_apply( gdiff, inittext )
end
local 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 }
end
local function WindowMoveResize( w, dim )
w:resize( dim.h, dim.w )
w:mvwin( dim.y, dim.x )
end
local win_focused
local function entryfn_winman( cord, env )
local tiled = {}
local modal = {}
local landscape = false
local fullscreened = false
local function moveTiledRight_portrait(nexisting)
local halfscreen = math.floor(env.curses.ncols/2)
local h = env.curses.nrows / nexisting
local lasttop = env.curses.nrows
for i = 1, nexisting do
local prev = tiled[i]
local newtop = math.floor(env.curses.nrows-(i*h))
local newdim = WindowDim( halfscreen, newtop, halfscreen, (lasttop-newtop) )
lasttop = newtop
WindowMoveResize( prev.win, newdim )
wv.log('debug','moveTiledRight resized')
prev.on.resized( newdim )
end
end
local function moveTiledRight_landscape(nexisting)
local halfscreen = math.floor(env.curses.nrows/2)
local width = env.curses.ncols / nexisting
local lasttop = env.curses.ncols
for i = 1, nexisting do
local prev = tiled[i]
local newtop = math.floor(env.curses.ncols-(i*width))
local newdim = WindowDim( newtop, halfscreen, (lasttop-newtop), halfscreen )
lasttop = newtop
WindowMoveResize( prev.win, newdim )
wv.log('debug','moveTiledRight resized')
prev.on.resized( newdim )
end
end
local function moveTiledRight(nexisting)
if landscape then
moveTiledRight_landscape(nexisting)
else
moveTiledRight_portrait(nexisting)
end
end
local 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 -1
moveTiledRight(nexisting)
if nexisting > 0 then
moveTiledRight( nexisting )
else
env.curses.screen:clear()
env.curses.screen:refresh()
end
top.on.resized( newdim )
end
local 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 -1
moveTiledRight(nexisting)
if nexisting > 0 then
moveTiledRight( nexisting )
else
env.curses.screen:clear()
env.curses.screen:refresh()
end
top.on.resized( newdim )
end
local function fullscreen_thisWindow( top )
local newdim = WindowDim(0,0,env.curses.ncols,env.curses.nrows)
if top.win then
WindowMoveResize( top.win, newdim )
else
wv.log('debug','weird, window has no window top=%s', top)
end
if top.on and top.on.resized then
top.on.resized( newdim )
else
wv.log('debug','weird, th top=%s', top)
end
end
local d_minimized = WindowDim( 0, 0, 1, 1 )
local function resizeAllTiled()
if not tiled[1] then -- no windows
env.curses.screen:mvaddstr(0,0,"No buffers; press C-l to open or create new record")
env.curses.screen:refresh()
return
end
if #tiled > 1 then
if fullscreened then
fullscreen_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
-- end
elseif landscape then
resizeAllTiled_landscape()
else
resizeAllTiled_portrait()
end
else -- only one window
fullscreen_thisWindow( tiled[1] )
end
for _, w in ipairs(modal) do
if w.on.resized then
w.on.resized()
end
end
end
local function change_focus(towin)
if win_focused ~= towin then
local prev = win_focused
win_focused = towin
local _ = prev and prev.on.resized and prev.on.resized()
local _ = towin and towin.on.resized and towin.on.resized()
end
end
local function set_best_focus()
change_focus( (#modal > 0) and modal[#modal] or tiled[#tiled] )
end
------------------------------------------------------------------
------------------------------------------------------------------
function cord.event.toggle_landscape()
landscape = not landscape
resizeAllTiled()
end
function cord.event.toggle_fullscreen()
if fullscreened then
fullscreened = false
else
if #tiled > 0 then
fullscreened = tiled[#tiled]
end
end
resizeAllTiled()
end
function cord.event.focused_window_to_primary()
for i, w in ipairs(tiled) do
if w == win_focused then
table.remove(tiled,i)
table.insert(tiled,win_focused)
resizeAllTiled()
return
end
end
end
function cord.event.this_window_to_primary(win)
for i, w in ipairs(tiled) do
if w == win then
table.remove(tiled,i)
table.insert(tiled,win)
resizeAllTiled()
return
end
end
end
function cord.event.other_window()
if #modal > 0 then
wv.log('debug','no window switching when modal dialog is up')
elseif win_focused and #tiled > 1 then
local donext = false
for n = #tiled, 1, -1 do
if donext then
change_focus( tiled[n] )
return
end
donext = (win_focused == tiled[n])
end
change_focus(tiled[#tiled]) -- we wrapped, last focused was probably window 1
end
end
-- add a managed window to the tiled list
function cord.event.push_tiled( mw )
-- if #tiled > 0 then
-- moveTiledRight(#tiled)
-- -- mw.cord.event.refresh(true)
-- end
table.insert( tiled, mw )
resizeAllTiled()
change_focus( mw )
end
function cord.event.remove_tiled( mw )
local remain = {}
for i, v in ipairs(tiled) do
wv.log('debug','winman remove_tiled mw=%s v=%s',mw, v)
if v ~= mw then
table.insert( remain, v )
end
end
tiled = remain
resizeAllTiled()
set_best_focus()
end
function cord.event.Refresh()
resizeAllTiled()
end
function cord.event.swap_tiled()
wv.log('debug','cord.event.swap_tiled #tiled=%d',#tiled)
if #tiled > 1 then
local prevtop = tiled[#tiled]
tiled[#tiled] = tiled[#tiled-1]
tiled[#tiled-1] = prevtop
if fullscreened == prevtop then
fullscreened = tiled[#tiled]
end
resizeAllTiled()
set_best_focus()
end
end
function cord.event.push_modal( mw )
table.insert( modal, mw )
change_focus( mw )
end
function cord.event.remove_modal( mw )
if modal[#modal] ~= mw then
wv.log('error','removed modal dialog not active, fixme')
else
table.remove(modal,#modal)
resizeAllTiled() -- just to force refresh
set_best_focus()
end
end
while true do
cord:sleep(1)
end
end
local 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 be
managed 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 requests
visual link activation, all visible windows are queried for targets and assigned
a alphanumeric sequence. If more than 36 links are active, all links must be
two characters. Generally, link enumeration should increase left to right and
then top to bottom within a window. < WATCHU TALKING BOUT WILLIS ???
--]]--
function Malwin:new( opt )
local o = setmetatable( {}, { __index = self } )
if not opt.win then
wv.log('abnorm','creating winman managed window with no curses window?')
end
o.win = opt.win
o.on = opt.on or {}
return o
end
local 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 handle
if win_focused and win_focused.on.key then
wv.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 then
unhandled_keys(rc)
end
else
wv.log('abnorm','input key/map [%s->%s] not handled by main app, no focused win with keyhandler',k,mapped)
end
else
wv.log('norm','input key/map [%s] handled by main app',k)
end
end
cord.event.never.wait()
end
local 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 )
end
local cord_key
local 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_key
end,
inject_key = function(k)
cord_key.event.key(k)
end
}
local function malwin_cleanup(malwin)
if not malwin.win then
wv.log('abnorm','removing win twice')
else
malwin.win:clear()
malwin.win:refresh()
malwin.win:forcedelete()
malwin.win = nil
end
end
function 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)
end
end
function module.swap_tiled()
wv.log('debug','Winman.swap_tiled fn')
cord_winman.event.swap_tiled()
end
function module.remove_focused()
cord_winman.event.remove_focused()
end
-- returns a function which can be used to remove the window
function 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 )
end
end
function module.other_window()
cord_winman.event.other_window()
end
function module.toggle_landscape()
cord_winman.event.toggle_landscape()
end
function module.toggle_fullscreen()
cord_winman.event.toggle_fullscreen()
end
function module.isFocused(w)
return w == win_focused
end
function module.Refresh()
cord_winman.event.Refresh()
end
function module.focused_window_to_primary()
cord_winman.event.focused_window_to_primary()
end
function module.this_window_to_primary(win)
cord_winman.event.this_window_to_primary(win)
end
end
return module
local wv = require 'nylon.debug'{ name = 'pls-theme' }
pcall( function() require 'NylonOs' end ) -- just to test for windows / not windows
local 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) do
table.insert(free_pairs,i)
end
wv.log 'welcome to pls-theme'
local function get_color( fg, bg )
local key = fg * 0x100000 + bg
if assigned_pairs[key] then
return assigned_pairs[key][1]
else
local 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 color
end
end
-- 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 bold
normal = { 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 }
end
local with = setmetatable( {}, {
__index = function(t,v)
local fgbg = theme[v]
if fgbg then
local color = get_color( fgbg[1], fgbg[2] )
return function(win,withfun)
if win then
win:attron( color )
if fgbg[3] then
win:attron( Pdcurses.Color.a_bold )
end
end
withfun()
if win then
if fgbg[3] then
win:attroff( Pdcurses.Color.a_bold )
end
win:attroff( color )
end
end -- end, 'with' function
end
end })
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 then
return get_color( fgbg[1], fgbg[2] )
end
end } )
local kIdealStringLength=768
local wv = require 'nylon.debug' { name = 'pls-bbuf' }
local Bbuf = {}
function Bbuf.setIdealStringLength( n )
kIdealStringLength = n
end
function Bbuf:_invalidateOnEdit()
self.cachept = 9.0E99
end
local function Node(text)
return { height = 0, v = text, len=#text }
end
local function tree_delta( self )
return (self.l and self.l.height or 0) - (self.r and self.r.height or 0)
end
local tree_balance
local function tree_rotl(self)
local r = self.r
self.r = r.l
r.l = tree_balance(self)
return tree_balance(r)
end
local function tree_rotr( self )
local l = self.l
self.l = l.r
l.r = tree_balance(self)
return tree_balance(l)
end
local function node_setlen( node )
node.len = (node.l and node.l.len or 0) --
node.len = node.len + #node.v
node.len = node.len + (node.r and node.r.len or 0)
end
tree_balance = function(self)
local delta = tree_delta(self)
if delta < -1 then
self.r = tree_delta(self.r) > 0 and tree_rotr(self.r) or self.r
return tree_rotl(self)
elseif delta > 1 then
self.l = tree_delta(self.l) < 0 and tree_rotl(self.l) or self.l
return tree_rotr(self)
end
self.height = 0
if self.l and self.l.height > self.height then
self.height = self.l.height
end
if self.r and self.r.height > self.height then
self.height = self.r.height
end
self.height = self.height + 1
node_setlen(self)
return self
end
local function tree_insert( self, pos, elm )
-- wv.log('debug','tree insert, e=%s', elm.v)
if not self then
return elm
end
if self.l then
if pos <= self.l.len then
self.l = tree_insert(self.l, pos, elm)
node_setlen(self)
return tree_balance(self)
else
pos = pos - self.l.len
end
elseif pos <= 1 then
self.l = tree_insert( self.l, pos, elm )
node_setlen(self)
return tree_balance( self )
end
if pos <= (#self.v+1) then
local 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 then
self.v = modified
node_setlen(self)
return self
else
local 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)
end
end
pos = pos - #self.v
self.r = tree_insert( self.r, pos, elm )
node_setlen(self)
return tree_balance(self)
end
local function tree_delete( self, posbeg, len )
if not self then
return
end
wv.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 then
if posbeg <= self.l.len then
local remain = tree_delete(self.l, posbeg, len)
if remain then
posbeg = 1
len = remain
wv.log('debug','back to clip more, remain=%d #v=%d',remain,#self.v)
else
node_setlen(self)
return -- tree_balance(self)
end
else
posbeg = posbeg - self.l.len
end
end
if posbeg <= #self.v then
local remain = #self.v - posbeg - len + 1
if remain > 0 then
local modified = self.v:sub( 1, posbeg-1 ) .. self.v:sub(posbeg+len)
self.v = modified
node_setlen(self)
return
else
local modified = self.v:sub( 1, posbeg-1 )
local removed = #self.v - posbeg + 1
self.v = modified
len = len - removed
posbeg = 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)
end
else
posbeg = posbeg - #self.v
end
if self.r then
local remain = tree_delete( self.r, posbeg, len )
node_setlen(self)
return remain -- tree_balance(self)
else
wv.log('debug','need to clip more on return, posbeg=%d len=%d', posbeg, len)
node_setlen(self)
return len
end
end
function treewalk_to( self, point )
if not self then -- out of bounds or something
wv.log('abnorm','avl text search out of bounds, point=%d',point)
return
end
if self.l then
if point <= self.l.len then
return treewalk_to( self.l, point )
else
point = point - self.l.len
end
end
if point <= #self.v then
-- wv.log('debug','treewalk_to point=%d #l=%d',point,#self.v)
return self.v, point
end
return treewalk_to( self.r, point - #self.v )
end
function tree_traverse( self, point, cbfun )
-- wv.log('debug','tree_traverse point=%d cbfun=%s', point, cbfun )
if not self then
return
end
if self.l then
if point <= self.l.len then
tree_traverse( self.l, point, cbfun )
end
point = point - self.l.len
end
if point <= #self.v then
cbfun( self.v, (point < 1 and 1 or point) )
end
point = point - #self.v
return 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.max
end
function Bbuf:char_at_point_dec( point )
local l, col = treewalk_to( self.textavlroot, point )
if not l then
wv.log('error','point maybe out of range? point=%d max=%d',point or -99,self:end_point())
return string.byte(' ',1)
else
return string.byte(l,col)
end
end
function Bbuf:char_at_point( point )
return string.char( self:char_at_point_dec(point) )
end
function 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 okay
self:_insert_int( self.max + 1, text )
end
function 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 then
return t, point
end
-- 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)
-- end
end
end
function Bbuf:walkFragmentsEOL( point )
local fn = self:walkFragments(point)
local lastt,lasteol
return function()
local t, point
if lasteol and (lasteol+1) <= #lastt then
t = lastt
point = lasteol + 1
lasteol = nil
else
t, point = fn()
end
if t then
local 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 then
lastt = t
lasteol = eol
return t, point, eol
else
return t, point
end
end
end
-- 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
-- end
end
function Bbuf:walkFragmentsEOL_orWidth( point, width )
local fn = self:walkFragmentsEOL(point)
local t, col, eol
local remain = width
return function()
if not t then
t,col,eol = fn()
if not t then
return
end
end
local w = (eol and eol or #t) - col + 1
if w > remain then
local oldcol = col
local nextcol = col+remain
if nextcol > #t then
t = nil
else
col = nextcol
end
return t, oldcol, nextcol
else
local savet = t
t = nil
if eol then
remain = width
end
return savet, col, eol
end
end
end
function Bbuf:searchPointsCharBackward(point)
local ndx, col = self:lcol4point(point)
return function()
local rcl ,rccol = self.lines[ndx], col
end
end
function tree_dump(self,path)
path = path or '[root'
if self.l then
tree_dump( self.l, path .. ', left' )
end
print( 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 then
tree_dump( self.r, path .. ', right' )
end
end
function Bbuf:dump()
tree_dump( self.textavlroot )
end
function Bbuf:new()
return setmetatable({
-- lines = {},
-- textavlroot = Node(''),
max = 0,
cachept = 9.0E99,
operations = {},
unmodified_operation_index = 0,
}, { __index = self })
end
function Bbuf:sub( l, r )
local substrleft = (r-l)
local frags = {}
for line, col in self:walkFragments(l) do
local llen = #line-col+1
if llen > substrleft then
table.insert(frags,line:sub(col,col+substrleft))
break
else
table.insert(frags,line:sub(col))
substrleft = substrleft - llen
end
end
return table.concat(frags)
end
-- returns some point near where the undone operation occurred or something
function Bbuf:undo()
if #self.operations < 1 then
return
end
local last = self.operations[#self.operations]
table.remove( self.operations, #self.operations )
if last.removed then
self:_insert_int( last.l, last.text )
return last.l
elseif last.inserted then
self:_remove_int( last.point, last.point + last.len - 1 )
return last.point
end
end
function Bbuf:isModified()
return self.unmodified_operation_index ~= #self.operations
end
function Bbuf:setUnmodified()
self.unmodified_operation_index = #self.operations
end
function Bbuf:_addoperation( o )
local last = self.operations[#self.operations]
if last and o.inserted and last.inserted and
(o.point == last.point + last.len) then
last.len = last.len + o.len
else
table.insert( self.operations, o )
end
end
function Bbuf:remove( l, r )
l = (l < 1) and 1 or l
r = r or l
r = (r > self.max) and self.max or r
if r < l then -- possibly if self.max = 0
return
end
if self.textavlroot then
self:_addoperation{ removed = true, text = self:sub(l,r), l = l, r = r }
self:_remove_int( l, r )
end
-- return self:_remove_int( l, r )
end
function Bbuf:_remove_int( l, r )
local len = (r-l)+1
tree_delete( self.textavlroot, l, len )
self.max = self.max - len
end
function 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 then
if lndx then
local line = self.lines[lndx]
self.lines[lndx] = line:sub(1,lcol-1) .. line:sub(rcol+1)
self.max = self.max - (r-l+1)
end
else
if r > self:end_point() and lndx == #self.lines then
local line = self.lines[lndx]
self.max = self.max - (#line-lcol+1)
self.lines[lndx] = line:sub(1,lcol-1)
else
error( string.format('multi-line remove not implemented [%d,%d]->[%d,%d]',
lndx or -99,lcol or -99, rndx or -99, rcol or -99))
end
end
self:_invalidateOnEdit()
end
function Bbuf:insert( point, text )
if point > self:end_point() + 1 and self:char_at_point_dec(self:end_point()) ~= 10 then
self:insert( self:end_point()+1, '\n' )
end
if #text > 0 then
self:_addoperation{ inserted = true, point = point, len = #text }
end
return self:_insert_int( point, text )
end
function Bbuf:_insert_int( point, text )
-- if point > self:end_point() then
-- return self:append(text)
-- else
local full = #text
-- wv.log('debug','insert point=%d max=%d #text=%d', point or -99, self.max, #text)
local off = 0
while #text > kIdealStringLength do
self.textavlroot = tree_insert( self.textavlroot, point+off, Node(text:sub(1,kIdealStringLength)) )
off = off + kIdealStringLength
text = text:sub(kIdealStringLength+1)
end
if #text > 0 then
self.textavlroot = tree_insert( self.textavlroot, point+off, Node(text:sub(1)) )
end
self.max = self.max + full
self:_invalidateOnEdit()
-- end
end
function 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' + insert
self:remove(ptl,ptr)
self:insert(ptl,text)
end
-- local subbed = l:gsub('\t',' '):gsub('\n','')
return Bbuf
local function stringsplit(str)
if #str > 1 then
return str:sub(1,1), stringsplit(str:sub(2))
else
return str
end
end
-- 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" hahaha
local function number2string( num, which )
-- print( 'number2string', num, which == vowel and 'vowel' or 'cons' )
if not which then
which = cons
num = tonumber(num) -- protect against receiving number as string, e.g, "8192" rather than 8192
else
if num <= 0 then return '' end
end
num = num - 1
local mod = num % #which
return ( number2string( math.floor(num/#which),
which == cons and vowel or cons ) .. which[mod+1] )
end
local function string2number( str, prior, which )
-- print('string2number', str, prior )
if not str then return end
if not string.find(cstr,str:sub(#str)) then return end -- last letter must be consonant
prior = prior or 0
local check = which or vstr
local other = (check == vstr) and cstr or vstr
local first = str:sub(1,1)
local f = string.find(check,first)
if f then
local acc = (prior*#check) + f
return (#str <= 1) and acc or string2number(str:sub(2), acc, other)
elseif not which then
f = string.find(other,first)
if not f then
return -- invalid string
end
local acc = prior*#other + f
return (#str <= 1) and acc or string2number(str:sub(2),acc,check)
end
end
local function isvalid( str )
local len = #str
if len < 1 then
return false
end
-- last letter must be consonant.
local which = cstr
for i = len, 1, -1 do
if not string.find(which,str:sub(i,i)) then
return false
end
-- must alternate consonant->vowel->consonant->vowel etc.
which = (which == cstr) and vstr or cstr
end
return true
end
--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))
--end
return {
to_s = number2string,
to_i = string2number,
isvalid = isvalid
}
require 'clibs' -- for nylono
local 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-l
For 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-m
editTitle = 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 then
local 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) do
Keys.everybody[i] = v
end
end
-- 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 prefix
Keys.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 then
wv.log('abnorm','NylonOs not found')
end
local Nylon = require 'nylon.core'()
local edopts = {
replaceMark = true
}
local function wait_first_event( cord, handlers )
local rc
local done = false
-- setup event handlers
for nEvent, fHandler in pairs(handlers) do
-- wv.log('debug','set event handler for nEvent=%s',nEvent)
cord.event[nEvent] = function(...)
rc = fHandler(...)
done = true
end
end
while not done do
cord:yield_to_sleep()
end
-- remove event handlers
for k, v in pairs(handlers) do
cord.event[k] = nil
end
return rc
end
local killring = {}
local killring_yankndx = 0
local function killring_add( t )
table.insert( killring, t )
killring_yankndx = #killring
if NylonOs then
NylonOs.Static.setclipboard(killring[#killring])
end
end
local function killring_append(t)
killring[#killring] = killring[#killring] .. t
if NylonOs then
NylonOs.Static.setclipboard(killring[#killring])
end
end
local function killring_getyanktext()
if NylonOs then
return NylonOs.Static.getclipboard()
else
return killring[killring_yankndx]
end
end
local function watch_cursor_pos( ptTopLeft, thePoint, wdim )
local charsInto = thePoint - ptTopLeft
local ptRow, ptCol
local drow = 0
local dcol = 0
local charsDrawn = 0
return function( l, col, eol )
local toDraw = eol and (eol - col + 1) or (#l-col+1) -- was #l
if (charsDrawn + toDraw) > charsInto then
return (dcol + charsInto-charsDrawn), drow
else
charsDrawn = charsDrawn + toDraw
end
if eol then
dcol = 0
drow = drow + 1
else
dcol = dcol + toDraw
end
end
end
local 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.wdim
local w = env.w
env.on = env.on or {} -- ensure table exists, to make test easier
env.report = env.report or function() end
opt.plug = opt.plug or {}
local wordWrap = false
local walkFun = wordWrap and buffer.walkFragmentsEOL_orWidth or buffer.walkFragmentsEOL
-- add refresh every 2s
if true then
local refresh_cord = Nylon.cord('refreshedit', function(refcord)
while true do
refcord:sleep(10)
cord.event.refresh()
end
end )
end
local function cursorpos( ptTopLeft, thePoint, wdim )
local f = watch_cursor_pos(ptTopLeft,thePoint,wdim)
for l, col, eol in walkFun(buffer, ptTopLeft, wdim.w) do
local cx, cy = f( l, col, eol )
if cx then
return cx, cy
end
end
end
local ptTopLeft = 1
local thePoint = 1
local theMark
local isearch_fstring = nil
local wasding -- true when last event was a "ding", should prevent status
-- update from overriding ding message
local function get_marked()
local markbeg = theMark and (thePoint < theMark) and thePoint or theMark
local markend = theMark and (theMark < thePoint) and thePoint or theMark
return markbeg, markend
end
local function get_line_marked( drawPoint, drawPointAfterLine )
local markbeg, markend = get_marked()
local lineMarkBeg, lineMarkEnd
if markbeg and drawPointAfterLine > markbeg and drawPoint < markend then
lineMarkBeg = drawPoint >= markbeg and 1 or (markbeg-drawPoint+1)
lineMarkEnd = drawPointAfterLine <= markend and (drawPointAfterLine-drawPoint) or (markend - drawPoint)
end
return lineMarkBeg, lineMarkEnd
end
local function pdcur_draw_window( wdim )
local charsInto = thePoint - ptTopLeft
local c = opt and opt.theme and opt.theme.text and Theme[opt.theme.text] or Theme.normal
w:attron( c )
local function draw_line( drow, l, markBeg, markEnd )
w:move(drow+wdim.y,wdim.x)
local s, e
if isearch_fstring and #isearch_fstring > 0 then
if isearch_fstring:find '[A-Z]' then
s,e = l:find(isearch_fstring)
else
s,e = l:lower():find(isearch_fstring)
end
end
local ndrawn = #l
if s then
local 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) )
else
if opt.plug.drawline then
ndrawn = opt.plug.drawline( w, wdim.x, drow+wdim.y, l, markBeg, markEnd ) or ndrawn
else
if not markBeg then
w:addstr((#l > wdim.w) and l:sub(1,wdim.w) or l)
else
local endPoint = markEnd > wdim.w and wdim.w or markEnd
w:addstr( l:sub(1,markBeg-1) )
local function drawMarked() w:addstr( l:sub(markBeg,endPoint) ) end
if opt and opt.theme and opt.theme.text == 'inverse' then
-- 160518 bah, this is not working as i expect
w:attroff(c)
Theme.with.classicmenu( w, drawMarked )
w:attron(c)
else
-- w:attroff(c)
Theme.with.inverse( w, drawMarked )
-- w:attron(c)
end
w:addstr( l:sub(endPoint+1,wdim.w) )
end
end
end
if 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)) )
end
end
local drow = 0
local linefrags = {}
local drawPoint = ptTopLeft
for 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 then
table.insert( linefrags, l:sub(col,eol-1) )
local wholeLine = table.concat(linefrags)
local drawPointAfterLine = drawPoint + #wholeLine + 1
local lineMarkBeg, lineMarkEnd = get_line_marked( drawPoint, drawPointAfterLine )
draw_line( drow, wholeLine, lineMarkBeg, lineMarkEnd )
drawPoint = drawPointAfterLine
drow = drow + 1
linefrags = {}
elseif col > 1 then
table.insert( linefrags, l:sub(col) )
else
table.insert( linefrags, l )
end
if drow >= wdim.h then
break
end
end
if #linefrags > 0 then
draw_line( drow, table.concat(linefrags) )
drow = drow + 1
end
while drow < wdim.h do
w:mvaddstr(drow+wdim.x,wdim.y,string.rep(' ',wdim.w))
drow = drow + 1
end
local 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) )
-- end
w:attroff(c)
if isearch_fstring then
env.report('isearch','i-search: ' .. isearch_fstring)
else
--local ndx,col = buffer:lcol4point(thePoint)
if not wasding then
env.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))
end
end
end
local keybindings = (require 'pls-keys').edit
local function ding(msg,p,...)
Pdcurses.Static.beep()
if msg then
wasding = true
env.report('oops',p and string.format(msg,p,...) or msg)
end
end
local function report(...)
env.report(...)
wasding = true
end
local 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 then
thePoint = nextpoint
local cx, cy = cursorpos(ptTopLeft,thePoint,wdim)
if cy >= wdim.h then -- ran off screen
local adjust = math.floor(cy - (wdim.h / 3)) -- put next point at top 1/3 of screen
wv.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 then
ptTopLeft = st
end
end
else
ding 'i-search failed'
end
cord.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] then
cord.event[ keybindings[k] ]()
elseif type(k) == 'number' then
if k < 256 and k >= 32 then -- don't insert meta chars and ctrl chars
if isearch_fstring then
isearch_fstring = isearch_fstring .. string.char(k)
next_isearch_forward( thePoint )
cord.event.refresh()
else
cord.event.text(k)
end
elseif k == 27 then -- don't insert meta chars and ctrl chars
if isearch_fstring then
isearch_fstring = nil
cord.event.refresh()
else
return k
end
else
return k
end
end
end
local baseline = 1
local ccol = 1
local crow = 1
local function cursor_line()
return buffer[crow+baseline-1] or ''
end
local function char_at_point()
return buffer:char_at_point(thePoint)
end
local function insert_at_point(t)
-- wv.log('debug','insert at pt=%d str=[%s]',thePoint, t)
buffer:insert( thePoint, t )
thePoint = thePoint + #t
if env.on.modified then
env.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 + #t
end
function cord.event.getPoint(ret)
ret(thePoint)
end
function cord.event.replace_marked(cbfun)
if cbfun then
local mb, me = get_marked()
if mb and me > mb then
local rc = cbfun( buffer:sub(mb,me-1) )
if rc ~= false then -- false return code means abort operation
buffer:remove(mb,me-1)
buffer:insert(mb,rc or '')
theMark = nil
cord.event.refresh()
end
else
cbfun() -- callback with no params to indicate no marked region
end
end
end
function cord.event.kill_buffer()
if env.on.kill_buffer then
env.on.kill_buffer()
else
ding 'kill-buffer not implemented'
end
end
function cord.event.undo()
local undopoint = buffer:undo()
if undopoint then
thePoint = undopoint
if env.on.modified then
env.on.modified()
end
cord.event.refresh()
else
ding 'No further undo information'
end
end
function 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 altogether
if isearch_fstring then -- search for next match
next_isearch_forward( thePoint + 1 )
else -- start new search
isearch_fstring = ''
end
cord.event.refresh()
end
function cord.event.ctrlg()
isearch_fstring = nil
theMark = nil
cord.event.refresh()
end
function cord.event.isearch_backward()
ding 'isearch_backward not implemented'
end
function 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 )
end
function cord.event.query_citations()
local _ = env.on.queryCitations and env.on.queryCitations()
end
function cord.event.save_buffer()
if env.on.save then
report('major','Saving buffer')
local one = buffer:asOneString()
if true then -- create temp record to save my sanity maybe
local f = io.open(string.format('/tmp/pls-r-%f.txt',Nylon.uptime()),"w")
f:write(one)
f:close()
end
env.on.save( one )
report('major','Buffer saved')
else
report('warn','No buffer save handler')
end
end
function cord.event.insert_at_point( t )
insert_at_point( t )
cord.event.refresh()
end
local function do_yank( special )
if env.on.yank and env.on.yank( special ) then
return
end
local yanktext = killring_getyanktext()
insert_at_point( yanktext )
cord.event.refresh()
end
function cord.event.yank()
do_yank()
end
function cord.event.special_yank() -- typically allows paste of html
do_yank(true)
end
function cord.event.text(t)
if t < 256 then
local theString = string.char(t)
if theMark and edopts.replaceMark then
local markbeg = get_marked()
cord.event.replace_marked( function()
thePoint = markbeg + 1
return theString
end)
else
insert_at_point(theString)
end
save_col = true
else
ding("unrecognized key=%d",t)
end
cord.event.refresh()
end
function cord.event.indent_for_tab_command()
insert_at_point(' ')
cord.event.refresh()
end
function cord.event.delete_char()
buffer:remove(thePoint)
cord.event.refresh()
end
local lastkill
local function killring_addorappend(t)
wv.log('debug','killring_addorappend lastkill=%s',lastkill)
if lastkill then
killring_append(t)
else
killring_add(t)
end
end
function cord.event.kill_line()
if buffer:char_at_point(thePoint) == '\n' then
buffer:remove(thePoint)
killring_addorappend '\n'
else
local eolPoint = buffer:end_of_line(thePoint)
if buffer:char_at_point(eolPoint) == '\n' then
eolPoint = eolPoint - 1
end
if eolPoint ~= thePoint then
local deleted = buffer:sub( thePoint, eolPoint )
buffer:remove( thePoint, eolPoint )
killring_addorappend( deleted )
end
end
cord.event.refresh_kill()
end
function cord.event.escape()
if isearch_fstring then
isearch_fstring = nil
cord.event.refresh()
end
if opt.oneLine then
wv.log 'oneLine edit got escape (cancel)'
local _ = env.on.cancelled and env.on.cancelled()
end
end
function cord.event.newline()
if opt.oneLine then
if env.on.save then
env.on.save( buffer:asOneString() )
end
else
buffer:insert(thePoint,'\n')
thePoint = thePoint+1
local cx, cy = cursorpos( ptTopLeft, thePoint, wdim )
if cy and (cy + 1 > wdim.h) then
local nextline = buffer:search_forward_to( ptTopLeft, '\n' )
if nextline then
ptTopLeft = nextline + 1
end
end
cord.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()
end
function cord.event.delete_backward_char()
if isearch_fstring then
isearch_fstring = isearch_fstring:sub(1,#isearch_fstring-1)
else
if thePoint > 1 then
thePoint = thePoint - 1
buffer:remove(thePoint)
else
ding '(delete back) beginning of buffer'
end
end
cord.event.refresh()
end
local last_col
local save_col
local function _go_if(newPoint)
if newPoint and newPoint ~= thePoint then
thePoint = newPoint
save_col = true
return newPoint
end
end
local function lfn_backward_word()
_go_if( buffer:backward_word(thePoint) )
cord.event.refresh()
end
cord.event.backward_word = lfn_backward_word
local function lfn_delete_word()
local endword = buffer:forward_word(thePoint) or (buffer:end_point()+1)
if endword and endword > thePoint then
local removed = buffer:sub(thePoint,endword-1)
killring_addorappend(removed)
buffer:remove(thePoint,endword-1)
cord.event.refresh_kill()
else
ding 'No more words (and no more promises)'
end
end
cord.event.delete_word = lfn_delete_word
function 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()
end
local _ = env.on.contextmenu and env.on.contextmenu(w,{ consume_word = consume_word })
cord.event.refresh()
end
local function restore_col()
-- wv.log('debug','last_col=%s',tostring(last_col))
if last_col then
local 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 then
thePoint = thePoint + last_col
else
thePoint = eolpoint
end
end
end
function cord.event.set_mark_command()
theMark = thePoint
cord.event.refresh()
end
local function _copy_marked_and(fn)
local mb, me = get_marked()
if me and me > mb then
local removed = buffer:sub(mb,me-1)
killring_addorappend(removed)
if fn then
fn(mb,me)
end
theMark = nil
cord.event.refresh()
else
ding 'No selection - use [C-x Spc] to mark'
end
end
function cord.event.kill_ring_save()
_copy_marked_and()
end
function cord.event.kill_region()
_copy_marked_and( function(mb,me)
buffer:remove(mb,me-1)
thePoint = mb
end)
end
function cord.event.next_line()
local point = ptTopLeft
local nextLineFromTop
local cx, cy
local thePointStart = thePoint
local curfun = watch_cursor_pos(ptTopLeft, thePoint, wdim)
for l, col, nl in walkFun(buffer,ptTopLeft,wdim.w) do
if not cx then
cx, cy = curfun( l, col, nl )
end
local 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 + toDraw
if nl then
if not nextLineFromTop then
nextLineFromTop = point + 1
-- wv.log('debug','got nextLineFromTop=%d',nextLineFromTop)
end
if point > thePoint then
if wordWrap and point >= (thePoint + wdim.w) then
thePoint = thePoint + wdim.w
break -- don't restore_col, for now
else
thePoint = point
end
wv.log('debug','point=%d cy=%d nextLineFromTop=%d',point,cy,nextLineFromTop)
assert(cy) -- should have found cursor
if cy + 1 >= wdim.h then
ptTopLeft = nextLineFromTop
end
-- if not wordWrap then
restore_col()
-- end
break
end
end
end
if thePoint == thePointStart then
thePoint = buffer:end_point() + 2
end
cord.event.refresh()
end
function cord.event.previous_line()
if thePoint == 1 then
ding 'no previous line'
return
end
nextpoint = 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 = 1
restore_col()
else
if nextpoint > 1 then
local bol = buffer:beginning_of_line(nextpoint-1)
if bol < ptTopLeft then
ptTopLeft = bol
end
thePoint = bol
restore_col()
else
ding 'no previous line'
end
end
cord.event.refresh()
end
local function _backward_char()
if thePoint > 1 then
thePoint = thePoint - 1
save_col = true
end
end
function cord.event.backward_char()
if thePoint > 1 then
_backward_char()
cord.event.refresh()
else
ding 'At beginning of buffer'
end
end
local function _forward_char()
local ep = buffer:end_point()
if thePoint <= ep then
thePoint = thePoint + 1
save_col = true
elseif 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' then
thePoint = thePoint + 1
save_col = true
end
else
ding 'End of buffer'
end
end
function cord.event.forward_char()
_forward_char()
cord.event.refresh()
end
function 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 then
w = w:sub( thePoint-bow+1 )
bow = thePoint
end
if w then
buffer:replace( bow, bow, w:sub(1,1):upper() )
buffer:replace( bow+1, eow, w:sub(2):lower() )
end
cord.event.forward_word()
end
function cord.event.upcase_word()
local w, bow, eow = buffer:word_at_point(thePoint)
if w then
buffer:replace( bow, eow, w:upper() )
end
cord.event.forward_word()
end
function cord.event.downcase_word()
local w, bow, eow = buffer:word_at_point(thePoint)
if w then
buffer:replace( bow, eow, w:lower() )
end
cord.event.forward_word()
end
local function _forward_to(pat)
return _go_if( buffer:search_forward_to(thePoint,pat) )
end
local function _forward_past(pat)
return _go_if( buffer:search_forward_past(thePoint,pat) )
end
local function _backward_to(pat)
return _go_if(buffer:search_backward_to(thePoint,pat))
end
local function _backward_past(pat)
return _go_if(buffer:search_backward_past(thePoint,pat))
end
function cord.event.move_beginning_of_line()
_go_if( buffer:beginning_of_line(thePoint) )
cord.event.refresh()
end
function 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() then
eol = eol + 1
end
_go_if( eol )
cord.event.refresh()
end
function 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()
end
function 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 then
thePoint = prior
restore_col()
local st = buffer:backward_lines(ptTopLeft,wdim.h)
if st then ptTopLeft = st end
else
ding()
end
cord.event.refresh()
end
function 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 then
thePoint = nextpoint
restore_col()
local st = buffer:forward_lines(ptTopLeft,wdim.h)
if st then
ptTopLeft = st
end
end
local e = NylonSysCore.uptime()
wv.log('debug','time to page down=%f',(e-s))
cord.event.refresh()
end
function cord.event.beginning_of_buffer()
ptTopLeft = 1
thePoint = 1
save_col = true
cord.event.refresh()
end
function 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 then
local prevline = buffer:backward_lines(top-1,1)
wv.log('debug','eob top=%s prevl=%s',top,prevline)
if prevline then
ptTopLeft = prevline
else
ptTopLeft = 1
end
else
ptTopLeft = 1
end
thePoint = ep + (buffer:char_at_point_dec(buffer:end_point())==10 and 1 or 2)
save_col = true
local e = NylonSysCore.uptime()
wv.log('debug','time for end_of_buffer=%f ptl=%d',(e-b),ptTopLeft)
cord.event.refresh()
end
local function do_redraw()
pdcur_draw_window( wdim )
local curx,cury = cursorpos(ptTopLeft,thePoint,wdim)
if curx then
w:move(wdim.y+cury, wdim.x+curx)
else
local ep = buffer:end_point()
if thePoint > ep then
local curx, cury = cursorpos( ptTopLeft, ep, wdim)
cury = cury or 0
curx = curx or 0
if (thePoint > ep+1) or buffer:char_at_point(ep)=='\n' then
w:move( wdim.y+cury+1, wdim.x )
else
w:move( wdim.y+cury, wdim.x+curx+1 )
end
end
-- 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 ))
end
if save_col then
last_col = curx
save_col = false
end
end
--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 then
ondone()
end
end
while true do
do_redraw()
w:refresh()
local waskill = false
wasding = false
wait_first_event( cord, {
refresh=function( force )
if force then
w:redraw()
end
end,
refresh_kill=function()
waskill = true
end,
})
-- wv.log('debug','cord got event, lastkill=%s waskill=%s',lastkill,waskill)
lastkill = waskill
end
end
return {
entryfn_edit = entryfn_edit
}
local function Logwrite(...)
end
local function gen_search( text )
Logwrite "Scratchpad.search text='#{text}'"
local pretext
repeat
pretext = text
text = text:gsub("%(%(", "( (")
text = text:gsub("%)%)", ") )")
until text == pretext
print(text)
local qstr = ''
local do_and
local do_or
local do_not
local wordlist = {}
local spcndx = 1
while true do
local b,e = text:find('%s+',spcndx)
if b then
table.insert(wordlist,text:sub(spcndx,b-1))
spcndx = e+1
else
table.insert(wordlist,text:sub(spcndx))
break
end
end
for _,v in ipairs(wordlist) do
-- print(string.format('"%s"',v))
end
--print ' ------------'
local function consume_logic()
qstr = do_and and (qstr .. " and") or qstr
qstr = do_or and (qstr .. " or") or qstr
do_and = true
do_or = false
end
--[[--
if word =~ /^\((.*)/ then
word = $1
consume_logic.call()
qstr += ' ( '
do_and = false
next if word.length < 1
end
--]]--
for _,word in ipairs(wordlist) do
local b,e,match = word:find('^%((%a*)')
if b then
-- print('openparen',match)
consume_logic()
qstr = qstr .. ' ( '
do_and = false
word = match
end
if #word > 0 then
if word == 'and' then
;
elseif word == 'not' then
do_not = true
elseif word == 'or' then
do_or = true
do_and = false
else
local doit = true
local closer = ''
local b,e,match = word:find('(%a*)%)$' )
if b then
word = match
closer = ' ) '
if #word < 1 then
qstr = qstr .. closer
doit = false
end
end
if doit then
consume_logic()
if do_not then
qstr = qstr .. " (detail not like '%" .. word .. "%' and title not like '%" .. word .. "%')" .. closer
do_not = false
else
qstr = qstr .. " (detail like '%" .. word .. "%' or title like '%" .. word .. "%')" .. closer
end
end
end -- not and, not, or or
end --, #word > 0
end -- for -loop
-- print( 'qstr:', qstr )
return qstr
end
--gen_search '(((mattm or miller) and (not discount)) or rcm) and reward'
return {
megasearch = gen_search
}
--[[--
if word == 'and' then
true # 'and' is assumed if not otherwise spec'ed
elsif word == 'not' then
do_not = true
elsif word == 'or' then
do_or = true
do_and = false
else
closer = ''
if word =~ /(.*)\)$/ then
word = $1
closer = ' ) '
if word.length < 1 then
qstr += closer
next
end
end
consume_logic.call()
if do_not then
qstr += " (details not like '%#{word}%' and title not like '%#{word}%')" + closer
do_not = false
else
qstr += " (details like '%#{word}%' or title like '%#{word}%')" + closer
end
end
}
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)
end
function 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)
-- end
local point = startPoint
if #pat == 1 or (#pat==2 and pat:sub(1,1) == '%') then
while not buffer:char_at_point(point):find(pat) do
if point < buffer:end_point() then
point = point + 1
else
return
end
end
return point
else
-- 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() do
local 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) then
return i
end
end
end
end
function Buffer.search_forward_past( buffer, startPoint, pat )
local point = startPoint
if #pat == 1 or (#pat==2 and pat:sub(1,1) == '%') then
while buffer:char_at_point(point):find(pat) and
point <= buffer:end_point() do
point = point + 1
end
return point <= buffer:end_point() and point
else
-- 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() do
local sub = buffer:sub( i, i+(#pat*2) )
if not sub:find(pat) then
return point
end
end
end
end
function Buffer.search_backward_to( buffer, startPoint, pat )
local point = startPoint
if #pat == 1 then
local c = string.byte(pat,1)
while buffer:char_at_point_dec(point) ~= c do
if point <= 1 then
return
end
point = point - 1
end
return point
elseif #pat==2 and pat:sub(1,1) == '%' then
while not buffer:char_at_point(point):find(pat) do
if point <= 1 then
return
end
point = point - 1
end
return point
else
error 'not implemented, multi-char search forward'
end
end
function Buffer.search_backward_past( buffer, startPoint, pat )
local point = startPoint
if #pat == 1 or (#pat==2 and pat:sub(1,1) == '%') then
while buffer:char_at_point(point):find(pat) do
if point <= 1 then
return
end
point = point - 1
end
return point
else
error 'not implemented, multi-char search forward'
end
end
function Buffer:beginning_of_line( startPoint )
local point = (self:char_at_point_dec(startPoint) == NEWLINE and startPoint > 1) and (startPoint-1) or startPoint
local found = self:search_backward_to(point,'\n')
if found then
return (found + 1)
else
return 1
end
end
function Buffer.end_of_line( buffer, startPoint )
local nexteol = buffer:search_forward_to(startPoint, '\n')
return nexteol and nexteol or buffer:end_point() + 1
end
--- 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 point
function Buffer.backward_lines( buffer, startPoint, nlines )
local point
if startPoint > buffer:end_point() +1 then
point = buffer:end_point()
nlines = nlines - 1
else
point = startPoint
end
local linestart = point
while buffer:char_at_point_dec(point) == NEWLINE do
if point ~= linestart then
if nlines > 0 then
nlines = nlines - 1
else
return point
end
end
point = point - 1
end
local lastfound
while nlines >= 0 do
local found = Buffer.search_backward_to( buffer, point, '\n')
if not found or found == point then
break
end
lastfound = found
if found <= 1 then
break
end
point = found
while (buffer:char_at_point_dec(point) == NEWLINE) and nlines >= 0 and point > 1 do
point = point - 1
nlines = nlines - 1
end
end
if lastfound then
return lastfound + 1
end
end
function Buffer.forward_word(buffer,point)
if not buffer:char_at_point(point):find('%w') then
local p = buffer:search_forward_to(point,'%w')
point = p or point
end
return buffer:search_forward_past( point, '%w' )
end
function Buffer:backward_word(point)
if point <= 2 then
return 1
end
if self:char_at_point(point):find('%w') then
if self:char_at_point(point-1):find('%w') then
local justBeforeWord = self:search_backward_past(point-1, '%w')
return justBeforeWord and justBeforeWord + 1 or 1
else
-- we are at the first letter in a word.
end
end
local lastLetterOfPreviousWord = self:search_backward_to( point-1, '%w' )
if not lastLetterOfPreviousWord or lastLetterOfPreviousWord < 2 then
return 1 -- no more words, beginning of buffer
end
local bpw = self:search_backward_past( lastLetterOfPreviousWord-1, '%w' )
return bpw and (bpw+1) or 1
end
function 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 then
return self:sub(beginningOfWord,endOfWord-1), beginningOfWord, endOfWord-1
end
end
function Buffer.asOneString( buffer )
local all = {}
for l in buffer:walkFragments(1) do
table.insert(all,l)
end
return table.concat(all)
end
function Buffer.forward_lines( buffer, startPoint, nlines )
local point = startPoint
for l, col, nl in buffer:walkFragmentsEOL(startPoint) do
if nl then
point = point + (nl-col) + 1
if nlines <= 1 then
return point
else
nlines = nlines - 1
end
else
if not l then
return (point ~= startPoint) and point
else
point = point + (#l - col) + 1
end
end
end
return point
end
function Buffer:insertFileAtPoint( point, fname )
local f = io.open(fname,'r')
-- local buffer = Buffer:new()
if f then
for l in f:lines() do
local fix = l:gsub('\t',' ') .. '\n'
self:insert( point, fix )
point = point + #fix
end
f:close()
end
end
local function buffer_openFile( fname )
local f = io.open(fname,'r')
if f then
local buffer = Buffer:new()
for l in f:lines() do
buffer:append(l:gsub('\t',' ') .. '\n')
end
f:close()
end
return buffer
end
local function buffer_withText(t)
local buffer = Buffer:new()
if t then
buffer:append(t)
end
return buffer
end
return {
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 ids
local found = {}
local srchstart = 1
while true do
local s, e, plsid = text:find('%s:(%w+)', srchstart )
if not s then
break
end
wv.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 etc
table.insert( found, { s, e, plsid } )
srchstart = e + 1
end
if #found > 0 then
end
-- @todo: compare list of fonud citations to current database table of citations.
end
return on_save_scan_citations
local ARGS = { ... }
if not string.find(arg[-1],'lua523r') then
table.move(package.searchers, 1, #package.searchers, 2)
package.searchers[1] = function(...)
print('searching... ', ...)
return
end
end
require '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 services
package.path = '../nylabus/?.lua;' .. package.path
glOpts = {
space = 'ncr'
}
if ARGS[1] then
glOpts.space = ARGS[1]
end
local 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 debugging
local Winman = require 'pls-winman'
local Numword = require 'pls-numword'
local Service
local ok, err = pcall( function() Service = require 'nylaservice' end )
if not ok then
wv.log('debug','could not load nylaservice, e=%s', err )
end
local gIdUser = 1 -- dmattp, default
local cord_app -- this should be moved down to creation point by removing single use in make_editor()
if arg[1] then
recid = tonumber(arg[1])
end
local dbname = ( 'space/' .. (glOpts.space) .. '/notes.db' )
-- print('db=', dbname)
local db = Sqlite:new( dbname )
if not db then
wv.log('error', 'db.a01 could not open name=%s', dbname)
error 'no database'
else
wv.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 start
local lastnote = db:selectOne('select ROWID from note where dt_modified=(select MAX(dt_modified) from note)')
if not lastnote then
local 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)')
end
local recid = lastnote.ROWID
local Buffer = require 'pls-buffer'
local keyhandler
local curses_started
local function start_curses()
if curses_started then
Pdcurses.Static.refresh()
else
Pdcurses.Static.noecho()
Pdcurses.Static.start_color()
Pdcurses.Static.raw()
-- when mouse enabled, generates Pdcurses.key.mouse
-- Pdcurses.Static.mouse_on(0x1fffffff) -- ALL_MOUSE_EVENTS
curses_started = true
end
end
local function end_curses()
if curses_started then -- and (not Pdcurses.Static.isendwin()) then
wv.log('debug', 'stopping curses end_curses()')
Pdcurses.Static.endwin()
end
end
local function curses_init()
local screen = Pdcurses.Static.initscr()
start_curses()
local ncols, nrows = screen:getmaxx(), screen:getmaxy()
return { screen = screen, ncols = ncols, nrows = nrows }
end
local 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()
end
local 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()
end
local function WindowDim( x, y, w, h )
return { x = x, y = y, w = w, h = h }
end
function centerwindow(env,h,w)
local x = (env.curses.ncols - w)/2
local y = (env.curses.nrows - h)/2
return h, w, y, x
end
local function withFocusAttrib( mw, fun )
Theme.with.focuswinborder( Winman.isFocused(mw) and mw.win, fun )
end
function ui_yesno(cord,env,prompt,default)
local wid = (#prompt)+2
wid = wid < 14 and 14 or wid
local 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()
end
default = default ~= nil and default or true
local mw
local function drawme()
withFocusAttrib( mw, function() w:stdbox_() end )
w:mvaddstr(1,1,prompt)
showopt(default)
end
local removeme
local 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' then
wakefun(false)
elseif s == 'y' or s == 'Y' then
wakefun(true)
elseif k == 27 then -- escape
wakefun(false)
elseif default and k == 10 then -- newline
wakefun(default)
end
end,
resized = function()
drawme()
end} }
drawme()
removeme = Winman.push_modal(mw)
end)
-- flash yes/no to provide feedback
showopt( yes )
w:refresh()
Nylon.self:sleep(0.15)
removeme()
return yes
end
function ui_oneline(env,prompt,opt)
opt = opt or {}
local cord = Nylon.self
local 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()
end
local 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 then
if true then
editcord.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()
end
end
local 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 )
end
function e.on.cancelled()
wv.log('debug','cancelled oneline text edit')
wakefun()
end
end)
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 edited
end
-- 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
--- end
local function picklist( title, query_sql, ... )
local on_callbacks = {}
if type(title) == 'table' then
if title.on then
on_callbacks = title.on
end
title = title.title
end
local records = {}
local function run_db_query(...)
local ok, err = pcall( function(...)
records = db:selectMany( query_sql, ... )
end, ...)
if not ok then
wv.log('error','error running SQL query=%s', tostring(err))
end
end
run_db_query(...)
local width = math.floor(env.curses.ncols/3)
local height = math.floor(env.curses.nrows*2/3)
if height > #records + 2 then
height = #records + 2
end
local 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 0
local theTop = 0
local mw
local sstring
local function draw()
if (dim.w < 2) or (dim.h <2) then
return
end
local maxtextlen = dim.w - 2
withFocusAttrib( 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 do
local drawingRecord = row + theTop
local r = records[drawingRecord]
if not r then break end
local 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,e
if sstring then
s,e = text:lower():find(sstring)
end
if s then
w: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)
else
w:mvaddstr(row,1,text)
end
end )
if #text < maxtextlen then
w:addstr( string.rep(' ',maxtextlen-#text) )
end
end
if sstring then
Theme.with.classicmenu( w, function()
w:mvaddstr(dim.h-1,4,'[i-search: ' .. sstring .. ' ]')
end)
end
w:refresh()
end
local removewin
local cord = Nylon.cord('picklist',
function(cord,args)
function cord.event.beginning_of_buffer()
theTop = 0
active = 1
draw()
end
function cord.event.end_of_buffer()
active = #records
theTop = active - (dim.h-2)
draw()
end
function cord.event.scroll_up_command()
active = active + (dim.h-2)
if active > #records then
active = #records
end
if active > theTop + (dim.h-2) then
theTop = active - (dim.h-2)
end
draw()
end
function cord.event.next_line()
if active < #records then
active = active + 1
if active > theTop + (dim.h-2) then
theTop = active - (dim.h-2)
end
draw()
end
end
function cord.event.scroll_down_command()
active = active - (dim.h-2)
if active < 1 then
active = 1
end
if (active-1) < theTop then
theTop = (active-1)
end
draw()
end
function cord.event.previous_line()
if active > 1 then
active = active - 1
if (active-1) < theTop then
theTop = (active-1)
end
draw()
end
end
local done
function cord.event.kill_buffer()
done = true
removewin()
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 mechanism
while true do
cord: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 window
draw()
end
end, { ... } )
local function isearch_search_from(sstring,from,dir)
dir = dir or 1
local nrecords = #records
if nrecords < 1 then wv.log('error','no search records?'); return end
for 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) or
string.find(Numword.to_s(records[i].ROWID):lower(),sstring) then
active = i
return
end
end
-- ding()
end
local function isearch_find_next(str,dir)
dir = dir or 1
isearch_search_from(sstring,active+dir,dir)
draw()
end
local function isearch_on_string_update(sstring)
isearch_search_from(sstring,active)
draw()
end
mw = 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) then
cord.event[k]()
elseif k == 10 then -- newline
local _ = records and records[active] and cord_app.event.OpenRecord( records[active].ROWID )
elseif k == 46 then -- '.'
if not sstring then
cord.event.kill_buffer()
local _ = records and records[active] and cord_app.event.OpenRecord( records[active].ROWID )
else
sstring = sstring .. string.char(k)
isearch_on_string_update( sstring )
end
elseif k == 8 and sstring then
sstring = sstring:sub(1,#sstring-1)
isearch_on_string_update(sstring)
elseif k == 19 or k == 18 then -- C-s
if sstring then
isearch_find_next(sstring, (k == 19) and 1 or -1)-- go to next after current
else
sstring = '' -- start isearch forward
draw()
end
else
if sstring then
if type(k) == 'number' and k >= 32 and k <= 128 then
sstring = sstring .. string.char(k)
isearch_on_string_update(sstring)
elseif k == 27 or k == 7 then -- ESC or Ctrl+g
sstring = nil
draw()
end
else
return k
end
end
end } }
removewin = Winman.push( mw )
end
local function args_concat( ... )
local t = { ... }
return table.concat(t)
end
local function menuOptions( keys )
menutext = {}
for _,v in pairs(keys) do
local 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 ) )
end
return menutext
end
local 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()
end
return menuwin, draw
end
local function MakeMenuVert( keys )
local menuText = menuOptions(keys)
local maxlen = 0
for i, v in ipairs(menuText) do
v = args_concat(' ', v, ' ')
menuText[i] = v
maxlen = (#v > maxlen) and #v or maxlen
end
wv.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))
end
end)
menuwin:refresh()
end
return menuwin, draw
end
local function menuedFunctions( invokingCord, ftab, opt )
local tOptions = {}
for k, _ in pairs(ftab) do
table.insert( tOptions, k )
end
local menuwin
local drawmenu
if opt and opt.vert then
menuwin, drawmenu = MakeMenuVert(tOptions)
else
menuwin, drawmenu = MakeMenu(tOptions)
end
local grabbed = {}
for k, v in pairs(ftab) do
local optkey
if type(k) == 'table' then
optkey = string.byte(k[1],1)
else
optkey = string.byte(string.lower(string.sub(k,1,1)),1)
end
grabbed[ optkey ] = v
end
local 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)
end
end )
winman_remove()
if grabbed[key] then
wv.log('debug','app grab key 2=%s',key)
grabbed[key]()
end
end
local function getrecid(recid)
recid = recid and (type(recid)=='number' or recid:find('^%d')) and recid or Numword.to_i(recid)
return recid
end
local function FindRecordById(recid1)
local altrecid
if type(recid1) == 'table' then
altrecid = recid1[2]
recid1 = recid1[1]
wv.log('debug','got rec/alt=%s/%s',recid1, altrecid)
end
local recid = getrecid(recid1)
wv.log('debug','getrecid=%s recid1=%s',recid, recid1)
if not recid and altrecid then
recid = getrecid(altrecid)
end
if not recid then
if not recid1 then
recid = ui_oneline(env,'Enter record id', {w=30})
if not recid then -- cancelled
return
end
recid1 = recid
local r2 = getrecid(recid)
wv.log('debug','getrecid oneline=%s r2=%s',recid, r2)
recid = r2
end
end
local function selectBestMatch(set,id)
id = id:lower()
local strmatch = ':' .. id .. '%W'
local best
for _, r in pairs(set) do
local 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 = r
else
best = 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!)
end
end
return best or set[1]
end
local record
if recid then -- check for keyword
record = db:selectOne('select rowid, title, detail, dt_created, dt_modified from note where rowid=?', tonumber(recid) )
end
if record then
return record, false, recid1
else
wv.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 match
records = 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, recid1
end
end
local 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] then
wv.log('abnorm','record[%s] already open', Numword.to_s(record.ROWID))
local win = gOpenRecords[record.ROWID]
Winman.this_window_to_primary(win)
return
end
local nrows, ncols = env.curses.nrows, env.curses.ncols
local dim = WindowDim( 0, 0, math.floor(ncols/2), env.curses.nrows )
local currentRev = 0
local function setCurrentRev()
if record.ROWID then
local found = db:selectOne('SELECT COUNT(*) as rev from patch where id_note=?',record.ROWID)
currentRev = found and tonumber(found.rev) or currentRev
end
end
setCurrentRev()
------------------------------------------------------------------
---- Border
-- local wb = Pdcurses.Window(h+2,w+2,y-1,x-1)
-- wb:stdbox_()
-- wb:refresh()
------------------------------------------------------------------
---- Edit window
local win = Pdcurses.Window( dim.h, dim.w, dim.y, dim.x )
local mw = { win = win } -- Winman managed window, set at the bottom of this function
local function drawbox()
withFocusAttrib( mw, function()
-- win:stdbox_()
win:box( tonumber(VLINE), HLINE ) --5VLINE, '-' )
end )
end
local bbuffer
------------------------------------------------------------------
---- Title
local function settitle()
local recword
if record.ROWID then
recword = Numword.to_s(record.ROWID)
else
recword = "*NEW*"
end
local 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 )
end
settitle()
local cord = Nylon.self
local function editTitle()
wv.log('debug','got call to editTitle')
local starttext = record.title == 'new record' and '' or record.title
local title = ui_oneline(env,'Edit Title', { text=starttext })
if title then
record.title = title
settitle()
win:refresh()
if record.ROWID then
db:retryexec('update note set title=?,dt_modified=DATETIME("NOW") where rowid=?', title, record.ROWID)
end
end
return true
end
------------------------------------------------------------------
---- Status bar
-- local statuswin = Pdcurses.Window(1,w,y+h+1,x)
local laststatus
local function winstatus( sttype, data )
-- statuswin:hline(HLINE,w+2)
local prevx, prevy = win:getx(), win:gety()
local statusrow = dim.y + dim.h - 1
laststatus = data
local crdate = '???'
local update = '???'
if record and record.dt_created then
local d = record.dt_created
crdate = d:sub(3,4) .. d:sub(6,7) .. d:sub(9,10)
end
if record and record.dt_modified then
local d = record.dt_modified
update = d:sub(3,4) .. d:sub(6,7) .. d:sub(9,10)
end
data = 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()
end
local titlecache = {}
local editcordrefresh = nil
local function plsed_drawline( win, x, y, text, markBeg, markEnd )
if #text > (dim.w - 2) then
win:mvaddstr( y, x, text:sub(1,dim.w-2) )
return dim.w-2
end
-- markBeg, markEnd indicate the text in the section should
-- be highlighted
if not markBeg then
local b,e,hdr,htext = text:find("^(h%d%.)(.*)") -- textile specific highlighting!! DANGER!! DANGER!!
if not b then
win:mvaddstr( y, x, text )
else
win:mvaddstr( y, x, hdr )
Theme.with.heading( win, function()
win:addstr(htext)
end)
end
else
local endPoint = markEnd > (dim.w-2) and (dim.w-2) or markEnd
win: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) )
end
local drawn = #text
local remain = (dim.w -2) - drawn
local s,e,tag = string.find(text,':(%w+)$')
if s then
if Numword.isvalid( tag ) then
-- win:move(y,x)
if not titlecache[tag] then
local rec = db:selectOne( 'select title from note where rowid=?',
Numword.to_i(tag) )
titlecache[tag] = rec and rec.title
end
local 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 + #todraw
end
end
-- here we look for codings, e.g., cross-system references that may be
-- unique to the active space
plugin.fixLine(text, win)
end
local e = setmetatable( { w = win,
report = winstatus,
on = {}
}, { __index = env } )
-- for simple testing
bbuffer = 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() end
local removewin -- callback set when Winman.push is called
-- kill_buffer() callback from editor cord
function e.on.kill_buffer()
-- cord.event._shutdown()
wv.log('debug','got kill buffer removewin=%s',removewin)
if record.ROWID then
gOpenRecords[record.ROWID] = nil
end
removewin()
end
function 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 then
local id = record.ROWID
local 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 )
end
end
menuedFunctions( cord_edit, {
XML = function() end,
Nonsense = function() end,
Link = insert_link
}, { vert = true })
end
-- @todo: check buffer for newlines
local function insert_yank_with_tag( tag )
-- if buffer:char_at_point(thePoint) ~= '\n' then cord_edit.event.insert_at_point( '\n' ) end
cord_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' ) end
cord_edit.event.insert_at_point( '</' .. tag .. '>\n' )
end
menuedFunctions( 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
})
end
local function possibly_transform_clipboard_text( text )
wv.log('debug', 'transform clibpboard=[[%s]]', text)
if string.find(text,'^(http://.*)') or string.find(text,'^(https://.*)') then
return string.gsub(text, ' ', '%%20')
else
return text
end
end
function e.on.yank(special)
if not NylonOs then
return
end
local 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 then
local tyyp = cbrc.tyyp
local 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' then
local _, _, m = content:find '<!%-%-StartFragment%-%->(.*)<!%-%-EndFragment%-%->'
if m then
cord_edit.event.insert_at_point( m ) -- :sub(s,e)
puthtm = true
end
end
if tyyp == 'text' and (not puthtm) then
cord_edit.event.insert_at_point( possibly_transform_clipboard_text(content) )
end
if 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 itext
if #content > 1 then
itext = '* ' .. table.concat( fileURLs, '\n* ' )
else
itext = fileURLs[1]
end
cord_edit.event.insert_at_point( itext )
end)()
end
if tyyp == 'image/png' then
dbimg = 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 then
wv.log('debug','Got image out, sz=%d', #copy.raw)
else
wv.log('error','cant read saved image??')
end
-- shell_cmd 'get-process snippingtool | stop-process"'
end
end
return true;
end
-- save() callback from editor cord
function 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 text
local 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 reversing
local rpatches = Diff.patch_make(prevText,text)
local rpatchtext = Diff.patch_toText(rpatches)
if #patchtext > 0 then
wv.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)
end
on_save_scan_citations( db, record.ROWID, text )
local isNew = false
if record.ROWID then
db:retryexec('update note set detail=?,dt_modified=DATETIME("NOW") where rowid=?', text, record.ROWID )
else
local 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] = mw
isNew = true
end
plugin.onEditRecord( isNew, record )
bbuffer:setUnmodified()
record.detail = text
setCurrentRev()
settitle()
end -- end e.on.save()
function e.on.modified()
settitle() -- reset the "modified" indicator
end
function e.on.tagActivated( tag, alttag )
wv.log('debug','tagActivated, tag=%s (alt=%s)',tag, alttag)
cord_app.event.OpenRecord{ tag, alttag }
end
function 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 me
local function winman_keyhandler(k)
if k == Keys.control.editTitle then
cord_app:add_pending(editTitle)
else
if type(k) == 'string' then
if cord_edit:has_event(k) then
cord_edit.event[k]()
else
if k == 'mail_record' then
local 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' then
Nylon.cord('extract_to_new_record',
function(cord)
cord:sleep_manual(
function(wake)
cord_edit.event.replace_marked(
function(marked_text)
if not marked_text then
wake()
return
end
local proposedTitle = 'New Record from: ' .. record.title
local newtitle = ui_oneline( env, 'Extracted Record Title', { text = proposedTitle })
if not newtitle then
return false
end
local 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 = 1379
make_editor(env,r)
wake()
return ':' .. Numword.to_s(r.ROWID) .. '\n'
end)
end )
end )
elseif k == 'insert_ref_to_new_record' then
Nylon.cord(k, function(cord)
local function doit(wakefun)
local proposedTitle = 'New Record from: ' .. record.title
local newtitle = ui_oneline( env, 'New Record Title', { text = proposedTitle })
if not newtitle then
wakefun()
return
end
local 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 = 1379
make_editor(env,r)
cord_edit.event.insert_at_point( ':' .. Numword.to_s(r.ROWID) )
end
cord:sleep_manual( doit )
end )
elseif k == 'insert_file' then
Nylon.cord('insert_file_cord',function(cord)
cord:sleep_manual( function(wake)
local fname = ui_oneline( env, 'Insert file' )
if fname then
wv.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)
end
end )
end )
else
return k
end
end
else
cord_edit.event.key(k)
end
end
end
-- callback when winman resizes me
function winman_resized( newdim )
if newdim then
dim.x, dim.y, dim.w, dim.h = newdim.x, newdim.y, newdim.w, newdim.h
end
-- editor x,y is always offset 1,1 of the window
editordim.w, editordim.h = dim.w-2, dim.h-2
drawbox()
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 focus
Nylon.self:sleep_manual(function(wakefun)
cord_edit.event.redraw( wakefun )
end)
end
wv.log('debug','winman_resize done')
end
mw = Winman.win:new{
win = win,
on = { resized = winman_resized,
key = winman_keyhandler,
}
}
removewin = Winman.push( mw )
if (record.ROWID) then
gOpenRecords[record.ROWID] = mw
end
end -- 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 then
collectgarbage();
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 longer
collectgarbage()
Pdcurses.Static.refresh()
end
function cord.event.save_buffers_kill_terminal()
cord.event.quit()
end
function cord.event.other_window()
Winman.other_window()
end
function cord.event.focused_window_to_primary()
Winman.focused_window_to_primary()
end
cord.event.SwapBuffers = function()
wv.log 'app cord.event.SwapBuffers'
Winman.swap_tiled()
end
cord.event.NewRecord = function( opt )
local record = { title = opt and opt.title or 'new record', detail = '\n\n' }
make_editor( env, record )
end
cord.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 then
local newt = tostring(recid1)
if newt:sub(1,1) == ':' then
newt = newt:sub(1,1) .. newt:sub(2,2):upper() .. newt:sub(3)
else
newt = ':' .. newt:sub(1,1):upper() .. newt:sub(2)
end
cord.event.NewRecord{ title = (newt .. ' new record') }
return
end
if record then
wv.log('debug','got record=%s',json:encode(record))
make_editor( env, record )
-- warn / status??
-- 'could not find record for id=%s', tostring(recid)
end
end
local lastsearch = ''
cord.event.Search = function()
local sstr = ui_oneline( env, 'Search For', { text = lastsearch, replace = true } )
if not sstr then return end
local S = require 'pls-db'
local fancysearch = S.megasearch( sstr )
wv.log('debug','fancysearch=%s',fancysearch)
lastsearch = sstr
picklist( ('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)) )
end
while true do
cord.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,
})
end
end -- end, function entryfn_app
local function ding(x,p,...)
end
-- state variables to track whether a prefix is active
local command_prefix = false
local personal_prefix = false
local 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 = 1
local esccount
cord.event.gotesc = function(k)
esccount = count
cord.event.sendesc()
end
cord.event.gotnonesc = function(k)
count = count + 1
end
while true do
cord.event.sendesc.wait()
cord:sleep(0.1) -- wait 100 ms
if esccount == count then -- no keys received since esc
wv.log('debug','should send escape here')
Winman.inject_key( 9027 ) -- ctrl+g
-- Winman.inject_key( 27 ) -- ctrl+g
end
end
end)
local keymap_everybody = Keys.everybody
local keymap_command_prefix = Keys.command_prefix
local keymap_personal_prefix = Keys.personal_prefix
local keymap_esc_prefix = Keys.esc_prefix
local function app_keymapper( k )
local function handle_if_app_event_or_return( k )
if cord_app:has_event(k) then
cord_app.event[k]()
else
return k
end
end
local function unknown(c)
if k >= 32 and k < 128 then
ding( 'Unknown prefix command Ctrl-%s + "%c"', c, k )
else
if k < 27 then
ding( 'Unknown prefix command Ctrl-%s + Ctrl-%c', c, (k+64) )
else
ding( 'Unknown prefix command Ctrl-%s + %d', c, k )
end
end
end
if k == 27 then -- esc
esc_prefix = true
cord_eschandler.event.gotesc()
return
else
cord_eschandler.event.gotnonesc()
end
if k == 9027 then -- real escape; see cord_eschandler
k = 27
end
if esc_prefix then
esc_prefix = false
if keymap_esc_prefix[k] then
return app_keymapper( keymap_esc_prefix[k] )
end
end
if command_prefix then
command_prefix = false
if 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 be
wv.log('debug','got cmd prefix mapped [%d=>%s]',k,keymap_command_prefix[k])
return handle_if_app_event_or_return( keymap_command_prefix[k] )
else
unknown 'X'
end
elseif personal_prefix then
personal_prefix = false
return handle_if_app_event_or_return( keymap_personal_prefix[k] )
elseif keymap_everybody[k] then
return handle_if_app_event_or_return( keymap_everybody[k] )
elseif k == 3 then -- begin personal prefix
personal_prefix = true
wv.log 'start personal prefix'
elseif k == 435 then -- M-s
wv.log 'got swap / M-s key'
cord_app.event.SwapBuffers()
elseif k == Keys.control.menu then -- C-l
cord_app.event.grab()
elseif k == 20 or k == 24 then -- C-t or C-x
command_prefix = true
else
return k -- not handled or translated; pass on the original key
end
end
local function app_unhandledkeys(k)
wv.log('debug','app unhandled key handler key=%s',k)
if k == 'jumptotag' then
cord_app.event.OpenRecord()
elseif k == 'toggle_landscape' then
Winman.toggle_landscape()
else
wv.log('debug','unhandled key/input event=%s',k)
end
end
------------------------------------------------------------------
------------------------------------------------------------------
local function cordfn_followup( cord )
while true do
local 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 then
local manualClose = false
local done = false
picklist(
{ title = string.format('Follow Up %s', today),
on = {
closed = function()
wv.log('debug','followup picklist manually closed')
manualClose = true
if done then
done()
end
done = function() end
end
} },
'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())) do
wv.log('debug', 'followup picklist open; wait until closed or new day')
cord:sleep_manual( function(wakefn)
done = function()
done = false
wakefn()
end
NylonSysCore.addOneShot( 120*1000, function() if done then done() end end) -- wake up every 2minutes to check for new day
end)
end
if manualClose then
wv.log('debug', 'followup picklist closed manually, wait 15min')
cord:sleep(15*60*1000) -- 15 minutes
end
else
cord:sleep(20) -- as long as no follow up items are found, check every 20s (to detect new ones)
end
end
end
Nylon.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 text
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 debugging
local 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 then
if arg[2] then
local 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) do
local gdiff = Diff.patch_fromText( patch.content )
record.detail = Diff.patch_apply( gdiff, record.detail )
end
end
print( 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] then
filename = arg[1]
end
require 'LbindPdcurses'
local Buffer = require 'pls-buffer'
local bbuffer = Buffer.openFile( filename )
local gl_keybindings = {
[3] = 'quit', -- ctrl+c
[27] = 'quit', -- esc
}
local keyhandler
-- 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
end
local 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' then
Pdcurses.Static.endwin()
os.exit(1)
else
wv.log('norm','keyhandler[%d]=%s',k,keyhandler)
if keyhandler then
keyhandler(k)
end
end
end )
end
local 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 }
end
local function WindowDim( x, y, w, h )
return { x = x, y = y, w = w, h = h }
end
local function make_editor( env )
local ed = require 'pls-ed'
local nrows, ncols = env.curses.nrows, env.curses.ncols
local 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.screen
local 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 + 2
statuswin:addstr(string.rep('_',left))
statuswin:refresh()
end
local e = setmetatable( { w = win,
report = winstatus
}, { __index = env } )
local cord_edit = Nylon.cord( 'edit', ed.entryfn_edit, WindowDim(0,0,w,h), bbuffer, e )
end
local 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.lshift
local type, setmetatable, ipairs, select
= type, setmetatable, ipairs, select
local unpack, tonumber, error
= unpack, tonumber, error
local strsub, strbyte, strchar, gmatch, gsub
= string.sub, string.byte, string.char, string.gmatch, string.gsub
local strmatch, strfind, strformat
= string.match, string.find, string.format
local tinsert, tremove, tconcat
= table.insert, table.remove, table.concat
local max, min, floor, ceil, abs
= math.max, math.min, math.floor, math.ceil, math.abs
local clock = os.clock
-- Utility functions.
local percentEncode_pattern = '[^A-Za-z0-9%-=;\',./~!@#$%&*%(%)_%+ %?]'
local function percentEncode_replace(v)
return strformat('%%%02X', strbyte(v))
end
local function tsplice(t, idx, deletions, ...)
local insertions = select('#', ...)
for i = 1, deletions do
tremove(t, idx)
end
for i = insertions, 1, -1 do
-- do not remove parentheses around select
tinsert(t, idx, (select(i, ...)))
end
end
local function strelement(str, i)
return strsub(str, i, i)
end
local function indexOf(a, b, start)
if (#b == 0) then
return nil
end
return strfind(a, b, start, true)
end
local 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_prettyHtml
local match_main
local 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 = -1
local DIFF_INSERT = 1
local 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 = 32
function settings(new)
if new then
Diff_Timeout = new.Diff_Timeout or Diff_Timeout
Diff_EditCost = new.Diff_EditCost or Diff_EditCost
Match_Threshold = new.Match_Threshold or Match_Threshold
Match_Distance = new.Match_Distance or Match_Distance
Patch_DeleteThreshold = new.Patch_DeleteThreshold or Patch_DeleteThreshold
Patch_Margin = new.Patch_Margin or Patch_Margin
Match_MaxBits = new.Match_MaxBits or Match_MaxBits
else
return {
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;
}
end
end
-- ---------------------------------------------------------------------------
-- DIFF API
-- ---------------------------------------------------------------------------
-- The private diff functions
local _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 then
if Diff_Timeout <= 0 then
opt_deadline = 2 ^ 31
else
opt_deadline = clock() + Diff_Timeout
end
end
local deadline = opt_deadline
-- Check for null inputs.
if text1 == nil or text1 == nil then
error('Null inputs. (diff_main)')
end
-- Check for equality (speedup).
if text1 == text2 then
if #text1 > 0 then
return {{DIFF_EQUAL, text1}}
end
return {}
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 commonprefix
if commonlength > 0 then
commonprefix = 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 commonsuffix
if commonlength > 0 then
commonsuffix = 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 then
tinsert(diffs, 1, {DIFF_EQUAL, commonprefix})
end
if commonsuffix then
diffs[#diffs + 1] = {DIFF_EQUAL, commonsuffix}
end
_diff_cleanupMerge(diffs)
return diffs
end
--[[
* 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 = false
local 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 = 0
local length_deletions1 = 0
-- Number of characters that changed after the equality.
local length_insertions2 = 0
local length_deletions2 = 0
while diffs[pointer] do
if diffs[pointer][1] == DIFF_EQUAL then -- Equality found.
equalitiesLength = equalitiesLength + 1
equalities[equalitiesLength] = pointer
length_insertions1 = length_insertions2
length_deletions1 = length_deletions2
length_insertions2 = 0
length_deletions2 = 0
lastequality = diffs[pointer][2]
else -- An insertion or deletion.
if diffs[pointer][1] == DIFF_INSERT then
length_insertions2 = length_insertions2 + #(diffs[pointer][2])
else
length_deletions2 = length_deletions2 + #(diffs[pointer][2])
end
-- Eliminate an equality that is smaller or equal to the edits on both
-- sides of it.
if lastequality
and (#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 - 1
pointer = (equalitiesLength > 0) and equalities[equalitiesLength] or 0
length_insertions1, length_deletions1 = 0, 0 -- Reset the counters.
length_insertions2, length_deletions2 = 0, 0
lastequality = nil
changes = true
end
end
pointer = pointer + 1
end
-- 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 = 2
while diffs[pointer] do
if (diffs[pointer - 1][1] == DIFF_DELETE and
diffs[pointer][1] == DIFF_INSERT) then
local 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) then
if (overlap_length1 >= #deletion / 2 or
overlap_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 + 1
end
else
if (overlap_length2 >= #deletion / 2 or
overlap_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 + 1
end
end
pointer = pointer + 1
end
pointer = pointer + 1
end
end
--[[
* 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 = 0
while diffs[pointer] do
if 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 + 1
equalities[equalitiesLength] = pointer
pre_ins, pre_del = post_ins, post_del
lastequality = diffText
else
-- Not a candidate, and can never become one.
equalitiesLength = 0
lastequality = nil
end
post_ins, post_del = 0, 0
else -- An insertion or deletion.
if diffs[pointer][1] == DIFF_DELETE then
post_del = 1
else
post_ins = 1
end
--[[
* 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 - 1
lastequality = nil
if (pre_ins == 1) and (pre_del == 1) then
-- No changes made which could affect previous entry, keep going.
post_ins, post_del = 1, 1
equalitiesLength = 0
else
-- Throw away the previous equality.
equalitiesLength = equalitiesLength - 1
pointer = (equalitiesLength > 0) and equalities[equalitiesLength] or 0
post_ins, post_del = 0, 0
end
changes = true
end
end
pointer = pointer + 1
end
if changes then
_diff_cleanupMerge(diffs)
end
end
--[[
* 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 = 0
local insertions, deletions = 0, 0
for x, diff in ipairs(diffs) do
local op, data = diff[1], diff[2]
if (op == DIFF_INSERT) then
insertions = insertions + #data
elseif (op == DIFF_DELETE) then
deletions = deletions + #data
elseif (op == DIFF_EQUAL) then
-- A deletion and an insertion is one substitution.
levenshtein = levenshtein + max(insertions, deletions)
insertions = 0
deletions = 0
end
end
levenshtein = levenshtein + max(insertions, deletions)
return levenshtein
end
--[[
* 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) do
local 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 then
html[x] = '<ins style="background:#e6ffe6;">' .. text .. '</ins>'
elseif op == DIFF_DELETE then
html[x] = '<del style="background:#ffe6e6;">' .. text .. '</del>'
elseif op == DIFF_EQUAL then
html[x] = '<span>' .. text .. '</span>'
end
end
return 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}}
end
if #text2 == 0 then
-- Just delete some text (speedup).
return {{DIFF_DELETE, text1}}
end
local diffs
local longtext = (#text1 > #text2) and text1 or text2
local shorttext = (#text1 > #text2) and text2 or text1
local 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 then
diffs[1][1], diffs[3][1] = DIFF_DELETE, DIFF_DELETE
end
return diffs
end
if #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.
do
local
text1_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_a
diffs = diffs_a
diffs[diffs_a_len + 1] = {DIFF_EQUAL, mid_common}
for i, b_diff in ipairs(diffs_b) do
diffs[diffs_a_len + 1 + i] = b_diff
end
return diffs
end
end
return _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 = #text1
local text2_length = #text2
local _sub, _element
local max_d = ceil((text1_length + text2_length) / 2)
local v_offset = max_d
local v_length = 2 * max_d
local v1 = {}
local v2 = {}
-- Setting all elements to -1 is faster in Lua than mixing integers and nil.
for x = 0, v_length - 1 do
v1[x] = -1
v2[x] = -1
end
v1[v_offset + 1] = 0
v2[v_offset + 1] = 0
local 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 = 0
local k1end = 0
local k2start = 0
local k2end = 0
for d = 0, max_d - 1 do
-- Bail out if deadline is reached.
if clock() > deadline then
break
end
-- Walk the front path one step.
for k1 = -d + k1start, d - k1end, 2 do
local k1_offset = v_offset + k1
local x1
if (k1 == -d) or ((k1 ~= d) and
(v1[k1_offset - 1] < v1[k1_offset + 1])) then
x1 = v1[k1_offset + 1]
else
x1 = v1[k1_offset - 1] + 1
end
local y1 = x1 - k1
while (x1 <= text1_length) and (y1 <= text2_length)
and (strelement(text1, x1) == strelement(text2, y1)) do
x1 = x1 + 1
y1 = y1 + 1
end
v1[k1_offset] = x1
if x1 > text1_length + 1 then
-- Ran off the right of the graph.
k1end = k1end + 2
elseif y1 > text2_length + 1 then
-- Ran off the bottom of the graph.
k1start = k1start + 2
elseif front then
local k2_offset = v_offset + delta - k1
if 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] + 1
if x1 > x2 then
-- Overlap detected.
return _diff_bisectSplit(text1, text2, x1, y1, deadline)
end
end
end
end
-- Walk the reverse path one step.
for k2 = -d + k2start, d - k2end, 2 do
local k2_offset = v_offset + k2
local x2
if (k2 == -d) or ((k2 ~= d) and
(v2[k2_offset - 1] < v2[k2_offset + 1])) then
x2 = v2[k2_offset + 1]
else
x2 = v2[k2_offset - 1] + 1
end
local y2 = x2 - k2
while (x2 <= text1_length) and (y2 <= text2_length)
and (strelement(text1, -x2) == strelement(text2, -y2)) do
x2 = x2 + 1
y2 = y2 + 1
end
v2[k2_offset] = x2
if x2 > text1_length + 1 then
-- Ran off the left of the graph.
k2end = k2end + 2
elseif y2 > text2_length + 1 then
-- Ran off the top of the graph.
k2start = k2start + 2
elseif not front then
local k1_offset = v_offset + delta - k2
if k1_offset >= 0 and k1_offset < v_length and v1[k1_offset] ~= -1 then
local x1 = v1[k1_offset]
local y1 = v_offset + x1 - k1_offset
-- Mirror x2 onto top-left coordinate system.
x2 = text1_length - x2 + 1
if x1 > x2 then
-- Overlap detected.
return _diff_bisectSplit(text1, text2, x1, y1, deadline)
end
end
end
end
end
-- 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 = #diffs
for i, v in ipairs(diffsb) do
diffs[diffs_len + i] = v
end
return diffs
end
--[[
* 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))
then
return 0
end
-- Binary search.
-- Performance analysis: http://neil.fraser.name/news/2007/10/09/
local pointermin = 1
local pointermax = min(#text1, #text2)
local pointermid = pointermax
local pointerstart = 1
while (pointermin < pointermid) do
if (strsub(text1, pointerstart, pointermid)
== strsub(text2, pointerstart, pointermid)) then
pointermin = pointermid
pointerstart = pointermin
else
pointermax = pointermid
end
pointermid = floor(pointermin + (pointermax - pointermin) / 2)
end
return pointermid
end
--[[
* 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)) then
return 0
end
-- Binary search.
-- Performance analysis: http://neil.fraser.name/news/2007/10/09/
local pointermin = 1
local pointermax = min(#text1, #text2)
local pointermid = pointermax
local pointerend = 1
while (pointermin < pointermid) do
if (strsub(text1, -pointermid, -pointerend)
== strsub(text2, -pointermid, -pointerend)) then
pointermin = pointermid
pointerend = pointermin
else
pointermax = pointermid
end
pointermid = floor(pointermin + (pointermax - pointermin) / 2)
end
return pointermid
end
--[[
* 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 = #text1
local text2_length = #text2
-- Eliminate the null case.
if text1_length == 0 or text2_length == 0 then
return 0
end
-- Truncate the longer string.
if text1_length > text2_length then
text1 = strsub(text1, text1_length - text2_length + 1)
elseif text1_length < text2_length then
text2 = strsub(text2, 1, text1_length)
end
local text_length = min(text1_length, text2_length)
-- Quick check for the worst case.
if text1 == text2 then
return text_length
end
-- 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 = 0
local length = 1
while true do
local pattern = strsub(text1, text_length - length + 1)
local found = strfind(text2, pattern, 1, true)
if found == nil then
return best
end
length = length + found - 1
if found == 1 or strsub(text1, text_length - length + 1) ==
strsub(text2, 1, length) then
best = length
length = length + 1
end
end
end
--[[
* 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 -1
local best_common = ''
local best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b
while true do
j = indexOf(shorttext, seed, j + 1)
if (j == nil) then
break
end
local 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 then
best_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)
end
end
if #best_common * 2 >= #longtext then
return {best_longtext_a, best_longtext_b,
best_shorttext_a, best_shorttext_b, best_common}
else
return nil
end
end
--[[
* 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 nil
end
local longtext = (#text1 > #text2) and text1 or text2
local shorttext = (#text1 > #text2) and text2 or text1
if (#longtext < 4) or (#shorttext * 2 < #longtext) then
return 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 hm
if not hm1 and not hm2 then
return nil
elseif not hm2 then
hm = hm1
elseif not hm1 then
hm = hm2
else
-- Both matched. Select the longest.
hm = (#hm1[5] > #hm2[5]) and hm1 or hm2
end
-- A half-match was found, sort out the return data.
local text1_a, text1_b, text2_a, text2_b
if (#text1 > #text2) then
text1_a, text1_b = hm[1], hm[2]
text2_a, text2_b = hm[3], hm[4]
else
text2_a, text2_b = hm[1], hm[2]
text1_a, text1_b = hm[3], hm[4]
end
local mid_common = hm[5]
return text1_a, text1_b, text2_a, text2_b, mid_common
end
--[[
* 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 6
end
-- 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 5
elseif lineBreak1 or lineBreak2 then
-- Four points for line breaks.
return 4
elseif nonAlphaNumeric1 and not whitespace1 and whitespace2 then
-- Three points for end of sentences.
return 3
elseif whitespace1 or whitespace2 then
-- Two points for whitespace.
return 2
elseif nonAlphaNumeric1 or nonAlphaNumeric2 then
-- One point for non-alphanumeric.
return 1
end
return 0
end
--[[
* 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] do
local 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 then
local commonString = strsub(edit, -commonOffset)
equality1 = strsub(equality1, 1, -commonOffset - 1)
edit = commonString .. strsub(edit, 1, -commonOffset - 1)
equality2 = commonString .. equality2
end
-- Second, step character by character right, looking for the best fit.
local bestEquality1 = equality1
local bestEdit = edit
local bestEquality2 = equality2
local bestScore = _diff_cleanupSemanticScore(equality1, edit)
+ _diff_cleanupSemanticScore(edit, equality2)
while strbyte(edit, 1) == strbyte(equality2, 1) do
equality1 = 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 then
bestScore = score
bestEquality1 = equality1
bestEdit = edit
bestEquality2 = equality2
end
end
if prevDiff[2] ~= bestEquality1 then
-- We have an improvement, save it back to the diff.
if #bestEquality1 > 0 then
diffs[pointer - 1][2] = bestEquality1
else
tremove(diffs, pointer - 1)
pointer = pointer - 1
end
diffs[pointer][2] = bestEdit
if #bestEquality2 > 0 then
diffs[pointer + 1][2] = bestEquality2
else
tremove(diffs, pointer + 1, 1)
pointer = pointer - 1
end
end
end
pointer = pointer + 1
end
end
--[[
* 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 = 1
local count_delete, count_insert = 0, 0
local text_delete, text_insert = '', ''
local commonlength
while diffs[pointer] do
local diff_type = diffs[pointer][1]
if diff_type == DIFF_INSERT then
count_insert = count_insert + 1
text_insert = text_insert .. diffs[pointer][2]
pointer = pointer + 1
elseif diff_type == DIFF_DELETE then
count_delete = count_delete + 1
text_delete = text_delete .. diffs[pointer][2]
pointer = pointer + 1
elseif diff_type == DIFF_EQUAL then
-- Upon reaching an equality, check for prior redundancies.
if count_delete + count_insert > 1 then
if (count_delete > 0) and (count_insert > 0) then
-- Factor out any common prefixies.
commonlength = _diff_commonPrefix(text_insert, text_delete)
if commonlength > 0 then
local back_pointer = pointer - count_delete - count_insert
if (back_pointer > 1) and (diffs[back_pointer - 1][1] == DIFF_EQUAL)
then
diffs[back_pointer - 1][2] = diffs[back_pointer - 1][2]
.. strsub(text_insert, 1, commonlength)
else
tinsert(diffs, 1,
{DIFF_EQUAL, strsub(text_insert, 1, commonlength)})
pointer = pointer + 1
end
text_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 then
diffs[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)
end
end
-- Delete the offending records and add the merged ones.
if count_delete == 0 then
tsplice(diffs, pointer - count_insert,
count_insert, {DIFF_INSERT, text_insert})
elseif count_insert == 0 then
tsplice(diffs, pointer - count_delete,
count_delete, {DIFF_DELETE, text_delete})
else
tsplice(diffs, pointer - count_delete - count_insert,
count_delete + count_insert,
{DIFF_DELETE, text_delete}, {DIFF_INSERT, text_insert})
end
pointer = pointer - count_delete - count_insert
+ (count_delete>0 and 1 or 0) + (count_insert>0 and 1 or 0) + 1
elseif (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)
else
pointer = pointer + 1
end
count_insert, count_delete = 0, 0
text_delete, text_insert = '', ''
end
end
if diffs[#diffs][2] == '' then
diffs[#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>AC
local changes = false
pointer = 2
-- Intentionally ignore the first and last element (don't need checking).
while pointer < #diffs do
local 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 = true
elseif strsub(currentText, 1, #nextText) == nextText then
-- Shift the edit over the next equality.
prevDiff[2] = prevText .. nextText
diff[2] = strsub(currentText, #nextText + 1) .. nextText
tremove(diffs, pointer + 1)
changes = true
end
end
pointer = pointer + 1
end
-- 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)
end
end
--[[
* 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 = 1
local chars2 = 1
local last_chars1 = 1
local last_chars2 = 1
local x
for _x, diff in ipairs(diffs) do
x = _x
if diff[1] ~= DIFF_INSERT then -- Equality or deletion.
chars1 = chars1 + #diff[2]
end
if diff[1] ~= DIFF_DELETE then -- Equality or insertion.
chars2 = chars2 + #diff[2]
end
if chars1 > loc then -- Overshot the location.
break
end
last_chars1 = chars1
last_chars2 = chars2
end
-- Was the location deleted?
if diffs[x + 1] and (diffs[x][1] == DIFF_DELETE) then
return last_chars2
end
-- 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) do
if diff[1] ~= DIFF_INSERT then
text[#text + 1] = diff[2]
end
end
return 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) do
if diff[1] ~= DIFF_DELETE then
text[#text + 1] = diff[2]
end
end
return 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) do
local op, data = diff[1], diff[2]
if op == DIFF_INSERT then
text[x] = '+' .. gsub(data, percentEncode_pattern, percentEncode_replace)
elseif op == DIFF_DELETE then
text[x] = '-' .. #data
elseif op == DIFF_EQUAL then
text[x] = '=' .. #data
end
end
return 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 faster
local pointer = 1 -- Cursor in text1
for 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 == '+') then
local invalidDecode = false
local decoded = gsub(param, '%%(.?.?)',
function(c)
local n = tonumber(c, 16)
if (#c ~= 2) or (n == nil) then
invalidDecode = true
return ''
end
return strchar(n)
end)
if invalidDecode then
-- Malformed URI sequence.
error('Illegal escape in _diff_fromDelta: ' .. param)
end
diffsLength = diffsLength + 1
diffs[diffsLength] = {DIFF_INSERT, decoded}
elseif (tokenchar == '-') or (tokenchar == '=') then
local n = tonumber(param)
if (n == nil) or (n < 0) then
error('Invalid number in _diff_fromDelta: ' .. param)
end
local text = strsub(text1, pointer, pointer + n - 1)
pointer = pointer + n
if (tokenchar == '=') then
diffsLength = diffsLength + 1
diffs[diffsLength] = {DIFF_EQUAL, text}
else
diffsLength = diffsLength + 1
diffs[diffsLength] = {DIFF_DELETE, text}
end
else
error('Invalid diff operation in _diff_fromDelta: ' .. token)
end
end
if (pointer ~= #text1 + 1) then
error('Delta length (' .. (pointer - 1)
.. ') does not equal source text length (' .. #text1 .. ').')
end
return diffs
end
-- ---------------------------------------------------------------------------
-- 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 then
error('Null inputs. (match_main)')
end
if text == pattern then
-- Shortcut (potentially not guaranteed by the algorithm)
return 1
elseif #text == 0 then
-- Nothing to match.
return -1
end
loc = 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 loc
else
-- Do a fuzzy compare.
return _match_bitap(text, pattern, loc)
end
end
-- ---------------------------------------------------------------------------
-- 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 = 0
for c in gmatch(pattern, '.') do
s[c] = bor(s[c] or 0, lshift(1, #pattern - i - 1))
i = i + 1
end
return s
end
--[[
* 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 then
error('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 / #pattern
local proximity = abs(loc - x)
if (Match_Distance == 0) then
-- Dodge divide by zero error.
return (proximity == 0) and 1 or accuracy
end
return 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 then
score_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 = -1
local bin_min, bin_mid
local bin_max = #pattern + #text
local last_rd
for 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 = 0
bin_mid = bin_max
while (bin_min < bin_mid) do
if (_match_bitapScore(d, loc + bin_mid) <= score_threshold) then
bin_min = bin_mid
else
bin_max = bin_mid
end
bin_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_mid
local start = max(1, loc - bin_mid + 1)
local finish = min(loc + bin_mid, #text) + #pattern
local rd = {}
for j = start, finish do
rd[j] = 0
end
rd[finish + 1] = lshift(1, d) - 1
for j = finish, start, -1 do
local charMatch = s[strsub(text, j - 1, j - 1)] or 0
if (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]
)
)
end
if (band(rd[j], matchmask) ~= 0) then
local 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 = score
best_loc = j - 1
if (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.
break
end
end
end
end
-- No hope for a (better) match at greater error levels.
if (_match_bitapScore(d + 1, loc) > score_threshold) then
break
end
last_rd = rd
end
return best_loc
end
-- -----------------------------------------------------------------------------
-- 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, diffs
local 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 = a
diffs = diff_main(text1, opt_b, true)
if (#diffs > 2) then
diff_cleanupSemantic(diffs)
diff_cleanupEfficiency(diffs)
end
elseif (type_a == 'table') and (type_b == 'nil') and (type_c == 'nil') then
-- Method 2: diffs
-- Compute text1 from diffs.
diffs = a
text1 = _diff_text1(diffs)
elseif (type_a == 'string') and (type_b == 'table') and (type_c == 'nil') then
-- Method 3: text1, diffs
text1 = a
diffs = opt_b
elseif (type_a == 'string') and (type_b == 'string') and (type_c == 'table')
then
-- Method 4: text1, text2, diffs
-- text2 is not used.
text1 = a
diffs = opt_c
else
error('Unknown call format to patch_make.')
end
if (diffs[1] == nil) then
return {} -- Get rid of the null case.
end
local 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, text1
for x, diff in ipairs(diffs) do
local 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 + 1
patch.start2 = char_count2 + 1
end
if (diff_type == DIFF_INSERT) then
patchDiffLength = patchDiffLength + 1
patch.diffs[patchDiffLength] = diff
patch.length2 = patch.length2 + #diff_text
postpatch_text = strsub(postpatch_text, 1, char_count2)
.. diff_text .. strsub(postpatch_text, char_count2 + 1)
elseif (diff_type == DIFF_DELETE) then
patch.length1 = patch.length1 + #diff_text
patchDiffLength = patchDiffLength + 1
patch.diffs[patchDiffLength] = diff
postpatch_text = strsub(postpatch_text, 1, char_count2)
.. strsub(postpatch_text, char_count2 + #diff_text + 1)
elseif (diff_type == DIFF_EQUAL) then
if (#diff_text <= Patch_Margin * 2)
and (patchDiffLength ~= 0) and (#diffs ~= x) then
-- Small equality inside a patch.
patchDiffLength = patchDiffLength + 1
patch.diffs[patchDiffLength] = diff
patch.length1 = patch.length1 + #diff_text
patch.length2 = patch.length2 + #diff_text
elseif (#diff_text >= Patch_Margin * 2) then
-- Time for a new patch.
if (patchDiffLength ~= 0) then
_patch_addContext(patch, prepatch_text)
patches[#patches + 1] = patch
patch = _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_text
char_count1 = char_count2
end
end
end
-- Update the current character count.
if (diff_type ~= DIFF_INSERT) then
char_count1 = char_count1 + #diff_text
end
if (diff_type ~= DIFF_DELETE) then
char_count2 = char_count2 + #diff_text
end
end
-- Pick up the leftover patch if not empty.
if (patchDiffLength > 0) then
_patch_addContext(patch, prepatch_text)
patches[#patches + 1] = patch
end
return patches
end
--[[
* 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 then
return 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 = 0
local results = {}
for x, patch in ipairs(patches) do
local expected_loc = patch.start2 + delta
local text1 = _diff_text1(patch.diffs)
local start_loc
local end_loc = -1
if #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 then
end_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 = -1
end
end
else
start_loc = match_main(text, text1, expected_loc)
end
if 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.length1
else
-- Found a match. :)
results[x] = true
delta = start_loc - expected_loc
local text2
if end_loc == -1 then
text2 = strsub(text, start_loc, start_loc + #text1 - 1)
else
text2 = strsub(text, start_loc, end_loc + Match_MaxBits - 1)
end
if 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] = false
else
_diff_cleanupSemanticLossless(diffs)
local index1 = 1
local index2
for y, mod in ipairs(patch.diffs) do
if mod[1] ~= DIFF_EQUAL then
index2 = _diff_xIndex(diffs, index1)
end
if mod[1] == DIFF_INSERT then
text = strsub(text, 1, start_loc + index2 - 2)
.. mod[2] .. strsub(text, start_loc + index2 - 1)
elseif mod[1] == DIFF_DELETE then
text = strsub(text, 1, start_loc + index2 - 2) .. strsub(text,
start_loc + _diff_xIndex(diffs, index1 + #mod[2] - 1))
end
if mod[1] ~= DIFF_DELETE then
index1 = index1 + #mod[2]
end
end
end
end
end
end
-- Strip the padding off.
text = strsub(text, #nullPadding + 1, -#nullPadding - 1)
return text, results
end
--[[
* 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)
end
return 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) then
return patches
end
local text = {}
for line in gmatch(textline, '([^\n]*)') do
text[#text + 1] = line
end
local textPointer = 1
while (textPointer <= #text) do
local start1, length1, start2, length2
= strmatch(text[textPointer], '^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@$')
if (start1 == nil) then
error('Invalid patch string: "' .. text[textPointer] .. '"')
end
local patch = _new_patch_obj()
patches[#patches + 1] = patch
start1 = tonumber(start1)
length1 = tonumber(length1) or 1
if (length1 == 0) then
start1 = start1 + 1
end
patch.start1 = start1
patch.length1 = length1
start2 = tonumber(start2)
length2 = tonumber(length2) or 1
if (length2 == 0) then
start2 = start2 + 1
end
patch.start2 = start2
patch.length2 = length2
textPointer = textPointer + 1
while true do
local line = text[textPointer]
if (line == nil) then
break
end
local sign; sign, line = strsub(line, 1, 1), strsub(line, 2)
local invalidDecode = false
local decoded = gsub(line, '%%(.?.?)',
function(c)
local n = tonumber(c, 16)
if (#c ~= 2) or (n == nil) then
invalidDecode = true
return ''
end
return strchar(n)
end)
if invalidDecode then
-- Malformed URI sequence.
error('Illegal escape in patch_fromText: ' .. line)
end
line = decoded
if (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.
break
elseif (sign == '') then
-- Blank line? Whatever.
else
-- WTF?
error('Invalid patch mode "' .. sign .. '" in: ' .. line)
end
textPointer = textPointer + 1
end
end
return patches
end
-- ---------------------------------------------------------------------------
-- 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) then
return
end
local 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 = nil
if (firstMatch ~= nil) then
secondMatch = indexOf(text, pattern, firstMatch + 1)
end
while (#pattern == 0 or secondMatch ~= nil)
and (#pattern < Match_MaxBits - Patch_Margin - Patch_Margin) do
padding = padding + Patch_Margin
pattern = strsub(text, max(1, patch.start2 - padding),
patch.start2 + patch.length1 - 1 + padding)
firstMatch = indexOf(text, pattern)
if (firstMatch ~= nil) then
secondMatch = indexOf(text, pattern, firstMatch + 1)
else
secondMatch = nil
end
end
-- 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) then
tinsert(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) then
patch.diffs[#patch.diffs + 1] = {DIFF_EQUAL, suffix}
end
-- Roll back the start points.
patch.start1 = patch.start1 - #prefix
patch.start2 = patch.start2 - #prefix
-- Extend the lengths.
patch.length1 = patch.length1 + #prefix + #suffix
patch.length2 = patch.length2 + #prefix + #suffix
end
--[[
* 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) do
local patchCopy = _new_patch_obj()
local diffsCopy = {}
for i, diff in ipairs(patch.diffs) do
diffsCopy[i] = {diff[1], diff[2]}
end
patchCopy.diffs = diffsCopy
patchCopy.start1 = patch.start1
patchCopy.start2 = patch.start2
patchCopy.length1 = patch.length1
patchCopy.length2 = patch.length2
patchesCopy[x] = patchCopy
end
return patchesCopy
end
--[[
* 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_Margin
local nullPadding = ''
for x = 1, paddingLength do
nullPadding = nullPadding .. strchar(x)
end
-- Bump all the patches forward.
for x, patch in ipairs(patches) do
patch.start1 = patch.start1 + paddingLength
patch.start2 = patch.start2 + paddingLength
end
-- Add some padding on start of first diff.
local patch = patches[1]
local diffs = patch.diffs
local 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 + paddingLength
patch.length2 = patch.length2 + paddingLength
elseif (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 - extraLength
patch.start2 = patch.start2 - extraLength
patch.length1 = patch.length1 + extraLength
patch.length2 = patch.length2 + extraLength
end
-- Add some padding on end of last diff.
patch = patches[#patches]
diffs = patch.diffs
local 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 + paddingLength
patch.length2 = patch.length2 + paddingLength
elseif (paddingLength > #lastDiff[2]) then
-- Grow last equality.
local extraLength = paddingLength - #lastDiff[2]
lastDiff[2] = lastDiff[2] .. strsub(nullPadding, 1, extraLength)
patch.length1 = patch.length1 + extraLength
patch.length2 = patch.length2 + extraLength
end
return nullPadding
end
--[[
* 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_MaxBits
local x = 1
while true do
local patch = patches[x]
if patch == nil then
return
end
if patch.length1 > patch_size then
local bigpatch = patch
-- Remove the big old patch.
tremove(patches, x)
x = x - 1
local start1 = bigpatch.start1
local start2 = bigpatch.start2
local precontext = ''
while bigpatch.diffs[1] do
-- Create one of several smaller patches.
local patch = _new_patch_obj()
local empty = true
patch.start1 = start1 - #precontext
patch.start2 = start2 - #precontext
if precontext ~= '' then
patch.length1, patch.length2 = #precontext, #precontext
patch.diffs[#patch.diffs + 1] = {DIFF_EQUAL, precontext}
end
while bigpatch.diffs[1] and (patch.length1 < patch_size-Patch_Margin) do
local 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_text
start2 = start2 + #diff_text
patch.diffs[#(patch.diffs) + 1] = bigpatch.diffs[1]
tremove(bigpatch.diffs, 1)
empty = false
elseif (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_text
start1 = start1 + #diff_text
empty = false
patch.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_text
start1 = start1 + #diff_text
if (diff_type == DIFF_EQUAL) then
patch.length2 = patch.length2 + #diff_text
start2 = start2 + #diff_text
else
empty = false
end
patch.diffs[#patch.diffs + 1] = {diff_type, diff_text}
if (diff_text == bigpatch.diffs[1][2]) then
tremove(bigpatch.diffs, 1)
else
bigpatch.diffs[1][2]
= strsub(bigpatch.diffs[1][2], #diff_text + 1)
end
end
end
-- 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 ~= '' then
patch.length1 = patch.length1 + #postcontext
patch.length2 = patch.length2 + #postcontext
if patch.diffs[1]
and (patch.diffs[#patch.diffs][1] == DIFF_EQUAL) then
patch.diffs[#patch.diffs][2] = patch.diffs[#patch.diffs][2]
.. postcontext
else
patch.diffs[#patch.diffs + 1] = {DIFF_EQUAL, postcontext}
end
end
if not empty then
x = x + 1
tinsert(patches, x, patch)
end
end
end
x = x + 1
end
end
--[[
* Emulate GNU diff's format.
* Header: @@ -382,8 +481,9 @@
* @return {string} The GNU diff string.
--]]
function _patch_appendText(patch, text)
local coords1, coords2
local length1, length2 = patch.length1, patch.length2
local start1, start2 = patch.start1, patch.start2
local diffs = patch.diffs
if length1 == 1 then
coords1 = start1
else
coords1 = ((length1 == 0) and (start1 - 1) or start1) .. ',' .. length1
end
if length2 == 1 then
coords2 = start2
else
coords2 = ((length2 == 0) and (start2 - 1) or start2) .. ',' .. length2
end
text[#text + 1] = '@@ -' .. coords1 .. ' +' .. coords2 .. ' @@\n'
local op
-- Escape the body of the patch with %xx notation.
for x, diff in ipairs(patch.diffs) do
local diff_type = diff[1]
if diff_type == DIFF_INSERT then
op = '+'
elseif diff_type == DIFF_DELETE then
op = '-'
elseif diff_type == DIFF_EQUAL then
op = ' '
end
text[#text + 1] = op
.. gsub(diffs[x][2], percentEncode_pattern, percentEncode_replace)
.. '\n'
end
return text
end
-- Expose the API
local _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 = settings
return _M
require 'site'
if not NylonSysCore then
require 'nylon.core'()
end
if not Pdcurses then
require 'LbindPdcurses'
end
if not NylonSqlite then
require 'NylonSqlite'
end
if not NylonOs then
pcall( 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 file
local 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 = isArray
local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject
function OBJDEF:newArray(tbl)
return setmetatable(tbl or {}, isArray)
end
function OBJDEF:newObject(tbl)
return setmetatable(tbl or {}, isObject)
end
local function unicode_codepoint_as_utf8(codepoint)
--
-- codepoint is a number
--
if codepoint <= 127 then
return 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 * highpart
local midpart = math.floor(remainder / 0x40)
local lowpart = remainder - 0x40 * midpart
highpart = 0xE0 + highpart
midpart = 0x80 + midpart
lowpart = 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 )
then
return "?"
else
return string.char(highpart,
midpart,
lowpart)
end
else
--
-- 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx
--
local highpart = math.floor(codepoint / 0x40000)
local remainder = codepoint - 0x40000 * highpart
local midA = math.floor(remainder / 0x1000)
remainder = remainder - 0x1000 * midA
local midB = math.floor(remainder / 0x40)
local lowpart = remainder - 0x40 * midB
return string.char(0xF0 + highpart,
0x80 + midA,
0x80 + midB,
0x80 + lowpart)
end
end
function OBJDEF:onDecodeError(message, text, location, etc)
if text then
if location then
message = string.format("%s at char %d of: %s", message, location, text)
else
message = string.format("%s: %s", message, text)
end
end
if etc ~= nil then
message = message .. " (" .. OBJDEF:encode(etc) .. ")"
end
if self.assert then
self.assert(false, message)
else
assert(false, message)
end
end
OBJDEF.onDecodeOfNilError = OBJDEF.onDecodeError
OBJDEF.onDecodeOfHTMLError = OBJDEF.onDecodeError
function OBJDEF:onEncodeError(message, etc)
if etc ~= nil then
message = message .. " (" .. OBJDEF:encode(etc) .. ")"
end
if self.assert then
self.assert(false, message)
else
assert(false, message)
end
end
local 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 then
self:onDecodeError("expected number", text, start, etc)
end
local 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_part
local as_number = tonumber(full_number_text)
if not as_number then
self:onDecodeError("bad number", text, start, etc)
end
return as_number, i
end
local function grok_string(self, text, start, etc)
if text:sub(start,start) ~= '"' then
self:onDecodeError("expected string's opening quote", text, start, etc)
end
local i = start + 1 -- +1 to bypass the initial quote
local text_len = text:len()
local VALUE = ""
while i <= text_len do
local c = text:sub(i,i)
if c == '"' then
return VALUE, i + 1
end
if c ~= '\\' then
VALUE = VALUE .. c
i = i + 1
elseif text:match('^\\b', i) then
VALUE = VALUE .. "\b"
i = i + 2
elseif text:match('^\\f', i) then
VALUE = VALUE .. "\f"
i = i + 2
elseif text:match('^\\n', i) then
VALUE = VALUE .. "\n"
i = i + 2
elseif text:match('^\\r', i) then
VALUE = VALUE .. "\r"
i = i + 2
elseif text:match('^\\t', i) then
VALUE = VALUE .. "\t"
i = i + 2
else
local hex = text:match('^\\u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i)
if hex then
i = 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 low
local lo_surrogate = text:match('^\\u([dD][cdefCDEF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i)
if lo_surrogate then
i = i + 6 -- bypass the low surrogate we just read
codepoint = 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.
end
end
VALUE = VALUE .. unicode_codepoint_as_utf8(codepoint)
else
-- just pass through what's escaped
VALUE = VALUE .. text:match('^\\(.)', i)
i = i + 2
end
end
end
self:onDecodeError("unclosed string", text, start, etc)
end
local function skip_whitespace(text, start)
local match_start, match_end = text:find("^[ \n\r\t]+", start) -- [http://www.ietf.org/rfc/rfc4627.txt] Section 2
if match_end then
return match_end + 1
else
return start
end
end
local grok_one -- assigned later
local function grok_object(self, text, start, etc)
if not text:sub(start,start) == '{' then
self:onDecodeError("expected '{'", text, start, etc)
end
local i = skip_whitespace(text, start + 1) -- +1 to skip the '{'
local VALUE = self.strictTypes and self:newObject { } or { }
if text:sub(i,i) == '}' then
return VALUE, i + 1
end
local text_len = text:len()
while i <= text_len do
local key, new_i = grok_string(self, text, i, etc)
i = skip_whitespace(text, new_i)
if text:sub(i, i) ~= ':' then
self:onDecodeError("expected colon", text, i, etc)
end
i = 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 == '}' then
return VALUE, i + 1
end
if text:sub(i, i) ~= ',' then
self:onDecodeError("expected comma or '}'", text, i, etc)
end
i = skip_whitespace(text, i + 1)
end
self:onDecodeError("unclosed '{'", text, start, etc)
end
local function grok_array(self, text, start, etc)
if not text:sub(start,start) == '[' then
self:onDecodeError("expected '['", text, start, etc)
end
local i = skip_whitespace(text, start + 1) -- +1 to skip the '['
local VALUE = self.strictTypes and self:newArray { } or { }
if text:sub(i,i) == ']' then
return VALUE, i + 1
end
local text_len = text:len()
while i <= text_len do
local 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 == ']' then
return VALUE, i + 1
end
if text:sub(i, i) ~= ',' then
self:onDecodeError("expected comma or '['", text, i, etc)
end
i = skip_whitespace(text, i + 1)
end
self:onDecodeError("unclosed '['", text, start, etc)
end
grok_one = function(self, text, start, etc)
-- Skip any whitespace
start = skip_whitespace(text, start)
if start > text:len() then
self:onDecodeError("unexpected end of string", text, nil, etc)
end
if text:find('^"', start) then
return grok_string(self, text, start, etc)
elseif text:find('^[-0123456789 ]', start) then
return grok_number(self, text, start, etc)
elseif text:find('^%{', start) then
return grok_object(self, text, start, etc)
elseif text:find('^%[', start) then
return grok_array(self, text, start, etc)
elseif text:find('^true', start) then
return true, start + 4
elseif text:find('^false', start) then
return false, start + 5
elseif text:find('^null', start) then
return nil, start + 4
else
self:onDecodeError("can't parse JSON", text, start, etc)
end
end
function OBJDEF:decode(text, etc)
if type(self) ~= 'table' or self.__index ~= OBJDEF then
OBJDEF:onDecodeError("JSON:decode must be called in method format", nil, nil, etc)
end
if text == nil then
self:onDecodeOfNilError(string.format("nil passed to JSON:decode()"), nil, nil, etc)
elseif type(text) ~= 'string' then
self:onDecodeError(string.format("expected string argument to JSON:decode(), got %s", type(text)), nil, nil, etc)
end
if text:match('^%s*$') then
return nil
end
if text:match('^%s*<') then
-- Can't be JSON... we'll assume it's HTML
self: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) then
self:onDecodeError("JSON package groks only UTF-8, sorry", text, nil, etc)
end
local success, value = pcall(grok_one, self, text, 1, etc)
if success then
return value
else
-- should never get here... JSON parse errors should have been caught earlier
assert(false, value)
return nil
end
end
local function backslash_replacement_function(c)
if c == "\n" then
return "\\n"
elseif c == "\r" then
return "\\r"
elseif c == "\t" then
return "\\t"
elseif c == "\b" then
return "\\b"
elseif c == "\f" then
return "\\f"
elseif c == '"' then
return '\\"'
elseif c == '\\' then
return '\\\\'
else
return string.format("\\u%04x", c:byte())
end
end
local 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 .. '"'
end
local 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 = false
local maximum_number_key
for key in pairs(T) do
if type(key) == 'string' then
table.insert(string_keys, key)
elseif type(key) == 'number' then
table.insert(number_keys, key)
if key <= 0 or key >= math.huge then
number_keys_must_be_strings = true
elseif not maximum_number_key or key > maximum_number_key then
maximum_number_key = key
end
else
self:onEncodeError("can't encode table with a key of type " .. type(key), etc)
end
end
if #string_keys == 0 and not number_keys_must_be_strings then
--
-- An empty table, or a numeric-only array
--
if #number_keys > 0 then
return nil, maximum_number_key -- an array
elseif tostring(T) == "JSON array" then
return nil
elseif tostring(T) == "JSON object" then
return { }
else
-- have to guess, so we'll pick array, since empty arrays are likely more common than empty objects
return nil
end
end
table.sort(string_keys)
local map
if #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 then
self: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) do
map[key] = val
end
table.sort(number_keys)
--
-- Throw numeric keys in there as strings
--
for _, number_key in ipairs(number_keys) do
local string_key = tostring(number_key)
if map[string_key] == nil then
table.insert(string_keys , string_key)
map[string_key] = T[number_key]
else
self:onEncodeError("conflict converting table with mixed-type keys into a JSON object: key " .. number_key .. " exists both as a string and a number.", etc)
end
end
end
return string_keys, nil, map
end
--
-- Encode
--
local encode_value -- must predeclare because it calls itself
function encode_value(self, value, parents, etc, indent) -- non-nil indent means pretty-printing
if value == nil then
return 'null'
elseif type(value) == 'string' then
return json_string_literal(value)
elseif type(value) == 'number' then
if 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"
else
return tostring(value)
end
elseif type(value) == 'boolean' then
return tostring(value)
elseif type(value) ~= 'table' then
self:onEncodeError("can't convert " .. type(value) .. " to JSON", etc)
else
--
-- A table to be converted to either a JSON object or array.
--
local T = value
if parents[T] then
self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc)
else
parents[T] = true
end
local result_value
local 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 do
table.insert(ITEMS, encode_value(self, T[i], parents, etc, indent))
end
if indent then
result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]"
else
result_value = "[" .. table.concat(ITEMS, ",") .. "]"
end
elseif object_keys then
--
-- An object
--
local TT = map or T
if indent then
local KEYS = { }
local max_key_length = 0
for _, key in ipairs(object_keys) do
local encoded = encode_value(self, tostring(key), parents, etc, "")
max_key_length = math.max(max_key_length, #encoded)
table.insert(KEYS, encoded)
end
local 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) do
local encoded_val = encode_value(self, TT[key], parents, etc, subtable_indent)
table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val))
end
result_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}"
else
local PARTS = { }
for _, key in ipairs(object_keys) do
local 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))
end
result_value = "{" .. table.concat(PARTS, ",") .. "}"
end
else
--
-- An empty array/object... we'll treat it as an array, though it should really be an option
--
result_value = "[]"
end
parents[T] = false
return result_value
end
end
function OBJDEF:encode(value, etc)
if type(self) ~= 'table' or self.__index ~= OBJDEF then
OBJDEF:onEncodeError("JSON:encode must be called in method format", etc)
end
return encode_value(self, value, {}, etc, nil)
end
function OBJDEF:encode_pretty(value, etc)
if type(self) ~= 'table' or self.__index ~= OBJDEF then
OBJDEF:onEncodeError("JSON:encode_pretty must be called in method format", etc)
end
return encode_value(self, value, {}, etc, "")
end
function OBJDEF.__tostring()
return "JSON encode/decode package"
end
OBJDEF.__index = OBJDEF
function OBJDEF:new(args)
local new = { }
if args then
for key, val in pairs(args) do
new[key] = val
end
end
return setmetatable(new, OBJDEF)
end
return 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
--