Updated stdlib to release 28.
Updated Penlight to 1.3.2. Updated SubLua to 1.8.10.master
parent
c7cf98f6a7
commit
f076d36e3c
|
@ -1,6 +1,11 @@
|
|||
=================== Lua For Windows ===================
|
||||
-=-=-=- Version 5.1.4-47
|
||||
03/18/2015 Version 5.1.4-47
|
||||
^ Updated stdlib to release 28.
|
||||
^ Updated Penlight to 1.3.2.
|
||||
^ Updated SubLua to 1.8.10.
|
||||
* Moved all downloads and code hosting to GitHub. Older
|
||||
releases will not function when Google Code is shut down.
|
||||
Make sure to upgrade.
|
||||
|
||||
08/07/2012 Version 5.1.4-46
|
||||
* Fixes #43 - require('lpeg') -- system error 14001
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,290 @@
|
|||
/* BEGIN RESET
|
||||
|
||||
Copyright (c) 2010, Yahoo! Inc. All rights reserved.
|
||||
Code licensed under the BSD License:
|
||||
http://developer.yahoo.com/yui/license.html
|
||||
version: 2.8.2r1
|
||||
*/
|
||||
html {
|
||||
color: #000;
|
||||
background: #FFF;
|
||||
}
|
||||
body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
fieldset,img {
|
||||
border: 0;
|
||||
}
|
||||
address,caption,cite,code,dfn,em,strong,th,var,optgroup {
|
||||
font-style: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
del,ins {
|
||||
text-decoration: none;
|
||||
}
|
||||
li {
|
||||
list-style: bullet;
|
||||
margin-left: 20px;
|
||||
}
|
||||
caption,th {
|
||||
text-align: left;
|
||||
}
|
||||
h1,h2,h3,h4,h5,h6 {
|
||||
font-size: 100%;
|
||||
font-weight: bold;
|
||||
}
|
||||
q:before,q:after {
|
||||
content: '';
|
||||
}
|
||||
abbr,acronym {
|
||||
border: 0;
|
||||
font-variant: normal;
|
||||
}
|
||||
sup {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
sub {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
legend {
|
||||
color: #000;
|
||||
}
|
||||
input,button,textarea,select,optgroup,option {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-style: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
input,button,textarea,select {*font-size:100%;
|
||||
}
|
||||
/* END RESET */
|
||||
|
||||
body {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
font-family: arial, helvetica, geneva, sans-serif;
|
||||
background-color: #ffffff; margin: 0px;
|
||||
}
|
||||
|
||||
code, tt { font-family: monospace; }
|
||||
span.parameter { font-family:monospace; }
|
||||
span.parameter:after { content:":"; }
|
||||
span.types:before { content:"("; }
|
||||
span.types:after { content:")"; }
|
||||
.type { font-weight: bold; font-style:italic }
|
||||
|
||||
body, p, td, th { font-size: .95em; line-height: 1.2em;}
|
||||
|
||||
p, ul { margin: 10px 0 0 10px;}
|
||||
|
||||
strong { font-weight: bold;}
|
||||
|
||||
em { font-style: italic;}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
h2, h3, h4 { margin: 15px 0 10px 0; }
|
||||
h2 { font-size: 1.25em; }
|
||||
h3 { font-size: 1.15em; }
|
||||
h4 { font-size: 1.06em; }
|
||||
|
||||
a:link { font-weight: bold; color: #004080; text-decoration: none; }
|
||||
a:visited { font-weight: bold; color: #006699; text-decoration: none; }
|
||||
a:link:hover { text-decoration: underline; }
|
||||
|
||||
hr {
|
||||
color:#cccccc;
|
||||
background: #00007f;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
blockquote { margin-left: 3em; }
|
||||
|
||||
ul { list-style-type: disc; }
|
||||
|
||||
p.name {
|
||||
font-family: "Andale Mono", monospace;
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
pre.example {
|
||||
background-color: rgb(245, 245, 245);
|
||||
border: 1px solid silver;
|
||||
padding: 10px;
|
||||
margin: 10px 0 10px 0;
|
||||
font-family: "Andale Mono", monospace;
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: rgb(245, 245, 245);
|
||||
border: 1px solid silver;
|
||||
padding: 10px;
|
||||
margin: 10px 0 10px 0;
|
||||
overflow: auto;
|
||||
font-family: "Andale Mono", monospace;
|
||||
}
|
||||
|
||||
|
||||
table.index { border: 1px #00007f; }
|
||||
table.index td { text-align: left; vertical-align: top; }
|
||||
|
||||
#container {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
#product {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #cccccc;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#product big {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
#main {
|
||||
background-color: #f0f0f0;
|
||||
border-left: 2px solid #cccccc;
|
||||
}
|
||||
|
||||
#navigation {
|
||||
float: left;
|
||||
width: 18em;
|
||||
vertical-align: top;
|
||||
background-color: #f0f0f0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#navigation h2 {
|
||||
background-color:#e7e7e7;
|
||||
font-size:1.1em;
|
||||
color:#000000;
|
||||
text-align: left;
|
||||
padding:0.2em;
|
||||
border-top:1px solid #dddddd;
|
||||
border-bottom:1px solid #dddddd;
|
||||
}
|
||||
|
||||
#navigation ul
|
||||
{
|
||||
font-size:1em;
|
||||
list-style-type: none;
|
||||
margin: 1px 1px 10px 1px;
|
||||
}
|
||||
|
||||
#navigation li {
|
||||
text-indent: -1em;
|
||||
display: block;
|
||||
margin: 3px 0px 0px 22px;
|
||||
}
|
||||
|
||||
#navigation li li a {
|
||||
margin: 0px 3px 0px -1em;
|
||||
}
|
||||
|
||||
#content {
|
||||
margin-left: 18em;
|
||||
padding: 1em;
|
||||
/*width: 700px;*/
|
||||
border-left: 2px solid #cccccc;
|
||||
border-right: 2px solid #cccccc;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#about {
|
||||
clear: both;
|
||||
padding: 5px;
|
||||
border-top: 2px solid #cccccc;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
font: 12pt "Times New Roman", "TimeNR", Times, serif;
|
||||
}
|
||||
a { font-weight: bold; color: #004080; text-decoration: underline; }
|
||||
|
||||
#main {
|
||||
background-color: #ffffff;
|
||||
border-left: 0px;
|
||||
}
|
||||
|
||||
#container {
|
||||
margin-left: 2%;
|
||||
margin-right: 2%;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 1em;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#navigation {
|
||||
display: none;
|
||||
}
|
||||
pre.example {
|
||||
font-family: "Andale Mono", monospace;
|
||||
font-size: 10pt;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
|
||||
table.module_list {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #cccccc;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.module_list td {
|
||||
border-width: 1px;
|
||||
padding: 3px;
|
||||
border-style: solid;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
table.module_list td.name { background-color: #f0f0f0; ; min-width: 200px; }
|
||||
table.module_list td.summary { width: 100%; }
|
||||
|
||||
|
||||
table.function_list {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #cccccc;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.function_list td {
|
||||
border-width: 1px;
|
||||
padding: 3px;
|
||||
border-style: solid;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
table.function_list td.name { background-color: #f0f0f0; ; min-width: 200px; }
|
||||
table.function_list td.summary { width: 100%; }
|
||||
|
||||
dl.table dt, dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;}
|
||||
dl.table dd, dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;}
|
||||
dl.table h3, dl.function h3 {font-size: .95em;}
|
||||
|
||||
/* stop sublists from having initial vertical space */
|
||||
ul ul { margin-top: 0px; }
|
||||
ol ul { margin-top: 0px; }
|
||||
ol ol { margin-top: 0px; }
|
||||
ul ol { margin-top: 0px; }
|
||||
|
||||
/* styles for prettification of source */
|
||||
.keyword {font-weight: bold; color: #6666AA; }
|
||||
.number { color: #AA6666; }
|
||||
.string { color: #8888AA; }
|
||||
.comment { color: #666600; }
|
||||
.prepro { color: #006666; }
|
||||
.global { color: #800080; }
|
|
@ -0,0 +1,610 @@
|
|||
#!/usr/bin/env lua
|
||||
---------------
|
||||
-- ## ldoc, a Lua documentation generator.
|
||||
--
|
||||
-- Compatible with luadoc-style annotations, but providing
|
||||
-- easier customization options.
|
||||
--
|
||||
-- C/C++ support for Lua extensions is provided.
|
||||
--
|
||||
-- Available from LuaRocks as 'ldoc' and as a [Zip file](http://stevedonovan.github.com/files/ldoc-1.3.0.zip)
|
||||
--
|
||||
-- [Github Page](https://github.com/stevedonovan/ldoc)
|
||||
--
|
||||
-- @author Steve Donovan
|
||||
-- @copyright 2011
|
||||
-- @license MIT/X11
|
||||
-- @script ldoc
|
||||
|
||||
local class = require 'pl.class'
|
||||
local app = require 'pl.app'
|
||||
local path = require 'pl.path'
|
||||
local dir = require 'pl.dir'
|
||||
local utils = require 'pl.utils'
|
||||
local List = require 'pl.List'
|
||||
local stringx = require 'pl.stringx'
|
||||
local tablex = require 'pl.tablex'
|
||||
|
||||
|
||||
local append = table.insert
|
||||
|
||||
local lapp = require 'pl.lapp'
|
||||
|
||||
-- so we can find our private modules
|
||||
app.require_here()
|
||||
|
||||
--- @usage
|
||||
local usage = [[
|
||||
ldoc, a documentation generator for Lua, vs 1.3.1
|
||||
-d,--dir (default docs) output directory
|
||||
-o,--output (default 'index') output name
|
||||
-v,--verbose verbose
|
||||
-a,--all show local functions, etc, in docs
|
||||
-q,--quiet suppress output
|
||||
-m,--module module docs as text
|
||||
-s,--style (default !) directory for style sheet (ldoc.css)
|
||||
-l,--template (default !) directory for template (ldoc.ltp)
|
||||
-1,--one use one-column output layout
|
||||
-p,--project (default ldoc) project name
|
||||
-t,--title (default Reference) page title
|
||||
-f,--format (default plain) formatting - can be markdown, discount or plain
|
||||
-b,--package (default .) top-level package basename (needed for module(...))
|
||||
-x,--ext (default html) output file extension
|
||||
-c,--config (default config.ld) configuration name
|
||||
-i,--ignore ignore any 'no doc comment or no module' warnings
|
||||
-D,--define (default none) set a flag to be used in config.ld
|
||||
-N,--nocolon don't treat colons specially
|
||||
-B,--boilerplate ignore first comment in source files
|
||||
--dump debug output dump
|
||||
--filter (default none) filter output as Lua data (e.g pl.pretty.dump)
|
||||
--tags (default none) show all references to given tags, comma-separated
|
||||
<file> (string) source file or directory containing source
|
||||
|
||||
`ldoc .` reads options from an `config.ld` file in same directory;
|
||||
`ldoc -c path/to/myconfig.ld .` reads options from `path/to/myconfig.ld`
|
||||
]]
|
||||
local args = lapp(usage)
|
||||
local lfs = require 'lfs'
|
||||
local doc = require 'ldoc.doc'
|
||||
local lang = require 'ldoc.lang'
|
||||
local tools = require 'ldoc.tools'
|
||||
local global = require 'ldoc.builtin.globals'
|
||||
local markup = require 'ldoc.markup'
|
||||
local parse = require 'ldoc.parse'
|
||||
local KindMap = tools.KindMap
|
||||
local Item,File,Module = doc.Item,doc.File,doc.Module
|
||||
local quit = utils.quit
|
||||
|
||||
|
||||
class.ModuleMap(KindMap)
|
||||
|
||||
function ModuleMap:_init ()
|
||||
self.klass = ModuleMap
|
||||
self.fieldname = 'section'
|
||||
end
|
||||
|
||||
ModuleMap:add_kind('function','Functions','Parameters')
|
||||
ModuleMap:add_kind('table','Tables','Fields')
|
||||
ModuleMap:add_kind('field','Fields')
|
||||
ModuleMap:add_kind('lfunction','Local Functions','Parameters')
|
||||
ModuleMap:add_kind('annotation','Issues')
|
||||
|
||||
|
||||
class.ProjectMap(KindMap)
|
||||
ProjectMap.project_level = true
|
||||
|
||||
function ProjectMap:_init ()
|
||||
self.klass = ProjectMap
|
||||
self.fieldname = 'type'
|
||||
end
|
||||
|
||||
ProjectMap:add_kind('module','Modules')
|
||||
ProjectMap:add_kind('script','Scripts')
|
||||
ProjectMap:add_kind('topic','Topics')
|
||||
ProjectMap:add_kind('example','Examples')
|
||||
|
||||
local lua, cc = lang.lua, lang.cc
|
||||
|
||||
local file_types = {
|
||||
['.lua'] = lua,
|
||||
['.ldoc'] = lua,
|
||||
['.luadoc'] = lua,
|
||||
['.c'] = cc,
|
||||
['.cpp'] = cc,
|
||||
['.cxx'] = cc,
|
||||
['.C'] = cc
|
||||
}
|
||||
|
||||
------- ldoc external API ------------
|
||||
|
||||
-- the ldoc table represents the API available in `config.ld`.
|
||||
local ldoc = {}
|
||||
local add_language_extension
|
||||
|
||||
local function override (field)
|
||||
if ldoc[field] ~= nil then args[field] = ldoc[field] end
|
||||
end
|
||||
|
||||
-- aliases to existing tags can be defined. E.g. just 'p' for 'param'
|
||||
function ldoc.alias (a,tag)
|
||||
doc.add_alias(a,tag)
|
||||
end
|
||||
|
||||
-- standard aliases --
|
||||
|
||||
ldoc.alias('tparam',{'param',modifiers={type="$1"}})
|
||||
ldoc.alias('treturn',{'return',modifiers={type="$1"}})
|
||||
ldoc.alias('tfield',{'field',modifiers={type="$1"}})
|
||||
|
||||
function ldoc.tparam_alias (name,type)
|
||||
type = type or name
|
||||
ldoc.alias(name,{'param',modifiers={type=type}})
|
||||
end
|
||||
|
||||
ldoc.tparam_alias 'string'
|
||||
ldoc.tparam_alias 'number'
|
||||
ldoc.tparam_alias 'int'
|
||||
ldoc.tparam_alias 'bool'
|
||||
ldoc.tparam_alias 'func'
|
||||
ldoc.tparam_alias 'tab'
|
||||
ldoc.tparam_alias 'thread'
|
||||
|
||||
function ldoc.add_language_extension(ext, lang)
|
||||
lang = (lang=='c' and cc) or (lang=='lua' and lua) or quit('unknown language')
|
||||
if ext:sub(1,1) ~= '.' then ext = '.'..ext end
|
||||
file_types[ext] = lang
|
||||
end
|
||||
|
||||
function ldoc.add_section (name, title, subname)
|
||||
ModuleMap:add_kind(name,title,subname)
|
||||
end
|
||||
|
||||
-- new tags can be added, which can be on a project level.
|
||||
function ldoc.new_type (tag, header, project_level)
|
||||
doc.add_tag(tag,doc.TAG_TYPE,project_level)
|
||||
if project_level then
|
||||
ProjectMap:add_kind(tag,header)
|
||||
else
|
||||
ModuleMap:add_kind(tag,header)
|
||||
end
|
||||
end
|
||||
|
||||
function ldoc.manual_url (url)
|
||||
global.set_manual_url(url)
|
||||
end
|
||||
|
||||
function ldoc.custom_see_handler(pat, handler)
|
||||
doc.add_custom_see_handler(pat, handler)
|
||||
end
|
||||
|
||||
local ldoc_contents = {
|
||||
'alias','add_language_extension','new_type','add_section', 'tparam_alias',
|
||||
'file','project','title','package','format','output','dir','ext', 'topics',
|
||||
'one','style','template','description','examples',
|
||||
'readme','all','manual_url', 'ignore', 'nocolon','boilerplate',
|
||||
'no_return_or_parms','no_summary','full_description','backtick_references', 'custom_see_handler',
|
||||
}
|
||||
ldoc_contents = tablex.makeset(ldoc_contents)
|
||||
|
||||
local function loadstr (ldoc,txt)
|
||||
local chunk, err
|
||||
local load
|
||||
-- Penlight's Lua 5.2 compatibility has wobbled over the years...
|
||||
if not rawget(_G,'loadin') then -- Penlight 0.9.5
|
||||
-- Penlight 0.9.7; no more global load() override
|
||||
load = load or utils.load
|
||||
chunk,err = load(txt,'config',nil,ldoc)
|
||||
else
|
||||
chunk,err = loadin(ldoc,txt)
|
||||
end
|
||||
return chunk, err
|
||||
end
|
||||
|
||||
-- any file called 'config.ld' found in the source tree will be
|
||||
-- handled specially. It will be loaded using 'ldoc' as the environment.
|
||||
local function read_ldoc_config (fname)
|
||||
local directory = path.dirname(fname)
|
||||
if directory == '' then
|
||||
directory = '.'
|
||||
end
|
||||
local chunk, err, ok
|
||||
if args.filter == 'none' then
|
||||
print('reading configuration from '..fname)
|
||||
end
|
||||
local txt,not_found = utils.readfile(fname)
|
||||
if txt then
|
||||
chunk, err = loadstr(ldoc,txt)
|
||||
if chunk then
|
||||
if args.define ~= 'none' then ldoc[args.define] = true end
|
||||
ok,err = pcall(chunk)
|
||||
end
|
||||
end
|
||||
if err then quit('error loading config file '..fname..': '..err) end
|
||||
for k in pairs(ldoc) do
|
||||
if not ldoc_contents[k] then
|
||||
quit("this config file field/function is unrecognized: "..k)
|
||||
end
|
||||
end
|
||||
return directory, not_found
|
||||
end
|
||||
|
||||
local quote = tools.quote
|
||||
--- processing command line and preparing for output ---
|
||||
|
||||
local F
|
||||
local file_list = List()
|
||||
File.list = file_list
|
||||
local config_dir
|
||||
|
||||
|
||||
local ldoc_dir = arg[0]:gsub('[^/\\]+$','')
|
||||
local doc_path = ldoc_dir..'/ldoc/builtin/?.lua'
|
||||
|
||||
-- ldoc -m is expecting a Lua package; this converts this to a file path
|
||||
if args.module then
|
||||
-- first check if we've been given a global Lua lib function
|
||||
if args.file:match '^%a+$' and global.functions[args.file] then
|
||||
args.file = 'global.'..args.file
|
||||
end
|
||||
local fullpath,mod,on_docpath = tools.lookup_existing_module_or_function (args.file, doc_path)
|
||||
if not fullpath then
|
||||
quit(mod)
|
||||
else
|
||||
args.nocolon = on_docpath
|
||||
args.file = fullpath
|
||||
args.module = mod
|
||||
end
|
||||
end
|
||||
|
||||
local abspath = tools.abspath
|
||||
|
||||
-- a special case: 'ldoc .' can get all its parameters from config.ld
|
||||
if args.file == '.' then
|
||||
local err
|
||||
config_dir,err = read_ldoc_config(args.config)
|
||||
if err then quit("no "..quote(args.config).." found") end
|
||||
local config_path = path.dirname(args.config)
|
||||
if config_path ~= '' then
|
||||
print('changing to directory',config_path)
|
||||
lfs.chdir(config_path)
|
||||
end
|
||||
config_is_read = true
|
||||
args.file = ldoc.file or '.'
|
||||
if args.file == '.' then
|
||||
args.file = lfs.currentdir()
|
||||
elseif type(args.file) == 'table' then
|
||||
for i,f in ipairs(args.file) do
|
||||
args.file[i] = abspath(f)
|
||||
print(args.file[i])
|
||||
end
|
||||
else
|
||||
args.file = abspath(args.file)
|
||||
end
|
||||
else
|
||||
args.file = abspath(args.file)
|
||||
end
|
||||
|
||||
local source_dir = args.file
|
||||
if type(source_dir) == 'table' then
|
||||
source_dir = source_dir[1]
|
||||
end
|
||||
if type(source_dir) == 'string' and path.isfile(source_dir) then
|
||||
source_dir = path.splitpath(source_dir)
|
||||
end
|
||||
|
||||
---------- specifying the package for inferring module names --------
|
||||
-- If you use module(...), or forget to explicitly use @module, then
|
||||
-- ldoc has to infer the module name. There are three sensible values for
|
||||
-- `args.package`:
|
||||
--
|
||||
-- * '.' the actual source is in an immediate subdir of the path given
|
||||
-- * '..' the path given points to the source directory
|
||||
-- * 'NAME' explicitly give the base module package name
|
||||
--
|
||||
|
||||
local function setup_package_base()
|
||||
if ldoc.package then args.package = ldoc.package end
|
||||
if args.package == '.' then
|
||||
args.package = source_dir
|
||||
elseif args.package == '..' then
|
||||
args.package = path.splitpath(source_dir)
|
||||
elseif not args.package:find '[\\/]' then
|
||||
local subdir,dir = path.splitpath(source_dir)
|
||||
if dir == args.package then
|
||||
args.package = subdir
|
||||
elseif path.isdir(path.join(source_dir,args.package)) then
|
||||
args.package = source_dir
|
||||
else
|
||||
quit("args.package is not the name of the source directory")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--------- processing files ---------------------
|
||||
-- ldoc may be given a file, or a directory. `args.file` may also be specified in config.ld
|
||||
-- where it is a list of files or directories. If specified on the command-line, we have
|
||||
-- to find an optional associated config.ld, if not already loaded.
|
||||
|
||||
if ldoc.ignore then args.ignore = true end
|
||||
|
||||
local function process_file (f, flist)
|
||||
local ext = path.extension(f)
|
||||
local ftype = file_types[ext]
|
||||
if ftype then
|
||||
if args.verbose then print(path.basename(f)) end
|
||||
local F,err = parse.file(f,ftype,args)
|
||||
if err then
|
||||
if F then
|
||||
F:warning("internal LDoc error")
|
||||
end
|
||||
quit(err)
|
||||
end
|
||||
flist:append(F)
|
||||
end
|
||||
end
|
||||
|
||||
local process_file_list = tools.process_file_list
|
||||
|
||||
setup_package_base()
|
||||
|
||||
|
||||
if type(args.file) == 'table' then
|
||||
-- this can only be set from config file so we can assume it's already read
|
||||
process_file_list(args.file,'*.*',process_file, file_list)
|
||||
if #file_list == 0 then quit "no source files specified" end
|
||||
elseif path.isdir(args.file) then
|
||||
local files = List(dir.getallfiles(args.file,'*.*'))
|
||||
-- use any configuration file we find, if not already specified
|
||||
if not config_dir then
|
||||
local config_files = files:filter(function(f)
|
||||
return path.basename(f) == args.config
|
||||
end)
|
||||
if #config_files > 0 then
|
||||
config_dir = read_ldoc_config(config_files[1])
|
||||
if #config_files > 1 then
|
||||
print('warning: other config files found: '..config_files[2])
|
||||
end
|
||||
end
|
||||
end
|
||||
for f in files:iter() do
|
||||
process_file(f, file_list)
|
||||
end
|
||||
if #file_list == 0 then
|
||||
quit(quote(args.file).." contained no source files")
|
||||
end
|
||||
elseif path.isfile(args.file) then
|
||||
-- a single file may be accompanied by a config.ld in the same dir
|
||||
if not config_dir then
|
||||
config_dir = path.dirname(args.file)
|
||||
if config_dir == '' then config_dir = '.' end
|
||||
local config = path.join(config_dir,args.config)
|
||||
if path.isfile(config) then
|
||||
read_ldoc_config(config)
|
||||
end
|
||||
end
|
||||
process_file(args.file, file_list)
|
||||
if #file_list == 0 then quit "unsupported file extension" end
|
||||
else
|
||||
quit ("file or directory does not exist: "..quote(args.file))
|
||||
end
|
||||
|
||||
-- create the function that renders text (descriptions and summaries)
|
||||
override 'format'
|
||||
ldoc.markup = markup.create(ldoc, args.format)
|
||||
|
||||
------ 'Special' Project-level entities ---------------------------------------
|
||||
-- Examples and Topics do not contain code to be processed for doc comments.
|
||||
-- Instead, they are intended to be rendered nicely as-is, whether as pretty-lua
|
||||
-- or as Markdown text. Treating them as 'modules' does stretch the meaning of
|
||||
-- of the term, but allows them to be treated much as modules or scripts.
|
||||
-- They define an item 'body' field (containing the file's text) and a 'postprocess'
|
||||
-- field which is used later to convert them into HTML. They may contain @{ref}s.
|
||||
|
||||
local function add_special_project_entity (f,tags,process)
|
||||
local F = File(f)
|
||||
tags.name = path.basename(f)
|
||||
local text = utils.readfile(f)
|
||||
local item = F:new_item(tags,1)
|
||||
if process then
|
||||
text = process(F, text)
|
||||
end
|
||||
F:finish()
|
||||
file_list:append(F)
|
||||
item.body = text
|
||||
return item, F
|
||||
end
|
||||
|
||||
if type(ldoc.examples) == 'string' then
|
||||
ldoc.examples = {ldoc.examples}
|
||||
end
|
||||
if type(ldoc.examples) == 'table' then
|
||||
local prettify = require 'ldoc.prettify'
|
||||
|
||||
process_file_list (ldoc.examples, '*.lua', function(f)
|
||||
local item = add_special_project_entity(f,{
|
||||
class = 'example',
|
||||
})
|
||||
-- wrap prettify for this example so it knows which file to blame
|
||||
-- if there's a problem
|
||||
item.postprocess = function(code) return prettify.lua(f,code) end
|
||||
end)
|
||||
end
|
||||
|
||||
ldoc.readme = ldoc.readme or ldoc.topics
|
||||
if type(ldoc.readme) == 'string' then
|
||||
ldoc.readme = {ldoc.readme}
|
||||
end
|
||||
if type(ldoc.readme) == 'table' then
|
||||
process_file_list(ldoc.readme, '*.md', function(f)
|
||||
local item, F = add_special_project_entity(f,{
|
||||
class = 'topic'
|
||||
}, markup.add_sections)
|
||||
-- add_sections above has created sections corresponding to the 2nd level
|
||||
-- headers in the readme, which are attached to the File. So
|
||||
-- we pass the File to the postprocesser, which will insert the section markers
|
||||
-- and resolve inline @ references.
|
||||
item.postprocess = function(txt) return ldoc.markup(txt,F) end
|
||||
end)
|
||||
end
|
||||
|
||||
-- extract modules from the file objects, resolve references and sort appropriately ---
|
||||
|
||||
local first_module
|
||||
local project = ProjectMap()
|
||||
local module_list = List()
|
||||
module_list.by_name = {}
|
||||
|
||||
local modcount = 0
|
||||
|
||||
for F in file_list:iter() do
|
||||
for mod in F.modules:iter() do
|
||||
if not first_module then first_module = mod end
|
||||
if doc.code_tag(mod.type) then modcount = modcount + 1 end
|
||||
module_list:append(mod)
|
||||
module_list.by_name[mod.name] = mod
|
||||
end
|
||||
end
|
||||
|
||||
for mod in module_list:iter() do
|
||||
if not args.module then -- no point if we're just showing docs on the console
|
||||
mod:resolve_references(module_list)
|
||||
end
|
||||
project:add(mod,module_list)
|
||||
end
|
||||
|
||||
-- the default is not to show local functions in the documentation.
|
||||
if not args.all and not ldoc.all then
|
||||
for mod in module_list:iter() do
|
||||
mod:mask_locals()
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(module_list,function(m1,m2)
|
||||
return m1.name < m2.name
|
||||
end)
|
||||
|
||||
ldoc.single = modcount == 1 and first_module or nil
|
||||
|
||||
|
||||
-------- three ways to dump the object graph after processing -----
|
||||
|
||||
-- ldoc -m will give a quick & dirty dump of the module's documentation;
|
||||
-- using -v will make it more verbose
|
||||
if args.module then
|
||||
if #module_list == 0 then quit("no modules found") end
|
||||
if args.module == true then
|
||||
file_list[1]:dump(args.verbose)
|
||||
else
|
||||
local fun = module_list[1].items.by_name[args.module]
|
||||
if not fun then quit(quote(args.module).." is not part of "..quote(args.file)) end
|
||||
fun:dump(true)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- ldoc --dump will do the same as -m, except for the currently specified files
|
||||
if args.dump then
|
||||
for mod in module_list:iter() do
|
||||
mod:dump(true)
|
||||
end
|
||||
os.exit()
|
||||
end
|
||||
if args.tags ~= 'none' then
|
||||
local tagset = {}
|
||||
for t in stringx.split(args.tags,','):iter() do
|
||||
tagset[t] = true
|
||||
end
|
||||
for mod in module_list:iter() do
|
||||
mod:dump_tags(tagset)
|
||||
end
|
||||
os.exit()
|
||||
end
|
||||
|
||||
-- ldoc --filter mod.name will load the module `mod` and pass the object graph
|
||||
-- to the function `name`. As a special case --filter dump will use pl.pretty.dump.
|
||||
if args.filter ~= 'none' then
|
||||
doc.filter_objects_through_function(args.filter, module_list)
|
||||
os.exit()
|
||||
end
|
||||
|
||||
ldoc.css, ldoc.templ = 'ldoc.css','ldoc.ltp'
|
||||
|
||||
local function style_dir (sname)
|
||||
local style = ldoc[sname]
|
||||
local dir
|
||||
if style then
|
||||
if style == true then
|
||||
dir = config_dir
|
||||
elseif type(style) == 'string' and path.isdir(style) then
|
||||
dir = style
|
||||
else
|
||||
quit(quote(tostring(name)).." is not a directory")
|
||||
end
|
||||
args[sname] = dir
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- the directories for template and stylesheet can be specified
|
||||
-- either by command-line '--template','--style' arguments or by 'template and
|
||||
-- 'style' fields in config.ld.
|
||||
-- The assumption here is that if these variables are simply true then the directory
|
||||
-- containing config.ld contains a ldoc.css and a ldoc.ltp respectively. Otherwise
|
||||
-- they must be a valid subdirectory.
|
||||
|
||||
style_dir 'style'
|
||||
style_dir 'template'
|
||||
|
||||
-- can specify format, output, dir and ext in config.ld
|
||||
override 'output'
|
||||
override 'dir'
|
||||
override 'ext'
|
||||
override 'one'
|
||||
override 'nocolon'
|
||||
override 'boilerplate'
|
||||
|
||||
if not args.ext:find '^%.' then
|
||||
args.ext = '.'..args.ext
|
||||
end
|
||||
|
||||
if args.one then
|
||||
ldoc.css = 'ldoc_one.css'
|
||||
end
|
||||
|
||||
if args.style == '!' or args.template == '!' then
|
||||
-- '!' here means 'use built-in templates'
|
||||
local tmpdir = path.join(path.is_windows and os.getenv('TMP') or '/tmp','ldoc')
|
||||
if not path.isdir(tmpdir) then
|
||||
lfs.mkdir(tmpdir)
|
||||
end
|
||||
local function tmpwrite (name)
|
||||
utils.writefile(path.join(tmpdir,name),require('ldoc.html.'..name:gsub('%.','_')))
|
||||
end
|
||||
if args.style == '!' then
|
||||
tmpwrite(ldoc.templ)
|
||||
args.style = tmpdir
|
||||
end
|
||||
if args.template == '!' then
|
||||
tmpwrite(ldoc.css)
|
||||
args.template = tmpdir
|
||||
end
|
||||
end
|
||||
|
||||
ldoc.log = print
|
||||
ldoc.kinds = project
|
||||
ldoc.modules = module_list
|
||||
ldoc.title = ldoc.title or args.title
|
||||
ldoc.project = ldoc.project or args.project
|
||||
ldoc.package = args.package:match '%a+' and args.package or nil
|
||||
|
||||
local html = require 'ldoc.html'
|
||||
|
||||
html.generate_output(ldoc, args, project)
|
||||
|
||||
if args.verbose then
|
||||
print 'modules'
|
||||
for k in pairs(module_list.by_name) do print(k) end
|
||||
end
|
||||
|
||||
|
|
@ -1,286 +0,0 @@
|
|||
body {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
font-family: arial, helvetica, geneva, sans-serif;
|
||||
background-color:#ffffff; margin:0px;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Andale Mono", monospace;
|
||||
}
|
||||
|
||||
tt {
|
||||
font-family: "Andale Mono", monospace;
|
||||
}
|
||||
|
||||
body, td, th { font-size: 11pt; }
|
||||
|
||||
h1, h2, h3, h4 { margin-left: 0em; }
|
||||
|
||||
textarea, pre, tt { font-size:10pt; }
|
||||
body, td, th { color:#000000; }
|
||||
small { font-size:0.85em; }
|
||||
h1 { font-size:1.5em; }
|
||||
h2 { font-size:1.25em; }
|
||||
h3 { font-size:1.15em; }
|
||||
h4 { font-size:1.06em; }
|
||||
|
||||
a:link { font-weight:bold; color: #004080; text-decoration: none; }
|
||||
a:visited { font-weight:bold; color: #006699; text-decoration: none; }
|
||||
a:link:hover { text-decoration:underline; }
|
||||
hr { color:#cccccc }
|
||||
img { border-width: 0px; }
|
||||
|
||||
|
||||
h3 { padding-top: 1em; }
|
||||
|
||||
p { margin-left: 1em; }
|
||||
|
||||
p.name {
|
||||
font-family: "Andale Mono", monospace;
|
||||
padding-top: 1em;
|
||||
margin-left: 0em;
|
||||
}
|
||||
|
||||
blockquote { margin-left: 3em; }
|
||||
|
||||
pre.example {
|
||||
background-color: rgb(245, 245, 245);
|
||||
border-top-width: 1px;
|
||||
border-right-width: 1px;
|
||||
border-bottom-width: 1px;
|
||||
border-left-width: 1px;
|
||||
border-top-style: solid;
|
||||
border-right-style: solid;
|
||||
border-bottom-style: solid;
|
||||
border-left-style: solid;
|
||||
border-top-color: silver;
|
||||
border-right-color: silver;
|
||||
border-bottom-color: silver;
|
||||
border-left-color: silver;
|
||||
padding: 1em;
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
font-family: "Andale Mono", monospace;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
|
||||
hr {
|
||||
margin-left: 0em;
|
||||
background: #00007f;
|
||||
border: 0px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
ul { list-style-type: disc; }
|
||||
|
||||
table.index { border: 1px #00007f; }
|
||||
table.index td { text-align: left; vertical-align: top; }
|
||||
table.index ul { padding-top: 0em; margin-top: 0em; }
|
||||
|
||||
table {
|
||||
border: 1px solid black;
|
||||
border-collapse: collapse;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
th {
|
||||
border: 1px solid black;
|
||||
padding: 0.5em;
|
||||
}
|
||||
td {
|
||||
border: 1px solid black;
|
||||
padding: 0.5em;
|
||||
}
|
||||
div.header, div.footer { margin-left: 0em; }
|
||||
|
||||
#container
|
||||
{
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
#product
|
||||
{
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #cccccc;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#product big {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
#product_logo
|
||||
{
|
||||
}
|
||||
|
||||
#product_name
|
||||
{
|
||||
}
|
||||
|
||||
#product_description
|
||||
{
|
||||
}
|
||||
|
||||
#main
|
||||
{
|
||||
background-color: #f0f0f0;
|
||||
border-left: 2px solid #cccccc;
|
||||
}
|
||||
|
||||
#navigation
|
||||
{
|
||||
float: left;
|
||||
width: 18em;
|
||||
margin: 0;
|
||||
vertical-align: top;
|
||||
background-color: #f0f0f0;
|
||||
overflow:visible;
|
||||
}
|
||||
|
||||
#navigation h1 {
|
||||
background-color:#e7e7e7;
|
||||
font-size:1.1em;
|
||||
color:#000000;
|
||||
text-align:left;
|
||||
margin:0px;
|
||||
padding:0.2em;
|
||||
border-top:1px solid #dddddd;
|
||||
border-bottom:1px solid #dddddd;
|
||||
}
|
||||
|
||||
#navigation ul
|
||||
{
|
||||
font-size:1em;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
#navigation li
|
||||
{
|
||||
text-indent: -1em;
|
||||
margin: 0em 0em 0em 0.5em;
|
||||
display: block;
|
||||
padding: 3px 0px 0px 12px;
|
||||
}
|
||||
|
||||
#navigation li li a
|
||||
{
|
||||
padding: 0px 3px 0px -1em;
|
||||
}
|
||||
|
||||
#content
|
||||
{
|
||||
margin-left: 18em;
|
||||
padding: 1em;
|
||||
border-left: 2px solid #cccccc;
|
||||
border-right: 2px solid #cccccc;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#about
|
||||
{
|
||||
clear: both;
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
border-top: 2px solid #cccccc;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
font: 12pt "Times New Roman", "TimeNR", Times, serif;
|
||||
}
|
||||
a { font-weight:bold; color: #004080; text-decoration: underline; }
|
||||
|
||||
#main
{
background-color: #ffffff;
border-left: 0px;
}
|
||||
#container
{
margin-left: 2%;
margin-right: 2%;
background-color: #ffffff;
}
|
||||
|
||||
#content
{
margin-left: 0px;
padding: 1em;
border-left: 0px;
border-right: 0px;
background-color: #ffffff;
}
|
||||
|
||||
#navigation
{
display: none;
|
||||
}
|
||||
pre.example {
|
||||
font-family: "Andale Mono", monospace;
|
||||
font-size: 10pt;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
|
||||
table.module_list td
|
||||
{
|
||||
border-width: 1px;
|
||||
padding: 3px;
|
||||
border-style: solid;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
table.module_list td.name { background-color: #f0f0f0; }
|
||||
table.module_list td.summary { width: 100%; }
|
||||
|
||||
table.file_list
|
||||
{
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #cccccc;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.file_list td
|
||||
{
|
||||
border-width: 1px;
|
||||
padding: 3px;
|
||||
border-style: solid;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
table.file_list td.name { background-color: #f0f0f0; }
|
||||
table.file_list td.summary { width: 100%; }
|
||||
|
||||
|
||||
table.function_list
|
||||
{
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #cccccc;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.function_list td
|
||||
{
|
||||
border-width: 1px;
|
||||
padding: 3px;
|
||||
border-style: solid;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
table.function_list td.name { background-color: #f0f0f0; }
|
||||
table.function_list td.summary { width: 100%; }
|
||||
|
||||
|
||||
table.table_list
|
||||
{
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #cccccc;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.table_list td
|
||||
{
|
||||
border-width: 1px;
|
||||
padding: 3px;
|
||||
border-style: solid;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
table.table_list td.name { background-color: #f0f0f0; }
|
||||
table.table_list td.summary { width: 100%; }
|
||||
|
||||
dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;}
|
||||
dl.function dd {padding-bottom: 1em;}
|
||||
dl.function h3 {padding: 0; margin: 0; font-size: medium;}
|
||||
|
||||
dl.table dt {border-top: 1px solid #ccc; padding-top: 1em;}
|
||||
dl.table dd {padding-bottom: 1em;}
|
||||
dl.table h3 {padding: 0; margin: 0; font-size: medium;}
|
||||
|
||||
#TODO: make module_list, file_list, function_list, table_list inherit from a list
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,7 @@
|
|||
-- See @{05-dates.md|the Guide}.
|
||||
--
|
||||
-- Dependencies: `pl.class`, `pl.stringx`
|
||||
-- @module pl.Date
|
||||
-- @classmod pl.Date
|
||||
-- @pragma nostrip
|
||||
|
||||
local class = require 'pl.class'
|
||||
|
@ -18,16 +18,18 @@ Date.Format = class()
|
|||
-- @param t this can be either
|
||||
--
|
||||
-- * `nil` or empty - use current date and time
|
||||
-- * number - seconds since epoch (as returned by @{os.time})
|
||||
-- * `Date` - copy constructor
|
||||
-- * number - seconds since epoch (as returned by `os.time`). Resulting time is UTC
|
||||
-- * `Date` - make a copy of this date
|
||||
-- * table - table containing year, month, etc as for `os.time`. You may leave out year, month or day,
|
||||
-- in which case current values will be used.
|
||||
-- *three to six numbers: year, month, day, hour, min, sec
|
||||
-- * year (will be followed by month, day etc)
|
||||
--
|
||||
-- @param ... true if Universal Coordinated Time, or two to five numbers: month,day,hour,min,sec
|
||||
-- @function Date
|
||||
function Date:_init(t,...)
|
||||
local time
|
||||
if select('#',...) > 2 then
|
||||
local nargs = select('#',...)
|
||||
if nargs > 2 then
|
||||
local extra = {...}
|
||||
local year = t
|
||||
t = {
|
||||
|
@ -39,18 +41,22 @@ function Date:_init(t,...)
|
|||
sec = extra[5]
|
||||
}
|
||||
end
|
||||
if t == nil then
|
||||
if nargs == 1 then
|
||||
self.utc = select(1,...) == true
|
||||
end
|
||||
if t == nil or t == 'utc' then
|
||||
time = os_time()
|
||||
self.utc = t == 'utc'
|
||||
elseif type(t) == 'number' then
|
||||
time = t
|
||||
local next = ...
|
||||
self.interval = next == true or next == 'interval'
|
||||
if self.utc == nil then self.utc = true end
|
||||
elseif type(t) == 'table' then
|
||||
if getmetatable(t) == Date then -- copy ctor
|
||||
time = t.time
|
||||
self.utc = t.utc
|
||||
else
|
||||
if not (t.year and t.month and t.year) then
|
||||
local lt = os.date('*t')
|
||||
if not (t.year and t.month) then
|
||||
local lt = os_date('*t')
|
||||
if not t.year and not t.month and not t.day then
|
||||
t.year = lt.year
|
||||
t.month = lt.month
|
||||
|
@ -61,92 +67,102 @@ function Date:_init(t,...)
|
|||
t.day = t.day or 1
|
||||
end
|
||||
end
|
||||
t.day = t.day or 1
|
||||
time = os_time(t)
|
||||
end
|
||||
else
|
||||
error("bad type for Date constructor: "..type(t),2)
|
||||
end
|
||||
self:set(time)
|
||||
end
|
||||
|
||||
local tzone_
|
||||
--- set the current time of this Date object.
|
||||
-- @int t seconds since epoch
|
||||
function Date:set(t)
|
||||
self.time = t
|
||||
if self.utc then
|
||||
self.tab = os_date('!*t',t)
|
||||
else
|
||||
self.tab = os_date('*t',t)
|
||||
end
|
||||
end
|
||||
|
||||
--- get the time zone offset from UTC.
|
||||
-- @return seconds ahead of UTC
|
||||
function Date.tzone ()
|
||||
if not tzone_ then
|
||||
local now = os.time()
|
||||
local utc = os.date('!*t',now)
|
||||
local lcl = os.date('*t',now)
|
||||
local unow = os.time(utc)
|
||||
tzone_ = os.difftime(now,unow)
|
||||
if lcl.isdst then
|
||||
if tzone_ > 0 then
|
||||
tzone_ = tzone_ - 3600
|
||||
else
|
||||
tzone_ = tzone_ + 3600
|
||||
end
|
||||
-- @int ts seconds ahead of UTC
|
||||
function Date.tzone (ts)
|
||||
if ts == nil then
|
||||
ts = os_time()
|
||||
elseif type(ts) == "table" then
|
||||
if getmetatable(ts) == Date then
|
||||
ts = ts.time
|
||||
else
|
||||
ts = Date(ts).time
|
||||
end
|
||||
end
|
||||
return tzone_
|
||||
local utc = os_date('!*t',ts)
|
||||
local lcl = os_date('*t',ts)
|
||||
lcl.isdst = false
|
||||
return os.difftime(os_time(lcl), os_time(utc))
|
||||
end
|
||||
|
||||
--- convert this date to UTC.
|
||||
function Date:toUTC ()
|
||||
self:add { sec = -Date.tzone() }
|
||||
local ndate = Date(self)
|
||||
if not self.utc then
|
||||
ndate.utc = true
|
||||
ndate:set(ndate.time)
|
||||
end
|
||||
return ndate
|
||||
end
|
||||
|
||||
--- convert this UTC date to local.
|
||||
function Date:toLocal ()
|
||||
self:add { sec = Date.tzone() }
|
||||
end
|
||||
|
||||
--- set the current time of this Date object.
|
||||
-- @param t seconds since epoch
|
||||
function Date:set(t)
|
||||
self.time = t
|
||||
if self.interval then
|
||||
self.tab = os_date('!*t',self.time)
|
||||
else
|
||||
self.tab = os_date('*t',self.time)
|
||||
local ndate = Date(self)
|
||||
if self.utc then
|
||||
ndate.utc = false
|
||||
ndate:set(ndate.time)
|
||||
--~ ndate:add { sec = Date.tzone(self) }
|
||||
end
|
||||
return ndate
|
||||
end
|
||||
|
||||
--- set the year.
|
||||
-- @param y Four-digit year
|
||||
-- @int y Four-digit year
|
||||
-- @class function
|
||||
-- @name Date:year
|
||||
|
||||
--- set the month.
|
||||
-- @param m month
|
||||
-- @int m month
|
||||
-- @class function
|
||||
-- @name Date:month
|
||||
|
||||
--- set the day.
|
||||
-- @param d day
|
||||
-- @int d day
|
||||
-- @class function
|
||||
-- @name Date:day
|
||||
|
||||
--- set the hour.
|
||||
-- @param h hour
|
||||
-- @int h hour
|
||||
-- @class function
|
||||
-- @name Date:hour
|
||||
|
||||
--- set the minutes.
|
||||
-- @param min minutes
|
||||
-- @int min minutes
|
||||
-- @class function
|
||||
-- @name Date:min
|
||||
|
||||
--- set the seconds.
|
||||
-- @param sec seconds
|
||||
-- @int sec seconds
|
||||
-- @class function
|
||||
-- @name Date:sec
|
||||
|
||||
--- set the day of year.
|
||||
-- @class function
|
||||
-- @param yday day of year
|
||||
-- @int yday day of year
|
||||
-- @name Date:yday
|
||||
|
||||
--- get the year.
|
||||
-- @param y Four-digit year
|
||||
-- @int y Four-digit year
|
||||
-- @class function
|
||||
-- @name Date:year
|
||||
|
||||
|
@ -189,32 +205,37 @@ for _,c in ipairs{'year','month','day','hour','min','sec','yday'} do
|
|||
end
|
||||
|
||||
--- name of day of week.
|
||||
-- @param full abbreviated if true, full otherwise.
|
||||
-- @return string name
|
||||
-- @bool full abbreviated if true, full otherwise.
|
||||
-- @ret string name
|
||||
function Date:weekday_name(full)
|
||||
return os_date(full and '%A' or '%a',self.time)
|
||||
end
|
||||
|
||||
--- name of month.
|
||||
-- @param full abbreviated if true, full otherwise.
|
||||
-- @return string name
|
||||
-- @int full abbreviated if true, full otherwise.
|
||||
-- @ret string name
|
||||
function Date:month_name(full)
|
||||
return os_date(full and '%B' or '%b',self.time)
|
||||
end
|
||||
|
||||
--- is this day on a weekend?.
|
||||
function Date:is_weekend()
|
||||
return self.tab.wday == 0 or self.tab.wday == 6
|
||||
return self.tab.wday == 1 or self.tab.wday == 7
|
||||
end
|
||||
|
||||
--- add to a date object.
|
||||
-- @param t a table containing one of the following keys and a value:<br>
|
||||
-- year,month,day,hour,min,sec
|
||||
-- @param t a table containing one of the following keys and a value:
|
||||
-- one of `year`,`month`,`day`,`hour`,`min`,`sec`
|
||||
-- @return this date
|
||||
function Date:add(t)
|
||||
local old_dst = self.tab.isdst
|
||||
local key,val = next(t)
|
||||
self.tab[key] = self.tab[key] + val
|
||||
self:set(os_time(self.tab))
|
||||
if old_dst ~= self.tab.isdst then
|
||||
self.tab.hour = self.tab.hour - (old_dst and 1 or -1)
|
||||
self:set(os_time(self.tab))
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
|
@ -232,35 +253,32 @@ function Date:last_day()
|
|||
end
|
||||
|
||||
--- difference between two Date objects.
|
||||
-- Note: currently the result is a regular @{Date} object,
|
||||
-- but also has `interval` field set, which means a more
|
||||
-- appropriate string rep is used.
|
||||
-- @param other Date object
|
||||
-- @return a Date object
|
||||
-- @tparam Date other Date object
|
||||
-- @treturn Date.Interval object
|
||||
function Date:diff(other)
|
||||
local dt = self.time - other.time
|
||||
if dt < 0 then error("date difference is negative!",2) end
|
||||
return Date(dt,true)
|
||||
return Date.Interval(dt)
|
||||
end
|
||||
|
||||
--- long numerical ISO data format version of this date.
|
||||
-- If it's an interval then the format is '2 hours 29 sec' etc.
|
||||
function Date:__tostring()
|
||||
if not self.interval then
|
||||
return os_date('%Y-%m-%d %H:%M:%S',self.time)
|
||||
local t = os_date('%Y-%m-%dT%H:%M:%S',self.time)
|
||||
if self.utc then
|
||||
return t .. 'Z'
|
||||
else
|
||||
local t, res = self.tab, ''
|
||||
local y,m,d = t.year - 1970, t.month - 1, t.day - 1
|
||||
if y > 0 then res = res .. y .. ' years ' end
|
||||
if m > 0 then res = res .. m .. ' months ' end
|
||||
if d > 0 then res = res .. d .. ' days ' end
|
||||
if y == 0 and m == 0 then
|
||||
local h = t.hour
|
||||
if h > 0 then res = res .. h .. ' hours ' end
|
||||
if t.min > 0 then res = res .. t.min .. ' min ' end
|
||||
if t.sec > 0 then res = res .. t.sec .. ' sec ' end
|
||||
local offs = self:tzone()
|
||||
if offs == 0 then
|
||||
return t .. 'Z'
|
||||
end
|
||||
local sign = offs > 0 and '+' or '-'
|
||||
local h = math.ceil(offs/3600)
|
||||
local m = (offs % 3600)/60
|
||||
if m == 0 then
|
||||
return t .. ('%s%02d'):format(sign,h)
|
||||
else
|
||||
return t .. ('%s%02d:%02d'):format(sign,h,m)
|
||||
end
|
||||
return res
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -269,11 +287,63 @@ function Date:__eq(other)
|
|||
return self.time == other.time
|
||||
end
|
||||
|
||||
--- equality between Date objects.
|
||||
--- ordering between Date objects.
|
||||
function Date:__lt(other)
|
||||
return self.time < other.time
|
||||
end
|
||||
|
||||
--- difference between Date objects.
|
||||
-- @function Date:__sub
|
||||
Date.__sub = Date.diff
|
||||
|
||||
--- add a date and an interval.
|
||||
-- @param other either a `Date.Interval` object or a table such as
|
||||
-- passed to `Date:add`
|
||||
function Date:__add(other)
|
||||
local nd = Date(self)
|
||||
if Date.Interval:class_of(other) then
|
||||
other = {sec=other.time}
|
||||
end
|
||||
nd:add(other)
|
||||
return nd
|
||||
end
|
||||
|
||||
Date.Interval = class(Date)
|
||||
|
||||
---- Date.Interval constructor
|
||||
-- @int t an interval in seconds
|
||||
-- @function Date.Interval
|
||||
function Date.Interval:_init(t)
|
||||
self:set(t)
|
||||
end
|
||||
|
||||
function Date.Interval:set(t)
|
||||
self.time = t
|
||||
self.tab = os_date('!*t',self.time)
|
||||
end
|
||||
|
||||
local function ess(n)
|
||||
if n > 1 then return 's '
|
||||
else return ' '
|
||||
end
|
||||
end
|
||||
|
||||
--- If it's an interval then the format is '2 hours 29 sec' etc.
|
||||
function Date.Interval:__tostring()
|
||||
local t, res = self.tab, ''
|
||||
local y,m,d = t.year - 1970, t.month - 1, t.day - 1
|
||||
if y > 0 then res = res .. y .. ' year'..ess(y) end
|
||||
if m > 0 then res = res .. m .. ' month'..ess(m) end
|
||||
if d > 0 then res = res .. d .. ' day'..ess(d) end
|
||||
if y == 0 and m == 0 then
|
||||
local h = t.hour
|
||||
if h > 0 then res = res .. h .. ' hour'..ess(h) end
|
||||
if t.min > 0 then res = res .. t.min .. ' min ' end
|
||||
if t.sec > 0 then res = res .. t.sec .. ' sec ' end
|
||||
end
|
||||
if res == '' then res = 'zero' end
|
||||
return res
|
||||
end
|
||||
|
||||
------------ Date.Format class: parsing and renderinig dates ------------
|
||||
|
||||
|
@ -287,34 +357,37 @@ local formats = {
|
|||
S = {'sec',{true,true}},
|
||||
}
|
||||
|
||||
--
|
||||
|
||||
--- Date.Format constructor.
|
||||
-- @param fmt. A string where the following fields are significant: <ul>
|
||||
-- <li>d day (either d or dd)</li>
|
||||
-- <li>y year (either yy or yyy)</li>
|
||||
-- <li>m month (either m or mm)</li>
|
||||
-- <li>H hour (either H or HH)</li>
|
||||
-- <li>M minute (either M or MM)</li>
|
||||
-- <li>S second (either S or SS)</li>
|
||||
-- </ul>
|
||||
-- @string fmt. A string where the following fields are significant:
|
||||
--
|
||||
-- * d day (either d or dd)
|
||||
-- * y year (either yy or yyy)
|
||||
-- * m month (either m or mm)
|
||||
-- * H hour (either H or HH)
|
||||
-- * M minute (either M or MM)
|
||||
-- * S second (either S or SS)
|
||||
--
|
||||
-- Alternatively, if fmt is nil then this returns a flexible date parser
|
||||
-- that tries various date/time schemes in turn:
|
||||
-- <ol>
|
||||
-- <li> <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>,
|
||||
-- like 2010-05-10 12:35:23Z or 2008-10-03T14:30+02<li>
|
||||
-- <li> times like 15:30 or 8.05pm (assumed to be today's date)</li>
|
||||
-- <li> dates like 28/10/02 (European order!) or 5 Feb 2012 </li>
|
||||
-- <li> month name like march or Mar (case-insensitive, first 3 letters);
|
||||
-- here the day will be 1 and the year this current year </li>
|
||||
-- </ol>
|
||||
--
|
||||
-- * [ISO 8601](http://en.wikipedia.org/wiki/ISO_8601), like `2010-05-10 12:35:23Z` or `2008-10-03T14:30+02`
|
||||
-- * times like 15:30 or 8.05pm (assumed to be today's date)
|
||||
-- * dates like 28/10/02 (European order!) or 5 Feb 2012
|
||||
-- * month name like march or Mar (case-insensitive, first 3 letters); here the
|
||||
-- day will be 1 and the year this current year
|
||||
--
|
||||
-- A date in format 3 can be optionally followed by a time in format 2.
|
||||
-- Please see test-date.lua in the tests folder for more examples.
|
||||
-- @usage df = Date.Format("yyyy-mm-dd HH:MM:SS")
|
||||
-- @class function
|
||||
-- @name Date.Format
|
||||
function Date.Format:_init(fmt)
|
||||
if not fmt then return end
|
||||
if not fmt then
|
||||
self.fmt = '%Y-%m-%d %H:%M:%S'
|
||||
self.outf = self.fmt
|
||||
self.plain = true
|
||||
return
|
||||
end
|
||||
local append = table.insert
|
||||
local D,PLUS,OPENP,CLOSEP = '\001','\002','\003','\004'
|
||||
local vars,used = {},{}
|
||||
|
@ -324,12 +397,12 @@ function Date.Format:_init(fmt)
|
|||
local ch = fmt:sub(i,i)
|
||||
local df = formats[ch]
|
||||
if df then
|
||||
if used[ch] then error("field appeared twice: "..ch,2) end
|
||||
if used[ch] then error("field appeared twice: "..ch,4) end
|
||||
used[ch] = true
|
||||
-- this field may be repeated
|
||||
local _,inext = fmt:find(ch..'+',i+1)
|
||||
local cnt = not _ and 1 or inext-i+1
|
||||
if not df[2][cnt] then error("wrong number of fields: "..ch,2) end
|
||||
if not df[2][cnt] then error("wrong number of fields: "..ch,4) end
|
||||
-- single chars mean 'accept more than one digit'
|
||||
local p = cnt==1 and (D..PLUS) or (D):rep(cnt)
|
||||
append(patt,OPENP..p..CLOSEP)
|
||||
|
@ -347,23 +420,23 @@ function Date.Format:_init(fmt)
|
|||
end
|
||||
end
|
||||
-- escape any magic characters
|
||||
fmt = table.concat(patt):gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1')
|
||||
fmt = utils.escape(table.concat(patt))
|
||||
-- fmt = table.concat(patt):gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1')
|
||||
-- replace markers with their magic equivalents
|
||||
fmt = fmt:gsub(D,'%%d'):gsub(PLUS,'+'):gsub(OPENP,'('):gsub(CLOSEP,')')
|
||||
self.fmt = fmt
|
||||
self.outf = table.concat(outf)
|
||||
self.vars = vars
|
||||
|
||||
end
|
||||
|
||||
local parse_date
|
||||
|
||||
--- parse a string into a Date object.
|
||||
-- @param str a date string
|
||||
-- @string str a date string
|
||||
-- @return date object
|
||||
function Date.Format:parse(str)
|
||||
assert_string(1,str)
|
||||
if not self.fmt then
|
||||
if self.plain then
|
||||
return parse_date(str,self.us)
|
||||
end
|
||||
local res = {str:match(self.fmt)}
|
||||
|
@ -375,11 +448,11 @@ function Date.Format:parse(str)
|
|||
end
|
||||
-- os.date() requires these fields; if not present, we assume
|
||||
-- that the time set is for the current day.
|
||||
if not (tab.year and tab.month and tab.year) then
|
||||
if not (tab.year and tab.month and tab.day) then
|
||||
local today = Date()
|
||||
tab.year = tab.year or today:year()
|
||||
tab.month = tab.month or today:month()
|
||||
tab.day = tab.day or today:month()
|
||||
tab.day = tab.day or today:day()
|
||||
end
|
||||
local Y = tab.year
|
||||
if Y < 100 then -- classic Y2K pivot
|
||||
|
@ -387,7 +460,6 @@ function Date.Format:parse(str)
|
|||
elseif not Y then
|
||||
tab.year = 1970
|
||||
end
|
||||
--dump(tab)
|
||||
return Date(tab)
|
||||
end
|
||||
|
||||
|
@ -396,18 +468,16 @@ end
|
|||
-- @return string
|
||||
function Date.Format:tostring(d)
|
||||
local tm = type(d) == 'number' and d or d.time
|
||||
if self.outf then
|
||||
return os.date(self.outf,tm)
|
||||
else
|
||||
return tostring(Date(d))
|
||||
end
|
||||
return os_date(self.outf,tm)
|
||||
end
|
||||
|
||||
--- force US order in dates like 9/11/2001
|
||||
function Date.Format:US_order(yesno)
|
||||
self.us = yesno
|
||||
end
|
||||
|
||||
local months = {jan=1,feb=2,mar=3,apr=4,may=5,jun=6,jul=7,aug=8,sep=9,oct=10,nov=11,dec=12}
|
||||
--local months = {jan=1,feb=2,mar=3,apr=4,may=5,jun=6,jul=7,aug=8,sep=9,oct=10,nov=11,dec=12}
|
||||
local months
|
||||
|
||||
--[[
|
||||
Allowed patterns:
|
||||
|
@ -473,11 +543,25 @@ local function parse_date_unsafe (s,US)
|
|||
end
|
||||
end
|
||||
if p and not year and is_number(p) then -- has to be date
|
||||
day = p
|
||||
nextp()
|
||||
if #p < 4 then
|
||||
day = p
|
||||
nextp()
|
||||
else -- unless it looks like a 24-hour time
|
||||
year = true
|
||||
end
|
||||
end
|
||||
if p and is_word(p) then
|
||||
p = p:sub(1,3)
|
||||
if not months then
|
||||
local ld, day1 = parse_date_unsafe '2000-12-31', {day=1}
|
||||
months = {}
|
||||
for i = 1,12 do
|
||||
ld = ld:last_day()
|
||||
ld:add(day1)
|
||||
local mon = ld:month_name():lower()
|
||||
months [mon] = i
|
||||
end
|
||||
end
|
||||
local mon = months[p]
|
||||
if mon then
|
||||
month = mon
|
||||
|
@ -515,6 +599,7 @@ local function parse_date_unsafe (s,US)
|
|||
end
|
||||
end
|
||||
local today
|
||||
if year == true then year = nil end
|
||||
if not (year and month and day) then
|
||||
today = Date()
|
||||
end
|
||||
|
@ -534,8 +619,9 @@ local function parse_date_unsafe (s,US)
|
|||
if tz then -- ISO 8601 UTC time
|
||||
res:add {hour = -tz.h}
|
||||
if tz.m ~= 0 then res:add {min = -tz.m} end
|
||||
res.utc = true
|
||||
-- we're in UTC, so let's go local...
|
||||
res:toLocal()
|
||||
res = res:toLocal()
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
@ -550,6 +636,5 @@ function parse_date (s)
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
return Date
|
||||
|
||||
|
|
|
@ -15,42 +15,29 @@
|
|||
-- Written for Lua version Nick Trout 4.0; Redone for Lua 5.1, Steve Donovan.
|
||||
--
|
||||
-- Dependencies: `pl.utils`, `pl.tablex`
|
||||
-- @module pl.List
|
||||
-- @classmod pl.List
|
||||
-- @pragma nostrip
|
||||
|
||||
local tinsert,tremove,concat,tsort = table.insert,table.remove,table.concat,table.sort
|
||||
local setmetatable, getmetatable,type,tostring,assert,string,next = setmetatable,getmetatable,type,tostring,assert,string,next
|
||||
local write = io.write
|
||||
local tablex = require 'pl.tablex'
|
||||
local filter,imap,imap2,reduce,transform,tremovevalues = tablex.filter,tablex.imap,tablex.imap2,tablex.reduce,tablex.transform,tablex.removevalues
|
||||
local tablex = tablex
|
||||
local tsub = tablex.sub
|
||||
local utils = require 'pl.utils'
|
||||
local function_arg = utils.function_arg
|
||||
local is_type = utils.is_type
|
||||
local split = utils.split
|
||||
local assert_arg = utils.assert_arg
|
||||
local class = require 'pl.class'
|
||||
|
||||
local array_tostring,split,assert_arg,function_arg = utils.array_tostring,utils.split,utils.assert_arg,utils.function_arg
|
||||
local normalize_slice = tablex._normalize_slice
|
||||
|
||||
--[[
|
||||
module ('pl.List',utils._module)
|
||||
]]
|
||||
|
||||
-- metatable for our list and map objects has already been defined..
|
||||
local Multimap = utils.stdmt.MultiMap
|
||||
-- metatable for our list objects
|
||||
local List = utils.stdmt.List
|
||||
List.__index = List
|
||||
List._class = List
|
||||
|
||||
local iter
|
||||
|
||||
-- we give the metatable its own metatable so that we can call it like a function!
|
||||
setmetatable(List,{
|
||||
__call = function (tbl,arg)
|
||||
return List.new(arg)
|
||||
end,
|
||||
})
|
||||
class(nil,nil,List)
|
||||
|
||||
-- we want the result to be _covariant_, i.e. t must have type of obj if possible
|
||||
local function makelist (t,obj)
|
||||
local klass = List
|
||||
if obj then
|
||||
|
@ -59,15 +46,16 @@ local function makelist (t,obj)
|
|||
return setmetatable(t,klass)
|
||||
end
|
||||
|
||||
local function is_list(t)
|
||||
return getmetatable(t) == List
|
||||
local function simple_table(t)
|
||||
return type(t) == 'table' and not getmetatable(t) and #t > 0
|
||||
end
|
||||
|
||||
local function simple_table(t)
|
||||
return type(t) == 'table' and not is_list(t) and #t > 0
|
||||
function List._create (src)
|
||||
if simple_table(src) then return src end
|
||||
end
|
||||
|
||||
function List:_init (src)
|
||||
if self == src then return end -- existing table used as self!
|
||||
if src then
|
||||
for v in iter(src) do
|
||||
tinsert(self,v)
|
||||
|
@ -76,73 +64,56 @@ function List:_init (src)
|
|||
end
|
||||
|
||||
--- Create a new list. Can optionally pass a table;
|
||||
-- passing another instance of List will cause a copy to be created
|
||||
-- passing another instance of List will cause a copy to be created;
|
||||
-- this will return a plain table with an appropriate metatable.
|
||||
-- we pass anything which isn't a simple table to iterate() to work out
|
||||
-- an appropriate iterator @see List.iterate
|
||||
-- @param t An optional list-like table
|
||||
-- an appropriate iterator
|
||||
-- @see List.iterate
|
||||
-- @param[opt] t An optional list-like table
|
||||
-- @return a new List
|
||||
-- @usage ls = List(); ls = List {1,2,3,4}
|
||||
function List.new(t)
|
||||
local ls
|
||||
if not simple_table(t) then
|
||||
ls = {}
|
||||
List._init(ls,t)
|
||||
else
|
||||
ls = t
|
||||
end
|
||||
makelist(ls)
|
||||
return ls
|
||||
end
|
||||
-- @function List.new
|
||||
|
||||
List.new = List
|
||||
|
||||
--- Make a copy of an existing list.
|
||||
-- The difference from a plain 'copy constructor' is that this returns
|
||||
-- the actual List subtype.
|
||||
function List:clone()
|
||||
local ls = makelist({},self)
|
||||
List._init(ls,self)
|
||||
ls:extend(self)
|
||||
return ls
|
||||
end
|
||||
|
||||
function List.default_map_with(T)
|
||||
return function(self,name)
|
||||
local f = T[name]
|
||||
if f then
|
||||
return function(self,...)
|
||||
return self:map(f,...)
|
||||
end
|
||||
else
|
||||
error("method not found: "..name,2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
---Add an item to the end of the list.
|
||||
-- @param i An item
|
||||
-- @return the list
|
||||
function List:append(i)
|
||||
tinsert(self,i)
|
||||
return self
|
||||
tinsert(self,i)
|
||||
return self
|
||||
end
|
||||
|
||||
List.push = tinsert
|
||||
|
||||
--- Extend the list by appending all the items in the given list.
|
||||
-- equivalent to 'a[len(a):] = L'.
|
||||
-- @param L Another List
|
||||
-- @tparam List L Another List
|
||||
-- @return the list
|
||||
function List:extend(L)
|
||||
assert_arg(1,L,'table')
|
||||
for i = 1,#L do tinsert(self,L[i]) end
|
||||
return self
|
||||
assert_arg(1,L,'table')
|
||||
for i = 1,#L do tinsert(self,L[i]) end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Insert an item at a given position. i is the index of the
|
||||
-- element before which to insert.
|
||||
-- @param i index of element before whichh to insert
|
||||
-- @int i index of element before whichh to insert
|
||||
-- @param x A data item
|
||||
-- @return the list
|
||||
function List:insert(i, x)
|
||||
assert_arg(1,i,'number')
|
||||
tinsert(self,i,x)
|
||||
return self
|
||||
assert_arg(1,i,'number')
|
||||
tinsert(self,i,x)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Insert an item at the begining of the list.
|
||||
|
@ -154,7 +125,7 @@ end
|
|||
|
||||
--- Remove an element given its index.
|
||||
-- (equivalent of Python's del s[i])
|
||||
-- @param i the index
|
||||
-- @int i the index
|
||||
-- @return the list
|
||||
function List:remove (i)
|
||||
assert_arg(1,i,'number')
|
||||
|
@ -178,7 +149,7 @@ function List:remove_value(x)
|
|||
--- Remove the item at the given position in the list, and return it.
|
||||
-- If no index is specified, a:pop() returns the last item in the list.
|
||||
-- The item is also removed from the list.
|
||||
-- @param i An index
|
||||
-- @int[opt] i An index
|
||||
-- @return the item
|
||||
function List:pop(i)
|
||||
if not i then i = #self end
|
||||
|
@ -190,10 +161,9 @@ List.get = List.pop
|
|||
|
||||
--- Return the index in the list of the first item whose value is given.
|
||||
-- Return nil if there is no such item.
|
||||
-- @class function
|
||||
-- @name List:index
|
||||
-- @function List:index
|
||||
-- @param x A data value
|
||||
-- @param idx where to start search (default 1)
|
||||
-- @int[opt=1] idx where to start search
|
||||
-- @return the index, or nil if not found.
|
||||
|
||||
local tfind = tablex.find
|
||||
|
@ -218,7 +188,7 @@ function List:count(x)
|
|||
end
|
||||
|
||||
--- Sort the items of the list, in place.
|
||||
-- @param cmp an optional comparison function, default '<'
|
||||
-- @func[opt='<'] cmp an optional comparison function
|
||||
-- @return the list
|
||||
function List:sort(cmp)
|
||||
if cmp then cmp = function_arg(1,cmp) end
|
||||
|
@ -227,7 +197,7 @@ function List:sort(cmp)
|
|||
end
|
||||
|
||||
--- return a sorted copy of this list.
|
||||
-- @param cmp an optional comparison function, default '<'
|
||||
-- @func[opt='<'] cmp an optional comparison function
|
||||
-- @return a new list
|
||||
function List:sorted(cmp)
|
||||
return List(self):sort(cmp)
|
||||
|
@ -281,43 +251,43 @@ local eps = 1.0e-10
|
|||
|
||||
--- Emulate Python's range(x) function.
|
||||
-- Include it in List table for tidiness
|
||||
-- @param start A number
|
||||
-- @param finish A number greater than start; if absent,
|
||||
-- @int start A number
|
||||
-- @int[opt] finish A number greater than start; if absent,
|
||||
-- then start is 1 and finish is start
|
||||
-- @param incr an optional increment (may be less than 1)
|
||||
-- @int[opt=1] incr an increment (may be less than 1)
|
||||
-- @return a List from start .. finish
|
||||
-- @usage List.range(0,3) == List{0,1,2,3}
|
||||
-- @usage List.range(4) = List{1,2,3,4}
|
||||
-- @usage List.range(5,1,-1) == List{5,4,3,2,1}
|
||||
function List.range(start,finish,incr)
|
||||
if not finish then
|
||||
finish = start
|
||||
start = 1
|
||||
end
|
||||
if incr then
|
||||
if not finish then
|
||||
finish = start
|
||||
start = 1
|
||||
end
|
||||
if incr then
|
||||
assert_arg(3,incr,'number')
|
||||
if not utils.is_integer(incr) then finish = finish + eps end
|
||||
else
|
||||
incr = 1
|
||||
end
|
||||
assert_arg(1,start,'number')
|
||||
assert_arg(2,finish,'number')
|
||||
local t = List.new()
|
||||
for i=start,finish,incr do tinsert(t,i) end
|
||||
return t
|
||||
if math.ceil(incr) ~= incr then finish = finish + eps end
|
||||
else
|
||||
incr = 1
|
||||
end
|
||||
assert_arg(1,start,'number')
|
||||
assert_arg(2,finish,'number')
|
||||
local t = List()
|
||||
for i=start,finish,incr do tinsert(t,i) end
|
||||
return t
|
||||
end
|
||||
|
||||
--- list:len() is the same as #list.
|
||||
function List:len()
|
||||
return #self
|
||||
return #self
|
||||
end
|
||||
|
||||
-- Extended operations --
|
||||
|
||||
--- Remove a subrange of elements.
|
||||
-- equivalent to 'del s[i1:i2]' in Python.
|
||||
-- @param i1 start of range
|
||||
-- @param i2 end of range
|
||||
-- @int i1 start of range
|
||||
-- @int i2 end of range
|
||||
-- @return the list
|
||||
function List:chop(i1,i2)
|
||||
return tremovevalues(self,i1,i2)
|
||||
|
@ -325,8 +295,8 @@ end
|
|||
|
||||
--- Insert a sublist into a list
|
||||
-- equivalent to 's[idx:idx] = list' in Python
|
||||
-- @param idx index
|
||||
-- @param list list to insert
|
||||
-- @int idx index
|
||||
-- @tparam List list list to insert
|
||||
-- @return the list
|
||||
-- @usage l = List{10,20}; l:splice(2,{21,22}); assert(l == List{10,21,22,20})
|
||||
function List:splice(idx,list)
|
||||
|
@ -341,9 +311,9 @@ function List:splice(idx,list)
|
|||
end
|
||||
|
||||
--- general slice assignment s[i1:i2] = seq.
|
||||
-- @param i1 start index
|
||||
-- @param i2 end index
|
||||
-- @param seq a list
|
||||
-- @int i1 start index
|
||||
-- @int i2 end index
|
||||
-- @tparam List seq a list
|
||||
-- @return the list
|
||||
function List:slice_assign(i1,i2,seq)
|
||||
assert_arg(1,i1,'number')
|
||||
|
@ -355,7 +325,8 @@ function List:slice_assign(i1,i2,seq)
|
|||
end
|
||||
|
||||
--- concatenation operator.
|
||||
-- @param L another List
|
||||
-- @within metamethods
|
||||
-- @tparam List L another List
|
||||
-- @return a new list consisting of the list with the elements of the new list appended
|
||||
function List:__concat(L)
|
||||
assert_arg(1,L,'table')
|
||||
|
@ -365,7 +336,8 @@ function List:__concat(L)
|
|||
end
|
||||
|
||||
--- equality operator ==. True iff all elements of two lists are equal.
|
||||
-- @param L another List
|
||||
-- @within metamethods
|
||||
-- @tparam List L another List
|
||||
-- @return true or false
|
||||
function List:__eq(L)
|
||||
if #self ~= #L then return false end
|
||||
|
@ -375,21 +347,20 @@ function List:__eq(L)
|
|||
return true
|
||||
end
|
||||
|
||||
--- join the elements of a list using a delimiter. <br>
|
||||
--- join the elements of a list using a delimiter.
|
||||
-- This method uses tostring on all elements.
|
||||
-- @param delim a delimiter string, can be empty.
|
||||
-- @string[opt=''] delim a delimiter string, can be empty.
|
||||
-- @return a string
|
||||
function List:join (delim)
|
||||
delim = delim or ''
|
||||
assert_arg(1,delim,'string')
|
||||
return concat(imap(tostring,self),delim)
|
||||
return concat(array_tostring(self),delim)
|
||||
end
|
||||
|
||||
--- join a list of strings. <br>
|
||||
-- Uses table.concat directly.
|
||||
-- @class function
|
||||
-- @name List:concat
|
||||
-- @param delim a delimiter
|
||||
-- Uses `table.concat` directly.
|
||||
-- @function List:concat
|
||||
-- @string[opt=''] delim a delimiter
|
||||
-- @return a string
|
||||
List.concat = concat
|
||||
|
||||
|
@ -402,73 +373,50 @@ local function tostring_q(val)
|
|||
end
|
||||
|
||||
--- how our list should be rendered as a string. Uses join().
|
||||
-- @within metamethods
|
||||
-- @see List:join
|
||||
function List:__tostring()
|
||||
return '{'..self:join(',',tostring_q)..'}'
|
||||
end
|
||||
|
||||
--[[
|
||||
-- NOTE: this works, but is unreliable. If you leave the loop before finishing,
|
||||
-- then the iterator is not reset.
|
||||
--- can iterate over a list directly.
|
||||
-- @usage for v in ls do print(v) end
|
||||
function List:__call()
|
||||
if not self.key then self.key = 1 end
|
||||
local value = self[self.key]
|
||||
self.key = self.key + 1
|
||||
if not value then self.key = nil end
|
||||
return value
|
||||
end
|
||||
--]]
|
||||
|
||||
--[[
|
||||
function List.__call(t,v,i)
|
||||
i = (i or 0) + 1
|
||||
v = t[i]
|
||||
if v then return i, v end
|
||||
end
|
||||
--]]
|
||||
|
||||
local MethodIter = {}
|
||||
|
||||
function MethodIter:__index (name)
|
||||
return function(mm,...)
|
||||
return self.list:foreachm(name,...)
|
||||
end
|
||||
end
|
||||
|
||||
--- call the function for each element of the list.
|
||||
-- @param fun a function or callable object
|
||||
--- call the function on each element of the list.
|
||||
-- @func fun a function or callable object
|
||||
-- @param ... optional values to pass to function
|
||||
function List:foreach (fun,...)
|
||||
if fun==nil then
|
||||
return setmetatable({list=self},MethodIter)
|
||||
end
|
||||
fun = function_arg(1,fun)
|
||||
for i = 1,#self do
|
||||
fun(self[i],...)
|
||||
end
|
||||
end
|
||||
|
||||
local function lookup_fun (obj,name)
|
||||
local f = obj[name]
|
||||
if not f then error(type(obj).." does not have method "..name,3) end
|
||||
return f
|
||||
end
|
||||
|
||||
--- call the named method on each element of the list.
|
||||
-- @string name the method name
|
||||
-- @param ... optional values to pass to function
|
||||
function List:foreachm (name,...)
|
||||
for i = 1,#self do
|
||||
local obj = self[i]
|
||||
local f = assert(obj[name],"method not found on object")
|
||||
local f = lookup_fun(obj,name)
|
||||
f(obj,...)
|
||||
end
|
||||
end
|
||||
|
||||
--- create a list of all elements which match a function.
|
||||
-- @param fun a boolean function
|
||||
-- @param arg optional argument to be passed as second argument of the predicate
|
||||
-- @func fun a boolean function
|
||||
-- @param[opt] arg optional argument to be passed as second argument of the predicate
|
||||
-- @return a new filtered list.
|
||||
function List:filter (fun,arg)
|
||||
return makelist(filter(self,fun,arg),self)
|
||||
end
|
||||
|
||||
--- split a string using a delimiter.
|
||||
-- @param s the string
|
||||
-- @param delim the delimiter (default spaces)
|
||||
-- @string s the string
|
||||
-- @string[opt] delim the delimiter (default spaces)
|
||||
-- @return a List of strings
|
||||
-- @see pl.utils.split
|
||||
function List.split (s,delim)
|
||||
|
@ -476,34 +424,20 @@ function List.split (s,delim)
|
|||
return makelist(split(s,delim))
|
||||
end
|
||||
|
||||
local MethodMapper = {}
|
||||
|
||||
function MethodMapper:__index (name)
|
||||
return function(mm,...)
|
||||
return self.list:mapm(name,...)
|
||||
end
|
||||
end
|
||||
|
||||
--- apply a function to all elements.
|
||||
-- Any extra arguments will be passed to the function; if the function
|
||||
-- is `nil` then `map` returns a mapper object that maps over a method
|
||||
-- of the items
|
||||
-- @param fun a function of at least one argument
|
||||
-- Any extra arguments will be passed to the function.
|
||||
-- @func fun a function of at least one argument
|
||||
-- @param ... arbitrary extra arguments.
|
||||
-- @return a new list: {f(x) for x in self}
|
||||
-- @usage List{'one','two'}:map(string.upper) == {'ONE','TWO'}
|
||||
-- @usage List{'one','two'}:map():sub(1,2) == {'on','tw'}
|
||||
-- @see pl.tablex.imap
|
||||
function List:map (fun,...)
|
||||
if fun==nil then
|
||||
return setmetatable({list=self},MethodMapper)
|
||||
end
|
||||
return makelist(imap(fun,self,...),self)
|
||||
end
|
||||
|
||||
--- apply a function to all elements, in-place.
|
||||
-- Any extra arguments are passed to the function.
|
||||
-- @param fun A function that takes at least one argument
|
||||
-- @func fun A function that takes at least one argument
|
||||
-- @param ... arbitrary extra arguments.
|
||||
-- @return the list.
|
||||
function List:transform (fun,...)
|
||||
|
@ -513,8 +447,8 @@ end
|
|||
|
||||
--- apply a function to elements of two lists.
|
||||
-- Any extra arguments will be passed to the function
|
||||
-- @param fun a function of at least two arguments
|
||||
-- @param ls another list
|
||||
-- @func fun a function of at least two arguments
|
||||
-- @tparam List ls another list
|
||||
-- @param ... arbitrary extra arguments.
|
||||
-- @return a new list: {f(x,y) for x in self, for x in arg1}
|
||||
-- @see pl.tablex.imap2
|
||||
|
@ -524,24 +458,44 @@ end
|
|||
|
||||
--- apply a named method to all elements.
|
||||
-- Any extra arguments will be passed to the method.
|
||||
-- @param name name of method
|
||||
-- @string name name of method
|
||||
-- @param ... extra arguments
|
||||
-- @return a new list of the results
|
||||
-- @see pl.seq.mapmethod
|
||||
function List:mapm (name,...)
|
||||
local res = {}
|
||||
local t = self
|
||||
for i = 1,#t do
|
||||
local val = t[i]
|
||||
local fn = val[name]
|
||||
if not fn then error(type(val).." does not have method "..name,2) end
|
||||
for i = 1,#self do
|
||||
local val = self[i]
|
||||
local fn = lookup_fun(val,name)
|
||||
res[i] = fn(val,...)
|
||||
end
|
||||
return makelist(res,self)
|
||||
end
|
||||
|
||||
local function composite_call (method,f)
|
||||
return function(self,...)
|
||||
return self[method](self,f,...)
|
||||
end
|
||||
end
|
||||
|
||||
function List.default_map_with(T)
|
||||
return function(self,name)
|
||||
local m
|
||||
if T then
|
||||
local f = lookup_fun(T,name)
|
||||
m = composite_call('map',f)
|
||||
else
|
||||
m = composite_call('mapn',name)
|
||||
end
|
||||
getmetatable(self)[name] = m -- and cache..
|
||||
return m
|
||||
end
|
||||
end
|
||||
|
||||
List.default_map = List.default_map_with
|
||||
|
||||
--- 'reduce' a list using a binary function.
|
||||
-- @param fun a function of two arguments
|
||||
-- @func fun a function of two arguments
|
||||
-- @return result of the function
|
||||
-- @see pl.tablex.reduce
|
||||
function List:reduce (fun)
|
||||
|
@ -550,10 +504,10 @@ end
|
|||
|
||||
--- partition a list using a classifier function.
|
||||
-- The function may return nil, but this will be converted to the string key '<nil>'.
|
||||
-- @param fun a function of at least one argument
|
||||
-- @func fun a function of at least one argument
|
||||
-- @param ... will also be passed to the function
|
||||
-- @return a table where the keys are the returned values, and the values are Lists
|
||||
-- of values where the function returned that key. It is given the type of Multimap.
|
||||
-- @treturn MultiMap a table where the keys are the returned values, and the values are Lists
|
||||
-- of values where the function returned that key.
|
||||
-- @see pl.MultiMap
|
||||
function List:partition (fun,...)
|
||||
fun = function_arg(1,fun)
|
||||
|
|
|
@ -7,12 +7,11 @@
|
|||
-- true
|
||||
--
|
||||
-- Dependencies: `pl.utils`, `pl.class`, `pl.tablex`, `pl.pretty`
|
||||
-- @module pl.Map
|
||||
-- @classmod pl.Map
|
||||
|
||||
local tablex = require 'pl.tablex'
|
||||
local utils = require 'pl.utils'
|
||||
local stdmt = utils.stdmt
|
||||
local is_callable = utils.is_callable
|
||||
local tmakeset,deepcompare,merge,keys,difference,tupdate = tablex.makeset,tablex.deepcompare,tablex.merge,tablex.keys,tablex.difference,tablex.update
|
||||
|
||||
local pretty_write = require 'pl.pretty' . write
|
||||
|
@ -97,15 +96,20 @@ function Map:getvalues (keys)
|
|||
end
|
||||
|
||||
--- update the map using key/value pairs from another table.
|
||||
-- @param table
|
||||
-- @tab table
|
||||
-- @function Map:update
|
||||
Map.update = tablex.update
|
||||
|
||||
--- equality between maps.
|
||||
-- @within metamethods
|
||||
-- @tparam Map m another map.
|
||||
function Map:__eq (m)
|
||||
-- note we explicitly ask deepcompare _not_ to use __eq!
|
||||
return deepcompare(self,m,true)
|
||||
end
|
||||
|
||||
--- string representation of a map.
|
||||
-- @within metamethods
|
||||
function Map:__tostring ()
|
||||
return pretty_write(self,'')
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
--- MultiMap, a Map which has multiple values per key.
|
||||
--
|
||||
-- Dependencies: `pl.utils`, `pl.class`, `pl.tablex`, `pl.List`
|
||||
-- @module pl.MultiMap
|
||||
-- @classmod pl.MultiMap
|
||||
|
||||
local classes = require 'pl.class'
|
||||
local tablex = require 'pl.tablex'
|
||||
|
@ -11,7 +11,6 @@ local List = require 'pl.List'
|
|||
local index_by,tsort,concat = tablex.index_by,table.sort,table.concat
|
||||
local append,extend,slice = List.append,List.extend,List.slice
|
||||
local append = table.insert
|
||||
local is_type = utils.is_type
|
||||
|
||||
local class = require 'pl.class'
|
||||
local Map = require 'pl.Map'
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
-- Derived from `pl.Map`.
|
||||
--
|
||||
-- Dependencies: `pl.utils`, `pl.tablex`, `pl.List`
|
||||
-- @module pl.OrderedMap
|
||||
-- @classmod pl.OrderedMap
|
||||
|
||||
local tablex = require 'pl.tablex'
|
||||
local utils = require 'pl.utils'
|
||||
|
@ -16,11 +16,13 @@ local Map = require 'pl.Map'
|
|||
local OrderedMap = class(Map)
|
||||
OrderedMap._name = 'OrderedMap'
|
||||
|
||||
local rawset = rawset
|
||||
|
||||
--- construct an OrderedMap.
|
||||
-- Will throw an error if the argument is bad.
|
||||
-- @param t optional initialization table, same as for @{OrderedMap:update}
|
||||
function OrderedMap:_init (t)
|
||||
self._keys = List()
|
||||
rawset(self,'_keys',List())
|
||||
if t then
|
||||
local map,err = self:update(t)
|
||||
if not map then error(err,2) end
|
||||
|
@ -30,10 +32,11 @@ end
|
|||
local assert_arg,raise = utils.assert_arg,utils.raise
|
||||
|
||||
--- update an OrderedMap using a table.
|
||||
-- If the table is itself an OrderedMap, then its entries will be appended. <br>
|
||||
-- if it s a table of the form <code>{{key1=val1},{key2=val2},...}</code> these will be appended. <br>
|
||||
-- If the table is itself an OrderedMap, then its entries will be appended.
|
||||
-- if it s a table of the form `{{key1=val1},{key2=val2},...}` these will be appended.
|
||||
--
|
||||
-- Otherwise, it is assumed to be a map-like table, and order of extra entries is arbitrary.
|
||||
-- @param t a table.
|
||||
-- @tab t a table.
|
||||
-- @return the map, or nil in case of error
|
||||
-- @return the error message
|
||||
function OrderedMap:update (t)
|
||||
|
@ -60,26 +63,34 @@ function OrderedMap:update (t)
|
|||
return self
|
||||
end
|
||||
|
||||
--- set the key's value. This key will be appended at the end of the map. <br>
|
||||
--- set the key's value. This key will be appended at the end of the map.
|
||||
--
|
||||
-- If the value is nil, then the key is removed.
|
||||
-- @param key the key
|
||||
-- @param val the value
|
||||
-- @return the map
|
||||
function OrderedMap:set (key,val)
|
||||
if not self[key] and val ~= nil then -- ensure that keys are unique
|
||||
self._keys:append(key)
|
||||
elseif val == nil then -- removing a key-value pair
|
||||
self._keys:remove_value(key)
|
||||
if self[key] == nil and val ~= nil then -- new key
|
||||
self._keys:append(key) -- we keep in order
|
||||
rawset(self,key,val) -- don't want to provoke __newindex!
|
||||
else -- existing key-value pair
|
||||
if val == nil then
|
||||
self._keys:remove_value(key)
|
||||
rawset(self,key,nil)
|
||||
else
|
||||
self[key] = val
|
||||
end
|
||||
end
|
||||
self[key] = val
|
||||
return self
|
||||
end
|
||||
|
||||
OrderedMap.__newindex = OrderedMap.set
|
||||
|
||||
--- insert a key/value pair before a given position.
|
||||
-- Note: if the map already contains the key, then this effectively
|
||||
-- moves the item to the new position by first removing at the old position.
|
||||
-- Has no effect if the key does not exist and val is nil
|
||||
-- @param pos a position starting at 1
|
||||
-- @int pos a position starting at 1
|
||||
-- @param key the key
|
||||
-- @param val the value; if nil use the old value
|
||||
function OrderedMap:insert (pos,key,val)
|
||||
|
@ -90,7 +101,7 @@ function OrderedMap:insert (pos,key,val)
|
|||
end
|
||||
if val then
|
||||
self._keys:insert(pos,key)
|
||||
self[key] = val
|
||||
rawset(self,key,val)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
@ -110,7 +121,7 @@ function OrderedMap:values ()
|
|||
end
|
||||
|
||||
--- sort the keys.
|
||||
-- @param cmp a comparison function as for @{table.sort}
|
||||
-- @func cmp a comparison function as for @{table.sort}
|
||||
-- @return the map
|
||||
function OrderedMap:sort (cmp)
|
||||
tsort(self._keys,cmp)
|
||||
|
@ -130,8 +141,13 @@ function OrderedMap:iter ()
|
|||
end
|
||||
end
|
||||
|
||||
--- iterate over an ordered map (5.2).
|
||||
-- @within metamethods
|
||||
-- @function OrderedMap:__pairs
|
||||
OrderedMap.__pairs = OrderedMap.iter
|
||||
|
||||
--- string representation of an ordered map.
|
||||
-- @within metamethods
|
||||
function OrderedMap:__tostring ()
|
||||
local res = {}
|
||||
for i,v in ipairs(self._keys) do
|
||||
|
|
|
@ -16,17 +16,17 @@
|
|||
-- > = fruit*colours
|
||||
-- [orange]
|
||||
--
|
||||
-- Depdencies: `pl.utils`, `pl.tablex`, `pl.class`
|
||||
-- Depdencies: `pl.utils`, `pl.tablex`, `pl.class`, (`pl.List` if __tostring is used)
|
||||
-- @module pl.Set
|
||||
|
||||
local tablex = require 'pl.tablex'
|
||||
local utils = require 'pl.utils'
|
||||
local stdmt = utils.stdmt
|
||||
local array_tostring, concat = utils.array_tostring, table.concat
|
||||
local tmakeset,deepcompare,merge,keys,difference,tupdate = tablex.makeset,tablex.deepcompare,tablex.merge,tablex.keys,tablex.difference,tablex.update
|
||||
local Map = require 'pl.Map'
|
||||
local Set = stdmt.Set
|
||||
local List = stdmt.List
|
||||
local class = require 'pl.class'
|
||||
local stdmt = utils.stdmt
|
||||
local Set = stdmt.Set
|
||||
|
||||
-- the Set class --------------------
|
||||
class(Map,nil,Set)
|
||||
|
@ -43,6 +43,7 @@ end
|
|||
-- @class function
|
||||
-- @name Set
|
||||
function Set:_init (t)
|
||||
t = t or {}
|
||||
local mt = getmetatable(t)
|
||||
if mt == Set or mt == Map then
|
||||
for k in pairs(t) do self[k] = true end
|
||||
|
@ -51,13 +52,16 @@ function Set:_init (t)
|
|||
end
|
||||
end
|
||||
|
||||
--- string representation of a set.
|
||||
-- @within metamethods
|
||||
function Set:__tostring ()
|
||||
return '['..Set.values(self):join ','..']'
|
||||
return '['..concat(array_tostring(Set.values(self)),',')..']'
|
||||
end
|
||||
|
||||
--- get a list of the values in a set.
|
||||
-- @param self a Set
|
||||
-- @function Set.values
|
||||
-- @return a list
|
||||
Set.values = Map.keys
|
||||
|
||||
--- map a function over the values of a set.
|
||||
|
@ -81,15 +85,33 @@ end
|
|||
function Set.union (self,set)
|
||||
return merge(self,set,true)
|
||||
end
|
||||
|
||||
--- union of sets.
|
||||
-- @within metamethods
|
||||
-- @function Set.__add
|
||||
Set.__add = Set.union
|
||||
|
||||
--- intersection of two sets (also *).
|
||||
-- @param self a Set
|
||||
-- @param set another set
|
||||
-- @return a new set
|
||||
-- @usage
|
||||
-- > s = Set{10,20,30}
|
||||
-- > t = Set{20,30,40}
|
||||
-- > = t
|
||||
-- [20,30,40]
|
||||
-- > = Set.intersection(s,t)
|
||||
-- [30,20]
|
||||
-- > = s*t
|
||||
-- [30,20]
|
||||
|
||||
function Set.intersection (self,set)
|
||||
return merge(self,set,false)
|
||||
end
|
||||
|
||||
--- intersection of sets.
|
||||
-- @within metamethods
|
||||
-- @function Set.__mul
|
||||
Set.__mul = Set.intersection
|
||||
|
||||
--- new set with elements in the set that are not in the other (also -).
|
||||
|
@ -99,6 +121,11 @@ Set.__mul = Set.intersection
|
|||
function Set.difference (self,set)
|
||||
return difference(self,set,false)
|
||||
end
|
||||
|
||||
|
||||
--- difference of sets.
|
||||
-- @within metamethods
|
||||
-- @function Set.__sub
|
||||
Set.__sub = Set.difference
|
||||
|
||||
-- a new set with elements in _either_ the set _or_ other but not both (also ^).
|
||||
|
@ -108,6 +135,10 @@ Set.__sub = Set.difference
|
|||
function Set.symmetric_difference (self,set)
|
||||
return difference(self,set,true)
|
||||
end
|
||||
|
||||
--- symmetric difference of sets.
|
||||
-- @within metamethods
|
||||
-- @function Set.__pow
|
||||
Set.__pow = Set.symmetric_difference
|
||||
|
||||
--- is the first set a subset of the second (also <)?.
|
||||
|
@ -120,12 +151,16 @@ function Set.issubset (self,set)
|
|||
end
|
||||
return true
|
||||
end
|
||||
Set.__lt = Set.subset
|
||||
|
||||
--- first set subset of second?
|
||||
-- @within metamethods
|
||||
-- @function Set.__lt
|
||||
Set.__lt = Set.issubset
|
||||
|
||||
--- is the set empty?.
|
||||
-- @param self a Set
|
||||
-- @return true or false
|
||||
function Set.issempty (self)
|
||||
function Set.isempty (self)
|
||||
return next(self) == nil
|
||||
end
|
||||
|
||||
|
@ -144,8 +179,13 @@ end
|
|||
-- @function Set.len
|
||||
Set.len = tablex.size
|
||||
|
||||
--- cardinality of set (5.2).
|
||||
-- @within metamethods
|
||||
-- @function Set.__len
|
||||
Set.__len = Set.len
|
||||
|
||||
--- equality between sets.
|
||||
-- @within metamethods
|
||||
function Set.__eq (s1,s2)
|
||||
return Set.issubset(s1,s2) and Set.issubset(s2,s1)
|
||||
end
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
--- Application support functions.
|
||||
-- See @{01-introduction.md.Application_Support|the Guide}
|
||||
--
|
||||
-- Dependencies: `pl.utils`, `pl.path`, `lfs`
|
||||
-- Dependencies: `pl.utils`, `pl.path`
|
||||
-- @module pl.app
|
||||
|
||||
local io,package,require = _G.io, _G.package, _G.require
|
||||
local utils = require 'pl.utils'
|
||||
local path = require 'pl.path'
|
||||
local lfs = require 'lfs'
|
||||
|
||||
|
||||
local app = {}
|
||||
|
||||
|
@ -23,12 +21,12 @@ end
|
|||
-- `base` allows these modules to be put in a specified subdirectory, to allow for
|
||||
-- cleaner deployment and resolve potential conflicts between a script name and its
|
||||
-- library directory.
|
||||
-- @param base optional base directory.
|
||||
-- @return the current script's path with a trailing slash
|
||||
-- @string base optional base directory.
|
||||
-- @treturn string the current script's path with a trailing slash
|
||||
function app.require_here (base)
|
||||
local p = path.dirname(check_script_name())
|
||||
if not path.isabs(p) then
|
||||
p = path.join(lfs.currentdir(),p)
|
||||
p = path.join(path.currentdir(),p)
|
||||
end
|
||||
if p:sub(-1,-1) ~= path.sep then
|
||||
p = p..path.sep
|
||||
|
@ -47,7 +45,7 @@ end
|
|||
--- return a suitable path for files private to this application.
|
||||
-- These will look like '~/.SNAME/file', with '~' as with expanduser and
|
||||
-- SNAME is the name of the script without .lua extension.
|
||||
-- @param file a filename (w/out path)
|
||||
-- @string file a filename (w/out path)
|
||||
-- @return a full pathname, or nil
|
||||
-- @return 'cannot create' error
|
||||
function app.appfile (file)
|
||||
|
@ -55,7 +53,7 @@ function app.appfile (file)
|
|||
local name,ext = path.splitext(sname)
|
||||
local dir = path.join(path.expanduser('~'),'.'..name)
|
||||
if not path.isdir(dir) then
|
||||
local ret = lfs.mkdir(dir)
|
||||
local ret = path.mkdir(dir)
|
||||
if not ret then return utils.raise ('cannot create '..dir) end
|
||||
end
|
||||
return path.join(dir,file)
|
||||
|
@ -75,8 +73,8 @@ function app.platform()
|
|||
end
|
||||
end
|
||||
|
||||
--- return the full command-line used to invoke this script
|
||||
-- any extra flags occupy slots, so that 'lua -lpl' gives us {[-2]='lua',[-1]='-lpl')
|
||||
--- return the full command-line used to invoke this script.
|
||||
-- Any extra flags occupy slots, so that `lua -lpl` gives us `{[-2]='lua',[-1]='-lpl'}`
|
||||
-- @return command-line
|
||||
-- @return name of Lua program used
|
||||
function app.lua ()
|
||||
|
@ -97,12 +95,12 @@ function app.lua ()
|
|||
end
|
||||
|
||||
--- parse command-line arguments into flags and parameters.
|
||||
-- Understands GNU-style command-line flags; short (-f) and long (--flag).
|
||||
-- These may be given a value with either '=' or ':' (-k:2,--alpha=3.2,-n2);
|
||||
-- Understands GNU-style command-line flags; short (`-f`) and long (`--flag`).
|
||||
-- These may be given a value with either '=' or ':' (`-k:2`,`--alpha=3.2`,`-n2`);
|
||||
-- note that a number value can be given without a space.
|
||||
-- Multiple short args can be combined like so: (-abcd).
|
||||
-- @param args an array of strings (default is the global 'arg')
|
||||
-- @param flags_with_values any flags that take values, e.g. <code>{out=true}</code>
|
||||
-- Multiple short args can be combined like so: ( `-abcd`).
|
||||
-- @tparam {string} args an array of strings (default is the global `arg`)
|
||||
-- @tab flags_with_values any flags that take values, e.g. `{out=true}`
|
||||
-- @return a table of flags (flag=value pairs)
|
||||
-- @return an array of parameters
|
||||
-- @raise if args is nil, then the global `args` must be available!
|
||||
|
@ -131,8 +129,8 @@ function app.parse_args (args,flags_with_values)
|
|||
flags[v] = args[i+1]
|
||||
i = i + 1
|
||||
else
|
||||
-- a value can be indicated with = or :
|
||||
local var,val = utils.splitv (v,'[=:]')
|
||||
-- a value can also be indicated with =
|
||||
local var,val = utils.splitv (v,'=')
|
||||
var = var or v
|
||||
val = val or true
|
||||
if not is_long then
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
--- Operations on two-dimensional arrays.
|
||||
-- See @{02-arrays.md.Operations_on_two_dimensional_tables|The Guide}
|
||||
--
|
||||
-- Dependencies: `pl.utils`, `pl.tablex`
|
||||
-- Dependencies: `pl.utils`, `pl.tablex`, `pl.types`
|
||||
-- @module pl.array2d
|
||||
|
||||
local require, type,tonumber,assert,tostring,io,ipairs,string,table =
|
||||
_G.require, _G.type,_G.tonumber,_G.assert,_G.tostring,_G.io,_G.ipairs,_G.string,_G.table
|
||||
local type,tonumber,assert,tostring,io,ipairs,string,table =
|
||||
_G.type,_G.tonumber,_G.assert,_G.tostring,_G.io,_G.ipairs,_G.string,_G.table
|
||||
local setmetatable,getmetatable = setmetatable,getmetatable
|
||||
|
||||
local tablex = require 'pl.tablex'
|
||||
local utils = require 'pl.utils'
|
||||
|
||||
local types = require 'pl.types'
|
||||
local imap,tmap,reduce,keys,tmap2,tset,index_by = tablex.imap,tablex.map,tablex.reduce,tablex.keys,tablex.map2,tablex.set,tablex.index_by
|
||||
local remove = table.remove
|
||||
local splitv,fprintf,assert_arg = utils.splitv,utils.fprintf,utils.assert_arg
|
||||
|
@ -37,16 +37,16 @@ local function index (t,k)
|
|||
end
|
||||
|
||||
--- return the row and column size.
|
||||
-- @param t a 2d array
|
||||
-- @return number of rows
|
||||
-- @return number of cols
|
||||
-- @array2d t a 2d array
|
||||
-- @treturn int number of rows
|
||||
-- @treturn int number of cols
|
||||
function array2d.size (t)
|
||||
assert_arg(1,t,'table')
|
||||
return #t,#t[1]
|
||||
end
|
||||
|
||||
--- extract a column from the 2D array.
|
||||
-- @param a 2d array
|
||||
-- @array2d a 2d array
|
||||
-- @param key an index or key
|
||||
-- @return 1d array
|
||||
function array2d.column (a,key)
|
||||
|
@ -56,8 +56,8 @@ end
|
|||
local column = array2d.column
|
||||
|
||||
--- map a function over a 2D array
|
||||
-- @param f a function of at least one argument
|
||||
-- @param a 2d array
|
||||
-- @func f a function of at least one argument
|
||||
-- @array2d a 2d array
|
||||
-- @param arg an optional extra argument to be passed to the function.
|
||||
-- @return 2d array
|
||||
function array2d.map (f,a,arg)
|
||||
|
@ -67,8 +67,8 @@ function array2d.map (f,a,arg)
|
|||
end
|
||||
|
||||
--- reduce the rows using a function.
|
||||
-- @param f a binary function
|
||||
-- @param a 2d array
|
||||
-- @func f a binary function
|
||||
-- @array2d a 2d array
|
||||
-- @return 1d array
|
||||
-- @see pl.tablex.reduce
|
||||
function array2d.reduce_rows (f,a)
|
||||
|
@ -77,8 +77,8 @@ function array2d.reduce_rows (f,a)
|
|||
end
|
||||
|
||||
--- reduce the columns using a function.
|
||||
-- @param f a binary function
|
||||
-- @param a 2d array
|
||||
-- @func f a binary function
|
||||
-- @array2d a 2d array
|
||||
-- @return 1d array
|
||||
-- @see pl.tablex.reduce
|
||||
function array2d.reduce_cols (f,a)
|
||||
|
@ -87,8 +87,8 @@ function array2d.reduce_cols (f,a)
|
|||
end
|
||||
|
||||
--- reduce a 2D array into a scalar, using two operations.
|
||||
-- @param opc operation to reduce the final result
|
||||
-- @param opr operation to reduce the rows
|
||||
-- @func opc operation to reduce the final result
|
||||
-- @func opr operation to reduce the rows
|
||||
-- @param a 2D array
|
||||
function array2d.reduce2 (opc,opr,a)
|
||||
assert_arg(3,a,'table')
|
||||
|
@ -102,18 +102,17 @@ end
|
|||
|
||||
--- map a function over two arrays.
|
||||
-- They can be both or either 2D arrays
|
||||
-- @param f function of at least two arguments
|
||||
-- @param ad order of first array
|
||||
-- @param bd order of second array
|
||||
-- @param a 1d or 2d array
|
||||
-- @param b 1d or 2d array
|
||||
-- @func f function of at least two arguments
|
||||
-- @int ad order of first array (1 or 2)
|
||||
-- @int bd order of second array (1 or 2)
|
||||
-- @tab a 1d or 2d array
|
||||
-- @tab b 1d or 2d array
|
||||
-- @param arg optional extra argument to pass to function
|
||||
-- @return 2D array, unless both arrays are 1D
|
||||
function array2d.map2 (f,ad,bd,a,b,arg)
|
||||
assert_arg(1,a,'table')
|
||||
assert_arg(2,b,'table')
|
||||
f = utils.function_arg(1,f)
|
||||
--local ad,bd = dimension(a),dimension(b)
|
||||
if ad == 1 and bd == 2 then
|
||||
return imap(function(row)
|
||||
return tmap2(f,a,row,arg)
|
||||
|
@ -132,9 +131,9 @@ function array2d.map2 (f,ad,bd,a,b,arg)
|
|||
end
|
||||
|
||||
--- cartesian product of two 1d arrays.
|
||||
-- @param f a function of 2 arguments
|
||||
-- @param t1 a 1d table
|
||||
-- @param t2 a 1d table
|
||||
-- @func f a function of 2 arguments
|
||||
-- @array t1 a 1d table
|
||||
-- @array t2 a 1d table
|
||||
-- @return 2d table
|
||||
-- @usage product('..',{1,2},{'a','b'}) == {{'1a','2a'},{'1b','2b'}}
|
||||
function array2d.product (f,t1,t2)
|
||||
|
@ -150,7 +149,7 @@ end
|
|||
|
||||
--- flatten a 2D array.
|
||||
-- (this goes over columns first.)
|
||||
-- @param t 2d table
|
||||
-- @array2d t 2d table
|
||||
-- @return a 1d table
|
||||
-- @usage flatten {{1,2},{3,4},{5,6}} == {1,2,3,4,5,6}
|
||||
function array2d.flatten (t)
|
||||
|
@ -166,9 +165,9 @@ function array2d.flatten (t)
|
|||
end
|
||||
|
||||
--- reshape a 2D array.
|
||||
-- @param t 2d array
|
||||
-- @param nrows new number of rows
|
||||
-- @param co column-order (Fortran-style) (default false)
|
||||
-- @array2d t 2d array
|
||||
-- @int nrows new number of rows
|
||||
-- @bool co column-order (Fortran-style) (default false)
|
||||
-- @return a new 2d array
|
||||
function array2d.reshape (t,nrows,co)
|
||||
local nr,nc = array2d.size(t)
|
||||
|
@ -199,18 +198,18 @@ function array2d.reshape (t,nrows,co)
|
|||
end
|
||||
|
||||
--- swap two rows of an array.
|
||||
-- @param t a 2d array
|
||||
-- @param i1 a row index
|
||||
-- @param i2 a row index
|
||||
-- @array2d t a 2d array
|
||||
-- @int i1 a row index
|
||||
-- @int i2 a row index
|
||||
function array2d.swap_rows (t,i1,i2)
|
||||
assert_arg(1,t,'table')
|
||||
t[i1],t[i2] = t[i2],t[i1]
|
||||
end
|
||||
|
||||
--- swap two columns of an array.
|
||||
-- @param t a 2d array
|
||||
-- @param j1 a column index
|
||||
-- @param j2 a column index
|
||||
-- @array2d t a 2d array
|
||||
-- @int j1 a column index
|
||||
-- @int j2 a column index
|
||||
function array2d.swap_cols (t,j1,j2)
|
||||
assert_arg(1,t,'table')
|
||||
for i = 1,#t do
|
||||
|
@ -220,15 +219,15 @@ function array2d.swap_cols (t,j1,j2)
|
|||
end
|
||||
|
||||
--- extract the specified rows.
|
||||
-- @param t 2d array
|
||||
-- @param ridx a table of row indices
|
||||
-- @array2d t 2d array
|
||||
-- @tparam {int} ridx a table of row indices
|
||||
function array2d.extract_rows (t,ridx)
|
||||
return obj(t,index_by(t,ridx))
|
||||
end
|
||||
|
||||
--- extract the specified columns.
|
||||
-- @param t 2d array
|
||||
-- @param cidx a table of column indices
|
||||
-- @array2d t 2d array
|
||||
-- @tparam {int} cidx a table of column indices
|
||||
function array2d.extract_cols (t,cidx)
|
||||
assert_arg(1,t,'table')
|
||||
local res = {}
|
||||
|
@ -239,15 +238,14 @@ function array2d.extract_cols (t,cidx)
|
|||
end
|
||||
|
||||
--- remove a row from an array.
|
||||
-- @class function
|
||||
-- @name array2d.remove_row
|
||||
-- @param t a 2d array
|
||||
-- @param i a row index
|
||||
-- @function array2d.remove_row
|
||||
-- @array2d t a 2d array
|
||||
-- @int i a row index
|
||||
array2d.remove_row = remove
|
||||
|
||||
--- remove a column from an array.
|
||||
-- @param t a 2d array
|
||||
-- @param j a column index
|
||||
-- @array2d t a 2d array
|
||||
-- @int j a column index
|
||||
function array2d.remove_col (t,j)
|
||||
assert_arg(1,t,'table')
|
||||
for i = 1,#t do
|
||||
|
@ -274,11 +272,11 @@ end
|
|||
--- parse a spreadsheet range.
|
||||
-- The range can be specified either as 'A1:B2' or 'R1C1:R2C2';
|
||||
-- a special case is a single element (e.g 'A1' or 'R1C1')
|
||||
-- @param s a range.
|
||||
-- @return start col
|
||||
-- @return start row
|
||||
-- @return end col
|
||||
-- @return end row
|
||||
-- @string s a range.
|
||||
-- @treturn int start col
|
||||
-- @treturn int start row
|
||||
-- @treturn int end col
|
||||
-- @treturn int end row
|
||||
function array2d.parse_range (s)
|
||||
if s:find ':' then
|
||||
local start,finish = splitv(s,':')
|
||||
|
@ -292,8 +290,8 @@ function array2d.parse_range (s)
|
|||
end
|
||||
|
||||
--- get a slice of a 2D array using spreadsheet range notation. @see parse_range
|
||||
-- @param t a 2D array
|
||||
-- @param rstr range expression
|
||||
-- @array2d t a 2D array
|
||||
-- @string rstr range expression
|
||||
-- @return a slice
|
||||
-- @see array2d.parse_range
|
||||
-- @see array2d.slice
|
||||
|
@ -318,11 +316,11 @@ end
|
|||
|
||||
--- get a slice of a 2D array. Note that if the specified range has
|
||||
-- a 1D result, the rank of the result will be 1.
|
||||
-- @param t a 2D array
|
||||
-- @param i1 start row (default 1)
|
||||
-- @param j1 start col (default 1)
|
||||
-- @param i2 end row (default N)
|
||||
-- @param j2 end col (default M)
|
||||
-- @array2d t a 2D array
|
||||
-- @int i1 start row (default 1)
|
||||
-- @int j1 start col (default 1)
|
||||
-- @int i2 end row (default N)
|
||||
-- @int j2 end col (default M)
|
||||
-- @return an array, 2D in general but 1D in special cases.
|
||||
function array2d.slice (t,i1,j1,i2,j2)
|
||||
assert_arg(1,t,'table')
|
||||
|
@ -346,12 +344,12 @@ function array2d.slice (t,i1,j1,i2,j2)
|
|||
end
|
||||
|
||||
--- set a specified range of an array to a value.
|
||||
-- @param t a 2D array
|
||||
-- @array2d t a 2D array
|
||||
-- @param value the value (may be a function)
|
||||
-- @param i1 start row (default 1)
|
||||
-- @param j1 start col (default 1)
|
||||
-- @param i2 end row (default N)
|
||||
-- @param j2 end col (default M)
|
||||
-- @int i1 start row (default 1)
|
||||
-- @int j1 start col (default 1)
|
||||
-- @int i2 end row (default N)
|
||||
-- @int j2 end col (default M)
|
||||
-- @see tablex.set
|
||||
function array2d.set (t,value,i1,j1,i2,j2)
|
||||
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
|
||||
|
@ -361,13 +359,13 @@ function array2d.set (t,value,i1,j1,i2,j2)
|
|||
end
|
||||
|
||||
--- write a 2D array to a file.
|
||||
-- @param t a 2D array
|
||||
-- @array2d t a 2D array
|
||||
-- @param f a file object (default stdout)
|
||||
-- @param fmt a format string (default is just to use tostring)
|
||||
-- @param i1 start row (default 1)
|
||||
-- @param j1 start col (default 1)
|
||||
-- @param i2 end row (default N)
|
||||
-- @param j2 end col (default M)
|
||||
-- @string fmt a format string (default is just to use tostring)
|
||||
-- @int i1 start row (default 1)
|
||||
-- @int j1 start col (default 1)
|
||||
-- @int i2 end row (default N)
|
||||
-- @int j2 end col (default M)
|
||||
function array2d.write (t,f,fmt,i1,j1,i2,j2)
|
||||
assert_arg(1,t,'table')
|
||||
f = f or stdout
|
||||
|
@ -384,13 +382,13 @@ function array2d.write (t,f,fmt,i1,j1,i2,j2)
|
|||
end
|
||||
|
||||
--- perform an operation for all values in a 2D array.
|
||||
-- @param t 2D array
|
||||
-- @param row_op function to call on each value
|
||||
-- @param end_row_op function to call at end of each row
|
||||
-- @param i1 start row (default 1)
|
||||
-- @param j1 start col (default 1)
|
||||
-- @param i2 end row (default N)
|
||||
-- @param j2 end col (default M)
|
||||
-- @array2d t 2D array
|
||||
-- @func row_op function to call on each value
|
||||
-- @func end_row_op function to call at end of each row
|
||||
-- @int i1 start row (default 1)
|
||||
-- @int j1 start col (default 1)
|
||||
-- @int i2 end row (default N)
|
||||
-- @int j2 end col (default M)
|
||||
function array2d.forall (t,row_op,end_row_op,i1,j1,i2,j2)
|
||||
assert_arg(1,t,'table')
|
||||
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
|
||||
|
@ -406,14 +404,14 @@ end
|
|||
local min, max = math.min, math.max
|
||||
|
||||
---- move a block from the destination to the source.
|
||||
-- @param dest a 2D array
|
||||
-- @param di start row in dest
|
||||
-- @param dj start col in dest
|
||||
-- @param src a 2D array
|
||||
-- @param i1 start row (default 1)
|
||||
-- @param j1 start col (default 1)
|
||||
-- @param i2 end row (default N)
|
||||
-- @param j2 end col (default M)
|
||||
-- @array2d dest a 2D array
|
||||
-- @int di start row in dest
|
||||
-- @int dj start col in dest
|
||||
-- @array2d src a 2D array
|
||||
-- @int i1 start row (default 1)
|
||||
-- @int j1 start col (default 1)
|
||||
-- @int i2 end row (default N)
|
||||
-- @int j2 end col (default M)
|
||||
function array2d.move (dest,di,dj,src,i1,j1,i2,j2)
|
||||
assert_arg(1,dest,'table')
|
||||
assert_arg(4,src,'table')
|
||||
|
@ -431,12 +429,12 @@ function array2d.move (dest,di,dj,src,i1,j1,i2,j2)
|
|||
end
|
||||
|
||||
--- iterate over all elements in a 2D array, with optional indices.
|
||||
-- @param a 2D array
|
||||
-- @param indices with indices (default false)
|
||||
-- @param i1 start row (default 1)
|
||||
-- @param j1 start col (default 1)
|
||||
-- @param i2 end row (default N)
|
||||
-- @param j2 end col (default M)
|
||||
-- @array2d a 2D array
|
||||
-- @tparam {int} indices with indices (default false)
|
||||
-- @int i1 start row (default 1)
|
||||
-- @int j1 start col (default 1)
|
||||
-- @int i2 end row (default N)
|
||||
-- @int j2 end col (default M)
|
||||
-- @return either value or i,j,value depending on indices
|
||||
function array2d.iter (a,indices,i1,j1,i2,j2)
|
||||
assert_arg(1,a,'table')
|
||||
|
@ -463,7 +461,7 @@ function array2d.iter (a,indices,i1,j1,i2,j2)
|
|||
end
|
||||
|
||||
--- iterate over all columns.
|
||||
-- @param a a 2D array
|
||||
-- @array2d a a 2D array
|
||||
-- @return each column in turn
|
||||
function array2d.columns (a)
|
||||
assert_arg(1,a,'table')
|
||||
|
@ -477,13 +475,13 @@ function array2d.columns (a)
|
|||
end
|
||||
|
||||
--- new array of specified dimensions
|
||||
-- @param rows number of rows
|
||||
-- @param cols number of cols
|
||||
-- @int rows number of rows
|
||||
-- @int cols number of cols
|
||||
-- @param val initial value; if it's a function then use `val(i,j)`
|
||||
-- @return new 2d array
|
||||
function array2d.new(rows,cols,val)
|
||||
local res = {}
|
||||
local fun = utils.is_callable(val)
|
||||
local fun = types.is_callable(val)
|
||||
for i = 1,rows do
|
||||
local row = {}
|
||||
if fun then
|
||||
|
|
|
@ -4,13 +4,16 @@
|
|||
-- B = class(A)
|
||||
-- class.B(A)
|
||||
--
|
||||
-- The latter form creates a named class.
|
||||
-- The latter form creates a named class within the current environment. Note
|
||||
-- that this implicitly brings in `pl.utils` as a dependency.
|
||||
--
|
||||
-- See the Guide for further @{01-introduction.md.Simplifying_Object_Oriented_Programming_in_Lua|discussion}
|
||||
-- @module pl.class
|
||||
|
||||
local error, getmetatable, io, pairs, rawget, rawset, setmetatable, tostring, type =
|
||||
_G.error, _G.getmetatable, _G.io, _G.pairs, _G.rawget, _G.rawset, _G.setmetatable, _G.tostring, _G.type
|
||||
local compat
|
||||
|
||||
-- this trickery is necessary to prevent the inheritance of 'super' and
|
||||
-- the resulting recursive call problems.
|
||||
local function call_ctor (c,obj,...)
|
||||
|
@ -18,17 +21,43 @@ local function call_ctor (c,obj,...)
|
|||
local base = rawget(c,'_base')
|
||||
if base then
|
||||
local parent_ctor = rawget(base,'_init')
|
||||
while not parent_ctor do
|
||||
base = rawget(base,'_base')
|
||||
if not base then break end
|
||||
parent_ctor = rawget(base,'_init')
|
||||
end
|
||||
if parent_ctor then
|
||||
obj.super = function(obj,...)
|
||||
rawset(obj,'super',function(obj,...)
|
||||
call_ctor(base,obj,...)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
local res = c._init(obj,...)
|
||||
obj.super = nil
|
||||
rawset(obj,'super',nil)
|
||||
return res
|
||||
end
|
||||
|
||||
--- initializes an __instance__ upon creation.
|
||||
-- @function class:_init
|
||||
-- @param ... parameters passed to the constructor
|
||||
-- @usage local Cat = class()
|
||||
-- function Cat:_init(name)
|
||||
-- --self:super(name) -- call the ancestor initializer if needed
|
||||
-- self.name = name
|
||||
-- end
|
||||
--
|
||||
-- local pussycat = Cat("pussycat")
|
||||
-- print(pussycat.name) --> pussycat
|
||||
|
||||
--- checks whether an __instance__ is derived from some class.
|
||||
-- Works the other way around as `class_of`.
|
||||
-- @function instance:is_a
|
||||
-- @param some_class class to check against
|
||||
-- @return `true` if `instance` is derived from `some_class`
|
||||
-- @usage local pussycat = Lion() -- assuming Lion derives from Cat
|
||||
-- if pussycat:is_a(Cat) then
|
||||
-- -- it's true
|
||||
-- end
|
||||
local function is_a(self,klass)
|
||||
local m = getmetatable(self)
|
||||
if not m then return false end --*can't be an object!
|
||||
|
@ -39,11 +68,29 @@ local function is_a(self,klass)
|
|||
return false
|
||||
end
|
||||
|
||||
--- checks whether an __instance__ is derived from some class.
|
||||
-- Works the other way around as `is_a`.
|
||||
-- @function some_class:class_of
|
||||
-- @param some_instance instance to check against
|
||||
-- @return `true` if `some_instance` is derived from `some_class`
|
||||
-- @usage local pussycat = Lion() -- assuming Lion derives from Cat
|
||||
-- if Cat:class_of(pussycat) then
|
||||
-- -- it's true
|
||||
-- end
|
||||
local function class_of(klass,obj)
|
||||
if type(klass) ~= 'table' or not rawget(klass,'is_a') then return false end
|
||||
return klass.is_a(obj,klass)
|
||||
end
|
||||
|
||||
--- cast an object to another class.
|
||||
-- It is not clever (or safe!) so use carefully.
|
||||
-- @param some_instance the object to be changed
|
||||
-- @function some_class:cast
|
||||
local function cast (klass, obj)
|
||||
return setmetatable(obj,klass)
|
||||
end
|
||||
|
||||
|
||||
local function _class_tostring (obj)
|
||||
local mt = obj._class
|
||||
local name = rawget(mt,'_name')
|
||||
|
@ -54,21 +101,31 @@ local function _class_tostring (obj)
|
|||
return str
|
||||
end
|
||||
|
||||
local function tupdate(td,ts)
|
||||
local function tupdate(td,ts,dont_override)
|
||||
for k,v in pairs(ts) do
|
||||
td[k] = v
|
||||
if not dont_override or td[k] == nil then
|
||||
td[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function _class(base,c_arg,c)
|
||||
c = c or {} -- a new class instance, which is the metatable for all objects of this type
|
||||
-- the class will be the metatable for all its objects,
|
||||
-- the class `c` will be the metatable for all its objects,
|
||||
-- and they will look up their methods in it.
|
||||
local mt = {} -- a metatable for the class instance
|
||||
|
||||
local mt = {} -- a metatable for the class to support __call and _handler
|
||||
-- can define class by passing it a plain table of methods
|
||||
local plain = type(base) == 'table' and not getmetatable(base)
|
||||
if plain then
|
||||
c = base
|
||||
base = c._base
|
||||
else
|
||||
c = c or {}
|
||||
end
|
||||
|
||||
if type(base) == 'table' then
|
||||
-- our new class is a shallow copy of the base class!
|
||||
tupdate(c,base)
|
||||
-- but be careful not to wipe out any methods we have been given at this point!
|
||||
tupdate(c,base,plain)
|
||||
c._base = base
|
||||
-- inherit the 'not found' handler, if present
|
||||
if rawget(c,'_handler') then mt.__index = c._handler end
|
||||
|
@ -78,7 +135,9 @@ local function _class(base,c_arg,c)
|
|||
|
||||
c.__index = c
|
||||
setmetatable(c,mt)
|
||||
c._init = nil
|
||||
if not plain then
|
||||
c._init = nil
|
||||
end
|
||||
|
||||
if base and rawget(base,'_class_init') then
|
||||
base._class_init(c,c_arg)
|
||||
|
@ -86,7 +145,9 @@ local function _class(base,c_arg,c)
|
|||
|
||||
-- expose a ctor which can be called by <classname>(<args>)
|
||||
mt.__call = function(class_tbl,...)
|
||||
local obj = {}
|
||||
local obj
|
||||
if rawget(c,'_create') then obj = c._create(...) end
|
||||
if not obj then obj = {} end
|
||||
setmetatable(obj,c)
|
||||
|
||||
if rawget(c,'_init') then -- explicit constructor
|
||||
|
@ -110,12 +171,17 @@ local function _class(base,c_arg,c)
|
|||
return obj
|
||||
end
|
||||
-- Call Class.catch to set a handler for methods/properties not found in the class!
|
||||
c.catch = function(handler)
|
||||
c.catch = function(self, handler)
|
||||
if type(self) == "function" then
|
||||
-- called using . instead of :
|
||||
handler = self
|
||||
end
|
||||
c._handler = handler
|
||||
mt.__index = handler
|
||||
end
|
||||
c.is_a = is_a
|
||||
c.class_of = class_of
|
||||
c.cast = cast
|
||||
c._class = c
|
||||
|
||||
return c
|
||||
|
@ -123,10 +189,13 @@ end
|
|||
|
||||
--- create a new class, derived from a given base class.
|
||||
-- Supporting two class creation syntaxes:
|
||||
-- either `Name = class(base)` or `class.Name(base)`
|
||||
-- either `Name = class(base)` or `class.Name(base)`.
|
||||
-- The first form returns the class directly and does not set its `_name`.
|
||||
-- The second form creates a variable `Name` in the current environment set
|
||||
-- to the class, and also sets `_name`.
|
||||
-- @function class
|
||||
-- @param base optional base class
|
||||
-- @param c_arg optional parameter to class ctor
|
||||
-- @param c_arg optional parameter to class constructor
|
||||
-- @param c optional table to be used as class
|
||||
local class
|
||||
class = setmetatable({},{
|
||||
|
@ -138,7 +207,8 @@ class = setmetatable({},{
|
|||
io.stderr:write('require("pl.class").class is deprecated. Use require("pl.class")\n')
|
||||
return class
|
||||
end
|
||||
local env = _G
|
||||
compat = compat or require 'pl.compat'
|
||||
local env = compat.getfenv(2)
|
||||
return function(...)
|
||||
local c = _class(...)
|
||||
c._name = key
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
----------------
|
||||
--- Lua 5.1/5.2 compatibility
|
||||
-- Ensures that `table.pack` and `package.searchpath` are available
|
||||
-- for Lua 5.1 and LuaJIT.
|
||||
-- The exported function `load` is Lua 5.2 compatible.
|
||||
-- `compat.setfenv` and `compat.getfenv` are available for Lua 5.2, although
|
||||
-- they are not always guaranteed to work.
|
||||
-- @module pl.compat
|
||||
|
||||
local compat = {}
|
||||
|
||||
compat.lua51 = _VERSION == 'Lua 5.1'
|
||||
|
||||
--- execute a shell command.
|
||||
-- This is a compatibility function that returns the same for Lua 5.1 and Lua 5.2
|
||||
-- @param cmd a shell command
|
||||
-- @return true if successful
|
||||
-- @return actual return code
|
||||
function compat.execute (cmd)
|
||||
local res1,res2,res2 = os.execute(cmd)
|
||||
if compat.lua51 then
|
||||
return res1==0,res1
|
||||
else
|
||||
return not not res1,res2
|
||||
end
|
||||
end
|
||||
|
||||
----------------
|
||||
-- Load Lua code as a text or binary chunk.
|
||||
-- @param ld code string or loader
|
||||
-- @param[opt] source name of chunk for errors
|
||||
-- @param[opt] mode 'b', 't' or 'bt'
|
||||
-- @param[opt] env environment to load the chunk in
|
||||
-- @function compat.load
|
||||
|
||||
---------------
|
||||
-- Get environment of a function.
|
||||
-- With Lua 5.2, may return nil for a function with no global references!
|
||||
-- Based on code by [Sergey Rozhenko](http://lua-users.org/lists/lua-l/2010-06/msg00313.html)
|
||||
-- @param f a function or a call stack reference
|
||||
-- @function compat.setfenv
|
||||
|
||||
---------------
|
||||
-- Set environment of a function
|
||||
-- @param f a function or a call stack reference
|
||||
-- @param env a table that becomes the new environment of `f`
|
||||
-- @function compat.setfenv
|
||||
|
||||
if compat.lua51 then -- define Lua 5.2 style load()
|
||||
if not tostring(assert):match 'builtin' then -- but LuaJIT's load _is_ compatible
|
||||
local lua51_load = load
|
||||
function compat.load(str,src,mode,env)
|
||||
local chunk,err
|
||||
if type(str) == 'string' then
|
||||
if str:byte(1) == 27 and not (mode or 'bt'):find 'b' then
|
||||
return nil,"attempt to load a binary chunk"
|
||||
end
|
||||
chunk,err = loadstring(str,src)
|
||||
else
|
||||
chunk,err = lua51_load(str,src)
|
||||
end
|
||||
if chunk and env then setfenv(chunk,env) end
|
||||
return chunk,err
|
||||
end
|
||||
else
|
||||
compat.load = load
|
||||
end
|
||||
compat.setfenv, compat.getfenv = setfenv, getfenv
|
||||
else
|
||||
compat.load = load
|
||||
-- setfenv/getfenv replacements for Lua 5.2
|
||||
-- by Sergey Rozhenko
|
||||
-- http://lua-users.org/lists/lua-l/2010-06/msg00313.html
|
||||
-- Roberto Ierusalimschy notes that it is possible for getfenv to return nil
|
||||
-- in the case of a function with no globals:
|
||||
-- http://lua-users.org/lists/lua-l/2010-06/msg00315.html
|
||||
function compat.setfenv(f, t)
|
||||
f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func)
|
||||
local name
|
||||
local up = 0
|
||||
repeat
|
||||
up = up + 1
|
||||
name = debug.getupvalue(f, up)
|
||||
until name == '_ENV' or name == nil
|
||||
if name then
|
||||
debug.upvaluejoin(f, up, function() return name end, 1) -- use unique upvalue
|
||||
debug.setupvalue(f, up, t)
|
||||
end
|
||||
if f ~= 0 then return f end
|
||||
end
|
||||
|
||||
function compat.getfenv(f)
|
||||
local f = f or 0
|
||||
f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func)
|
||||
local name, val
|
||||
local up = 0
|
||||
repeat
|
||||
up = up + 1
|
||||
name, val = debug.getupvalue(f, up)
|
||||
until name == '_ENV' or name == nil
|
||||
return val
|
||||
end
|
||||
end
|
||||
|
||||
--- Lua 5.2 Functions Available for 5.1
|
||||
-- @section lua52
|
||||
|
||||
--- pack an argument list into a table.
|
||||
-- @param ... any arguments
|
||||
-- @return a table with field n set to the length
|
||||
-- @return the length
|
||||
-- @function table.pack
|
||||
if not table.pack then
|
||||
function table.pack (...)
|
||||
return {n=select('#',...); ...}
|
||||
end
|
||||
end
|
||||
|
||||
------
|
||||
-- return the full path where a Lua module name would be matched.
|
||||
-- @param mod module name, possibly dotted
|
||||
-- @param path a path in the same form as package.path or package.cpath
|
||||
-- @see path.package_path
|
||||
-- @function package.searchpath
|
||||
if not package.searchpath then
|
||||
local sep = package.config:sub(1,1)
|
||||
function package.searchpath (mod,path)
|
||||
mod = mod:gsub('%.',sep)
|
||||
for m in path:gmatch('[^;]+') do
|
||||
local nm = m:gsub('?',mod)
|
||||
local f = io.open(nm,'r')
|
||||
if f then f:close(); return nm end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return compat
|
|
@ -253,7 +253,7 @@ local function new(env)
|
|||
-- performance penalty.
|
||||
|
||||
if not env then
|
||||
env = getfenv(2)
|
||||
env = utils.getfenv(2)
|
||||
end
|
||||
|
||||
local mt = {}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
--- Reads configuration files into a Lua table.
|
||||
-- Understands INI files, classic Unix config files, and simple
|
||||
-- delimited columns of values.
|
||||
-- delimited columns of values. See @{06-data.md.Reading_Configuration_Files|the Guide}
|
||||
--
|
||||
-- # test.config
|
||||
-- # Read timeout in seconds
|
||||
|
@ -11,7 +11,7 @@
|
|||
-- ports = 1002,1003,1004
|
||||
--
|
||||
-- -- readconfig.lua
|
||||
-- require 'pl'
|
||||
-- local config = require 'config'
|
||||
-- local t = config.read 'test.config'
|
||||
-- print(pretty.write(t))
|
||||
--
|
||||
|
@ -26,9 +26,6 @@
|
|||
-- read_timeout = 10
|
||||
-- }
|
||||
--
|
||||
-- See the Guide for further @{06-data.md.Reading_Configuration_Files|discussion}
|
||||
--
|
||||
-- Dependencies: none
|
||||
-- @module pl.config
|
||||
|
||||
local type,tonumber,ipairs,io, table = _G.type,_G.tonumber,_G.ipairs,_G.io,_G.table
|
||||
|
@ -71,16 +68,19 @@ function config.lines(file)
|
|||
return function()
|
||||
local l = f:read()
|
||||
while l do
|
||||
-- does the line end with '\'?
|
||||
local i = l:find '\\%s*$'
|
||||
if i then -- if so,
|
||||
line = line..l:sub(1,i-1)
|
||||
elseif line == '' then
|
||||
return l
|
||||
else
|
||||
l = line..l
|
||||
line = ''
|
||||
return l
|
||||
-- only for non-blank lines that don't begin with either ';' or '#'
|
||||
if l:match '%S' and not l:match '^%s*[;#]' then
|
||||
-- does the line end with '\'?
|
||||
local i = l:find '\\%s*$'
|
||||
if i then -- if so,
|
||||
line = line..l:sub(1,i-1)
|
||||
elseif line == '' then
|
||||
return l
|
||||
else
|
||||
l = line..l
|
||||
line = ''
|
||||
return l
|
||||
end
|
||||
end
|
||||
l = f:read()
|
||||
end
|
||||
|
@ -90,36 +90,61 @@ end
|
|||
|
||||
--- read a configuration file into a table
|
||||
-- @param file either a file-like object or a string, which must be a filename
|
||||
-- @param cnfg a configuration table that may contain these fields:
|
||||
-- @tab[opt] cnfg a configuration table that may contain these fields:
|
||||
--
|
||||
-- * `variablilize` make names into valid Lua identifiers (default `true`)
|
||||
-- * `convert_numbers` function to convert values into numbers (default `tonumber`)
|
||||
-- * `trim_space` ensure that there is no starting or trailing whitespace with values (default `true`)
|
||||
-- * `trim_quotes` remove quotes from strings (default `false`)
|
||||
-- * `smart` try to deduce what kind of config file we have (default false)
|
||||
-- * `variablilize` make names into valid Lua identifiers (default true)
|
||||
-- * `convert_numbers` try to convert values into numbers (default true)
|
||||
-- * `trim_space` ensure that there is no starting or trailing whitespace with values (default true)
|
||||
-- * `trim_quotes` remove quotes from strings (default false)
|
||||
-- * `list_delim` delimiter to use when separating columns (default ',')
|
||||
-- * `ignore_assign` ignore any key-pair assignments (default `false`)
|
||||
-- * `kepsep` use this as key-pair separator (default '=')
|
||||
--
|
||||
-- @return a table containing items, or nil
|
||||
-- @return error message (same as @{config.lines})
|
||||
-- * `keysep` separator between key and value pairs (default '=')
|
||||
--
|
||||
-- @return a table containing items, or `nil`
|
||||
-- @return error message (same as @{config.lines}
|
||||
function config.read(file,cnfg)
|
||||
local f,openf,err
|
||||
local f,openf,err,auto
|
||||
|
||||
local iter,err = config.lines(file)
|
||||
if not iter then return nil,err end
|
||||
local line = iter()
|
||||
cnfg = cnfg or {}
|
||||
if cnfg.smart then
|
||||
auto = true
|
||||
if line:match '^[^=]+=' then
|
||||
cnfg.keysep = '='
|
||||
elseif line:match '^[^:]+:' then
|
||||
cnfg.keysep = ':'
|
||||
cnfg.list_delim = ':'
|
||||
elseif line:match '^%S+%s+' then
|
||||
cnfg.keysep = ' '
|
||||
-- more than two columns assume that it's a space-delimited list
|
||||
-- cf /etc/fstab with /etc/ssh/ssh_config
|
||||
if line:match '^%S+%s+%S+%s+%S+' then
|
||||
cnfg.list_delim = ' '
|
||||
end
|
||||
cnfg.variabilize = false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function check_cnfg (var,def)
|
||||
local val = cnfg[var]
|
||||
if val == nil then return def else return val end
|
||||
end
|
||||
|
||||
local initial_digits = '^[%d%+%-]'
|
||||
local t = {}
|
||||
local top_t = t
|
||||
local variablilize = check_cnfg ('variabilize',true)
|
||||
local list_delim = check_cnfg('list_delim',',')
|
||||
local convert_numbers = check_cnfg('convert_numbers',tonumber)
|
||||
if convert_numbers==true then convert_numbers = tonumber end
|
||||
local convert_numbers = check_cnfg('convert_numbers',true)
|
||||
local trim_space = check_cnfg('trim_space',true)
|
||||
local trim_quotes = check_cnfg('trim_quotes',false)
|
||||
local ignore_assign = check_cnfg('ignore_assign',false)
|
||||
local keysep = check_cnfg('keysep','=')
|
||||
local keypat = keysep == ' ' and '%s+' or '%s*'..keysep..'%s*'
|
||||
if list_delim == ' ' then list_delim = '%s+' end
|
||||
|
||||
local function process_name(key)
|
||||
if variablilize then
|
||||
|
@ -134,26 +159,26 @@ function config.read(file,cnfg)
|
|||
for i,v in ipairs(value) do
|
||||
value[i] = process_value(v)
|
||||
end
|
||||
elseif convert_numbers and value:find('^[%d%+%-]') then
|
||||
local val = convert_numbers(value)
|
||||
elseif convert_numbers and value:find(initial_digits) then
|
||||
local val = tonumber(value)
|
||||
if not val and value:match ' kB$' then
|
||||
value = value:gsub(' kB','')
|
||||
val = tonumber(value)
|
||||
end
|
||||
if val then value = val end
|
||||
end
|
||||
if type(value) == 'string' then
|
||||
if trim_space then value = strip(value) end
|
||||
if not trim_quotes and auto and value:match '^"' then
|
||||
trim_quotes = true
|
||||
end
|
||||
if trim_quotes then value = strip_quotes(value) end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local iter,err = config.lines(file)
|
||||
if not iter then return nil,err end
|
||||
for line in iter do
|
||||
-- strips comments
|
||||
local ci = line:find('%s*[#;]')
|
||||
if ci then line = line:sub(1,ci-1) end
|
||||
-- and ignore blank lines
|
||||
if line:find('^%s*$') then
|
||||
elseif line:find('^%[') then -- section!
|
||||
while line do
|
||||
if line:find('^%[') then -- section!
|
||||
local section = process_name(line:match('%[([^%]]+)%]'))
|
||||
t = top_t
|
||||
t[section] = {}
|
||||
|
@ -169,8 +194,10 @@ function config.read(file,cnfg)
|
|||
t[#t+1] = process_value(line)
|
||||
end
|
||||
end
|
||||
line = iter()
|
||||
end
|
||||
return top_t
|
||||
end
|
||||
|
||||
return config
|
||||
|
||||
|
|
|
@ -20,11 +20,11 @@
|
|||
local utils = require 'pl.utils'
|
||||
local _DEBUG = rawget(_G,'_DEBUG')
|
||||
|
||||
local patterns,function_arg,usplit = utils.patterns,utils.function_arg,utils.split
|
||||
local patterns,function_arg,usplit,array_tostring = utils.patterns,utils.function_arg,utils.split,utils.array_tostring
|
||||
local append,concat = table.insert,table.concat
|
||||
local gsub = string.gsub
|
||||
local io = io
|
||||
local _G,print,type,tonumber,ipairs,setmetatable,pcall,error,setfenv = _G,print,type,tonumber,ipairs,setmetatable,pcall,error,setfenv
|
||||
local _G,print,type,tonumber,ipairs,setmetatable,pcall,error = _G,print,type,tonumber,ipairs,setmetatable,pcall,error
|
||||
|
||||
|
||||
local data = {}
|
||||
|
@ -38,25 +38,50 @@ local function count(s,chr)
|
|||
end
|
||||
|
||||
local function rstrip(s)
|
||||
return s:gsub('%s+$','')
|
||||
return (s:gsub('%s+$',''))
|
||||
end
|
||||
|
||||
local function strip (s)
|
||||
return (rstrip(s):gsub('^%s*',''))
|
||||
end
|
||||
|
||||
-- this gives `l` the standard List metatable, so that if you
|
||||
-- do choose to pull in pl.List, you can use its methods on such lists.
|
||||
local function make_list(l)
|
||||
return setmetatable(l,utils.stdmt.List)
|
||||
end
|
||||
|
||||
local function split(s,delim)
|
||||
return make_list(usplit(s,delim))
|
||||
end
|
||||
|
||||
local function map(fun,t)
|
||||
local res = {}
|
||||
for i = 1,#t do
|
||||
append(res,fun(t[i]))
|
||||
res[i] = fun(t[i])
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local function split(line,delim,csv,n)
|
||||
local massage
|
||||
-- CSV fields may be double-quoted and may contain commas!
|
||||
if csv and line:match '"' then
|
||||
line = line:gsub('"([^"]+)"',function(str)
|
||||
local s,cnt = str:gsub(',','\001')
|
||||
if cnt > 0 then massage = true end
|
||||
return s
|
||||
end)
|
||||
if massage then
|
||||
massage = function(s) return (s:gsub('\001',',')) end
|
||||
end
|
||||
end
|
||||
local res = (usplit(line,delim,false,n))
|
||||
if csv then
|
||||
-- restore CSV commas-in-fields
|
||||
if massage then res = map(massage,res) end
|
||||
-- in CSV mode trailiing commas are significant!
|
||||
if line:match ',$' then append(res,'') end
|
||||
end
|
||||
return make_list(res)
|
||||
end
|
||||
|
||||
local function find(t,v)
|
||||
for i = 1,#t do
|
||||
if v == t[i] then return i end
|
||||
|
@ -109,16 +134,16 @@ end
|
|||
-- @function Data.column_by_name
|
||||
|
||||
--- return a query iterator on this data (method).
|
||||
-- @param condn the query expression
|
||||
-- @string condn the query expression
|
||||
-- @function Data.select
|
||||
-- @see data.query
|
||||
|
||||
--- return a row iterator on this data (method).
|
||||
-- @param condn the query expression
|
||||
-- @string condn the query expression
|
||||
-- @function Data.select_row
|
||||
|
||||
--- return a new data object based on this query (method).
|
||||
-- @param condn the query expression
|
||||
-- @string condn the query expression
|
||||
-- @function Data.copy_select
|
||||
|
||||
--- return the field names of this data object (method).
|
||||
|
@ -177,110 +202,127 @@ end
|
|||
--- read a delimited file in a Lua table.
|
||||
-- By default, attempts to treat first line as separated list of fieldnames.
|
||||
-- @param file a filename or a file-like object (default stdin)
|
||||
-- @param cnfg options table: can override delim (a string pattern), fieldnames (a list),
|
||||
-- specify no_convert (default is to convert), numfields (indices of columns known
|
||||
-- to be numbers) and thousands_dot (thousands separator in Excel CSV is '.')
|
||||
-- @tab cnfg options table: can override `delim` (a string pattern), `fieldnames` (a list),
|
||||
-- specify `no_convert` (default is to conversion), `numfields` (indices of columns known
|
||||
-- to be numbers) and `thousands_dot` (thousands separator in Excel CSV is '.').
|
||||
-- If `csv` is set then fields may be double-quoted and contain commas;
|
||||
-- @return `data` object, or `nil`
|
||||
-- @return error message. May be a file error, 'not a file-like object'
|
||||
-- or a conversion error
|
||||
function data.read(file,cnfg)
|
||||
local convert,err,opened
|
||||
local err,opened,count,line,csv
|
||||
local D = {}
|
||||
if not cnfg then cnfg = {} end
|
||||
local f,err,opened = open_file(file,'r')
|
||||
if not f then return nil, err end
|
||||
local thousands_dot = cnfg.thousands_dot
|
||||
|
||||
local function try_tonumber(x)
|
||||
-- note that using dot as the thousands separator (@thousands_dot)
|
||||
-- requires a special conversion function!
|
||||
local tonumber = tonumber
|
||||
local function try_number(x)
|
||||
if thousands_dot then x = x:gsub('%.(...)','%1') end
|
||||
return tonumber(x)
|
||||
local v = tonumber(x)
|
||||
if v == nil then return nil,"not a number" end
|
||||
return v
|
||||
end
|
||||
|
||||
local line = f:read()
|
||||
csv = cnfg.csv
|
||||
if csv then cnfg.delim = ',' end
|
||||
count = 1
|
||||
line = f:read()
|
||||
if not line then return nil, "empty file" end
|
||||
|
||||
-- first question: what is the delimiter?
|
||||
D.delim = cnfg.delim and cnfg.delim or guess_delim(line)
|
||||
local delim = D.delim
|
||||
local collect_end = cnfg.last_field_collect
|
||||
local numfields = cnfg.numfields
|
||||
|
||||
local conversion
|
||||
local numfields = {}
|
||||
local function append_conversion (idx,conv)
|
||||
conversion = conversion or {}
|
||||
append(numfields,idx)
|
||||
append(conversion,conv)
|
||||
end
|
||||
if cnfg.numfields then
|
||||
for _,n in ipairs(cnfg.numfields) do append_conversion(n,try_number) end
|
||||
end
|
||||
|
||||
-- some space-delimited data starts with a space. This should not be a column,
|
||||
-- although it certainly would be for comma-separated, etc.
|
||||
local strip
|
||||
local stripper
|
||||
if delim == '%s+' and line:find(delim) == 1 then
|
||||
strip = function(s) return s:gsub('^%s+','') end
|
||||
line = strip(line)
|
||||
stripper = function(s) return s:gsub('^%s+','') end
|
||||
line = stripper(line)
|
||||
end
|
||||
-- first line will usually be field names. Unless fieldnames are specified,
|
||||
-- we check if it contains purely numerical values for the case of reading
|
||||
-- plain data files.
|
||||
if not cnfg.fieldnames then
|
||||
local fields = split(line,delim)
|
||||
local nums = map(tonumber,fields)
|
||||
if #nums == #fields then
|
||||
convert = tonumber
|
||||
append(D,nums)
|
||||
numfields = {}
|
||||
for i = 1,#nums do numfields[i] = i end
|
||||
else
|
||||
local fields,nums
|
||||
fields = split(line,delim,csv)
|
||||
if not cnfg.convert then
|
||||
nums = map(tonumber,fields)
|
||||
if #nums == #fields then -- they're ALL numbers!
|
||||
append(D,nums) -- add the first converted row
|
||||
-- and specify conversions for subsequent rows
|
||||
for i = 1,#nums do append_conversion(i,try_number) end
|
||||
else -- we'll try to check numbers just now..
|
||||
nums = nil
|
||||
end
|
||||
else -- [explicit column conversions] (any deduced number conversions will be added)
|
||||
for idx,conv in pairs(cnfg.convert) do append_conversion(idx,conv) end
|
||||
end
|
||||
if nums == nil then
|
||||
cnfg.fieldnames = fields
|
||||
end
|
||||
line = f:read()
|
||||
if strip then line = strip(line) end
|
||||
count = count + 1
|
||||
if stripper then line = stripper(line) end
|
||||
elseif type(cnfg.fieldnames) == 'string' then
|
||||
cnfg.fieldnames = split(cnfg.fieldnames,delim)
|
||||
cnfg.fieldnames = split(cnfg.fieldnames,delim,csv)
|
||||
end
|
||||
local nfields
|
||||
-- at this point, the column headers have been read in. If the first
|
||||
-- row consisted of numbers, it has already been added to the dataset.
|
||||
if cnfg.fieldnames then
|
||||
D.fieldnames = cnfg.fieldnames
|
||||
-- [conversion] unless @no_convert, we need the numerical field indices
|
||||
-- of the first data row. Can also be specified by @numfields.
|
||||
-- [collecting end field] If @last_field_collect then we'll
|
||||
-- only split as many fields as there are fieldnames
|
||||
if cnfg.last_field_collect then
|
||||
nfields = #D.fieldnames
|
||||
end
|
||||
-- [implicit column conversion] unless @no_convert, we need the numerical field indices
|
||||
-- of the first data row. These can also be specified explicitly by @numfields.
|
||||
if not cnfg.no_convert then
|
||||
if not numfields then
|
||||
numfields = {}
|
||||
local fields = split(line,D.delim)
|
||||
for i = 1,#fields do
|
||||
if tonumber(fields[i]) then
|
||||
append(numfields,i)
|
||||
end
|
||||
local fields = split(line,D.delim,csv,nfields)
|
||||
for i = 1,#fields do
|
||||
if not find(numfields,i) and tonumber(fields[i]) then
|
||||
append_conversion(i,try_number)
|
||||
end
|
||||
end
|
||||
if #numfields > 0 then -- there are numerical fields
|
||||
-- note that using dot as the thousands separator (@thousands_dot)
|
||||
-- requires a special conversion function!
|
||||
convert = thousands_dot and try_tonumber or tonumber
|
||||
end
|
||||
end
|
||||
end
|
||||
-- keep going until finished
|
||||
while line do
|
||||
if not line:find ('^%s*$') then
|
||||
if strip then line = strip(line) end
|
||||
local fields = split(line,delim)
|
||||
if convert then
|
||||
if not line:find ('^%s*$') then -- [blank lines] ignore them!
|
||||
if stripper then line = stripper(line) end
|
||||
local fields = split(line,delim,csv,nfields)
|
||||
if conversion then -- there were field conversions...
|
||||
for k = 1,#numfields do
|
||||
local i = numfields[k]
|
||||
local val = convert(fields[i])
|
||||
local i,conv = numfields[k],conversion[k]
|
||||
local val,err = conv(fields[i])
|
||||
if val == nil then
|
||||
return nil, "not a number: "..fields[i]
|
||||
return nil, err..": "..fields[i].." at line "..count
|
||||
else
|
||||
fields[i] = val
|
||||
end
|
||||
end
|
||||
end
|
||||
-- [collecting end field] If @last_field_collect then we will collect
|
||||
-- all extra space-delimited fields into a single last field.
|
||||
if collect_end and #fields > #D.fieldnames then
|
||||
local ends,N = {},#D.fieldnames
|
||||
for i = N+1,#fields do
|
||||
append(ends,fields[i])
|
||||
end
|
||||
ends = concat(ends,' ')
|
||||
local cfields = {}
|
||||
for i = 1,N do cfields[i] = fields[i] end
|
||||
cfields[N] = cfields[N]..' '..ends
|
||||
fields = cfields
|
||||
end
|
||||
append(D,fields)
|
||||
end
|
||||
line = f:read()
|
||||
count = count + 1
|
||||
end
|
||||
if opened then f:close() end
|
||||
if delim == '%s+' then D.delim = ' ' end
|
||||
|
@ -289,7 +331,8 @@ function data.read(file,cnfg)
|
|||
end
|
||||
|
||||
local function write_row (data,f,row,delim)
|
||||
f:write(concat(row,delim),'\n')
|
||||
data.temp = array_tostring(row,data.temp)
|
||||
f:write(concat(data.temp,delim),'\n')
|
||||
end
|
||||
|
||||
function DataMT:write_row(f,row)
|
||||
|
@ -301,8 +344,8 @@ end
|
|||
-- generated with `new` or `read`.
|
||||
-- @param data 2D array
|
||||
-- @param file filename or file-like object
|
||||
-- @param fieldnames list of fields (optional)
|
||||
-- @param delim delimiter (default tab)
|
||||
-- @tparam[opt] {string} fieldnames list of fields (optional)
|
||||
-- @string[opt='\t'] delim delimiter (default tab)
|
||||
function data.write (data,file,fieldnames,delim)
|
||||
local f,err,opened = open_file(file,'w')
|
||||
if not f then return nil, err end
|
||||
|
@ -321,10 +364,13 @@ function DataMT:write(file)
|
|||
data.write(self,file,self.fieldnames,self.delim)
|
||||
end
|
||||
|
||||
local function massage_fieldnames (fields)
|
||||
local function massage_fieldnames (fields,copy)
|
||||
-- fieldnames must be valid Lua identifiers; ignore any surrounding padding
|
||||
-- but keep the original fieldnames...
|
||||
for i = 1,#fields do
|
||||
fields[i] = rstrip(fields[i]):gsub('^%s*',''):gsub('%W','_')
|
||||
local f = strip(fields[i])
|
||||
copy[i] = f
|
||||
fields[i] = f:gsub('%W','_')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -335,7 +381,7 @@ end
|
|||
-- If the table does not have a field called 'delim', then an attempt will be
|
||||
-- made to guess it from the fieldnames string, defaults otherwise to tab.
|
||||
-- @param d the table.
|
||||
-- @param fieldnames optional fieldnames
|
||||
-- @tparam[opt] {string} fieldnames optional fieldnames
|
||||
-- @return the table.
|
||||
function data.new (d,fieldnames)
|
||||
d.fieldnames = d.fieldnames or fieldnames or ''
|
||||
|
@ -344,7 +390,8 @@ function data.new (d,fieldnames)
|
|||
d.fieldnames = split(d.fieldnames,d.delim)
|
||||
end
|
||||
d.fieldnames = make_list(d.fieldnames)
|
||||
massage_fieldnames(d.fieldnames)
|
||||
d.original_fieldnames = {}
|
||||
massage_fieldnames(d.fieldnames,d.original_fieldnames)
|
||||
setmetatable(d,DataMT)
|
||||
-- a query with just the fieldname will return a sequence
|
||||
-- of values, which seq.copy turns into a table.
|
||||
|
@ -545,7 +592,7 @@ function data.query(data,condn,context,return_row)
|
|||
end
|
||||
if _DEBUG then print(query) end
|
||||
|
||||
local fn,err = loadstring(query,'tmp')
|
||||
local fn,err = utils.load(query,'tmp')
|
||||
if not fn then return nil,err end
|
||||
fn = fn() -- get the function
|
||||
if condn.where then
|
||||
|
@ -557,7 +604,7 @@ function data.query(data,condn,context,return_row)
|
|||
-- 'injected'into the condition's custom context
|
||||
append(context,_G)
|
||||
local lookup = {}
|
||||
setfenv(qfun,lookup)
|
||||
utils.setfenv(qfun,lookup)
|
||||
setmetatable(lookup,{
|
||||
__index = function(tbl,key)
|
||||
-- _G.print(tbl,key)
|
||||
|
@ -577,10 +624,10 @@ DataMT.select_row = function(d,condn,context)
|
|||
end
|
||||
|
||||
--- Filter input using a query.
|
||||
-- @param Q a query string
|
||||
-- @string Q a query string
|
||||
-- @param infile filename or file-like object
|
||||
-- @param outfile filename or file-like object
|
||||
-- @param dont_fail true if you want to return an error, not just fail
|
||||
-- @bool dont_fail true if you want to return an error, not just fail
|
||||
function data.filter (Q,infile,outfile,dont_fail)
|
||||
local err
|
||||
local d = data.read(infile or 'stdin')
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
--- Useful functions for getting directory contents and matching them against wildcards.
|
||||
--
|
||||
-- Dependencies: `pl.utils`, `pl.path`, `pl.tablex`
|
||||
--
|
||||
|
@ -39,9 +38,9 @@ end
|
|||
|
||||
--- does the filename match the shell pattern?.
|
||||
-- (cf. fnmatch.fnmatch in Python, 11.8)
|
||||
-- @param file A file name
|
||||
-- @param pattern A shell pattern
|
||||
-- @return true or false
|
||||
-- @string file A file name
|
||||
-- @string pattern A shell pattern
|
||||
-- @treturn bool
|
||||
-- @raise file and pattern must be strings
|
||||
function dir.fnmatch(file,pattern)
|
||||
assert_string(1,file)
|
||||
|
@ -51,9 +50,9 @@ end
|
|||
|
||||
--- return a list of all files which match the pattern.
|
||||
-- (cf. fnmatch.filter in Python, 11.8)
|
||||
-- @param files A table containing file names
|
||||
-- @param pattern A shell pattern.
|
||||
-- @return list of files
|
||||
-- @string files A table containing file names
|
||||
-- @string pattern A shell pattern.
|
||||
-- @treturn List(string) list of files
|
||||
-- @raise file and pattern must be strings
|
||||
function dir.filter(files,pattern)
|
||||
assert_arg(1,files,'table')
|
||||
|
@ -82,9 +81,9 @@ local function _listfiles(dir,filemode,match)
|
|||
end
|
||||
|
||||
--- return a list of all files in a directory which match the a shell pattern.
|
||||
-- @param dir A directory. If not given, all files in current directory are returned.
|
||||
-- @param mask A shell pattern. If not given, all files are returned.
|
||||
-- @return lsit of files
|
||||
-- @string dir A directory. If not given, all files in current directory are returned.
|
||||
-- @string mask A shell pattern. If not given, all files are returned.
|
||||
-- @treturn {string} list of files
|
||||
-- @raise dir and mask must be strings
|
||||
function dir.getfiles(dir,mask)
|
||||
assert_dir(1,dir)
|
||||
|
@ -100,9 +99,9 @@ function dir.getfiles(dir,mask)
|
|||
end
|
||||
|
||||
--- return a list of all subdirectories of the directory.
|
||||
-- @param dir A directory
|
||||
-- @return a list of directories
|
||||
-- @raise dir must be a string
|
||||
-- @string dir A directory
|
||||
-- @treturn {string} a list of directories
|
||||
-- @raise dir must be a a valid directory
|
||||
function dir.getdirectories(dir)
|
||||
assert_dir(1,dir)
|
||||
return _listfiles(dir,false)
|
||||
|
@ -208,10 +207,11 @@ local function file_op (is_copy,src,dest,flag)
|
|||
dest = path.normcase(dest)
|
||||
local cmd = is_copy and 'copy' or 'rename'
|
||||
local res, err = execute_command('copy',two_arguments(src,dest))
|
||||
if not res then return nil,err end
|
||||
if not res then return false,err end
|
||||
if not is_copy then
|
||||
return execute_command('del',quote_argument(src))
|
||||
end
|
||||
return true
|
||||
else
|
||||
if path.isdir(dest) then
|
||||
dest = path.join(dest,path.basename(src))
|
||||
|
@ -235,10 +235,10 @@ local function file_op (is_copy,src,dest,flag)
|
|||
end
|
||||
|
||||
--- copy a file.
|
||||
-- @param src source file
|
||||
-- @param dest destination file or directory
|
||||
-- @param flag true if you want to force the copy (default)
|
||||
-- @return true if operation succeeded
|
||||
-- @string src source file
|
||||
-- @string dest destination file or directory
|
||||
-- @bool flag true if you want to force the copy (default)
|
||||
-- @treturn bool operation succeeded
|
||||
-- @raise src and dest must be strings
|
||||
function dir.copyfile (src,dest,flag)
|
||||
assert_string(1,src)
|
||||
|
@ -248,9 +248,9 @@ function dir.copyfile (src,dest,flag)
|
|||
end
|
||||
|
||||
--- move a file.
|
||||
-- @param src source file
|
||||
-- @param dest destination file or directory
|
||||
-- @return true if operation succeeded
|
||||
-- @string src source file
|
||||
-- @string dest destination file or directory
|
||||
-- @treturn bool operation succeeded
|
||||
-- @raise src and dest must be strings
|
||||
function dir.movefile (src,dest)
|
||||
assert_string(1,src)
|
||||
|
@ -293,11 +293,11 @@ end
|
|||
-- before we go deeper. This means that you can modify the returned list of directories before
|
||||
-- continuing.
|
||||
-- This is a clone of os.walk from the Python libraries.
|
||||
-- @param root A starting directory
|
||||
-- @param bottom_up False if we start listing entries immediately.
|
||||
-- @param follow_links follow symbolic links
|
||||
-- @string root A starting directory
|
||||
-- @bool bottom_up False if we start listing entries immediately.
|
||||
-- @bool follow_links follow symbolic links
|
||||
-- @return an iterator returning root,dirs,files
|
||||
-- @raise root must be a string
|
||||
-- @raise root must be a directory
|
||||
function dir.walk(root,bottom_up,follow_links)
|
||||
assert_dir(1,root)
|
||||
local attrib
|
||||
|
@ -310,7 +310,7 @@ function dir.walk(root,bottom_up,follow_links)
|
|||
end
|
||||
|
||||
--- remove a whole directory tree.
|
||||
-- @param fullpath A directory path
|
||||
-- @string fullpath A directory path
|
||||
-- @return true or nil
|
||||
-- @return error if failed
|
||||
-- @raise fullpath must be a string
|
||||
|
@ -341,7 +341,8 @@ function _makepath(p)
|
|||
end
|
||||
if not path.isdir(p) then
|
||||
local subp = p:match(dirpat)
|
||||
if not _makepath(subp) then return raise ('cannot create '..subp) end
|
||||
local ok, err = _makepath(subp)
|
||||
if not ok then return nil, err end
|
||||
return mkdir(p)
|
||||
else
|
||||
return true
|
||||
|
@ -350,9 +351,9 @@ end
|
|||
|
||||
--- create a directory path.
|
||||
-- This will create subdirectories as necessary!
|
||||
-- @param p A directory path
|
||||
-- @return a valid created path
|
||||
-- @raise p must be a string
|
||||
-- @string p A directory path
|
||||
-- @return true on success, nil + errormsg on failure
|
||||
-- @raise failure to create
|
||||
function dir.makepath (p)
|
||||
assert_string(1,p)
|
||||
return _makepath(path.normcase(path.abspath(p)))
|
||||
|
@ -361,10 +362,10 @@ end
|
|||
|
||||
--- clone a directory tree. Will always try to create a new directory structure
|
||||
-- if necessary.
|
||||
-- @param path1 the base path of the source tree
|
||||
-- @param path2 the new base path for the destination
|
||||
-- @param file_fun an optional function to apply on all files
|
||||
-- @param verbose an optional boolean to control the verbosity of the output.
|
||||
-- @string path1 the base path of the source tree
|
||||
-- @string path2 the new base path for the destination
|
||||
-- @func file_fun an optional function to apply on all files
|
||||
-- @bool verbose an optional boolean to control the verbosity of the output.
|
||||
-- It can also be a logging function that behaves like print()
|
||||
-- @return true, or nil
|
||||
-- @return error message, or list of failed directory creations
|
||||
|
@ -416,7 +417,7 @@ function dir.clonetree (path1,path2,file_fun,verbose)
|
|||
end
|
||||
|
||||
--- return an iterator over all entries in a directory tree
|
||||
-- @param d a directory
|
||||
-- @string d a directory
|
||||
-- @return an iterator giving pathname and mode (true for dir, false otherwise)
|
||||
-- @raise d must be a non-empty string
|
||||
function dir.dirtree( d )
|
||||
|
@ -448,12 +449,12 @@ function dir.dirtree( d )
|
|||
end
|
||||
|
||||
|
||||
--- Recursively returns all the file starting at <i>path</i>. It can optionally take a shell pattern and
|
||||
-- only returns files that match <i>pattern</i>. If a pattern is given it will do a case insensitive search.
|
||||
-- @param start_path {string} A directory. If not given, all files in current directory are returned.
|
||||
-- @param pattern {string} A shell pattern. If not given, all files are returned.
|
||||
-- @return Table containing all the files found recursively starting at <i>path</i> and filtered by <i>pattern</i>.
|
||||
-- @raise start_path must be a string
|
||||
--- Recursively returns all the file starting at _path_. It can optionally take a shell pattern and
|
||||
-- only returns files that match _pattern_. If a pattern is given it will do a case insensitive search.
|
||||
-- @string start_path A directory. If not given, all files in current directory are returned.
|
||||
-- @string pattern A shell pattern. If not given, all files are returned.
|
||||
-- @treturn List(string) containing all the files found recursively starting at _path_ and filtered by _pattern_.
|
||||
-- @raise start_path must be a directory
|
||||
function dir.getallfiles( start_path, pattern )
|
||||
assert_dir(1,start_path)
|
||||
pattern = pattern or ""
|
||||
|
@ -469,7 +470,7 @@ function dir.getallfiles( start_path, pattern )
|
|||
end
|
||||
end
|
||||
|
||||
return files
|
||||
return setmetatable(files,List)
|
||||
end
|
||||
|
||||
return dir
|
||||
|
|
|
@ -13,58 +13,50 @@ module ('pl.file',utils._module)
|
|||
local file = {}
|
||||
|
||||
--- return the contents of a file as a string
|
||||
-- @class function
|
||||
-- @name file.read
|
||||
-- @param filename The file path
|
||||
-- @function file.read
|
||||
-- @string filename The file path
|
||||
-- @return file contents
|
||||
file.read = utils.readfile
|
||||
|
||||
--- write a string to a file
|
||||
-- @class function
|
||||
-- @name file.write
|
||||
-- @param filename The file path
|
||||
-- @param str The string
|
||||
-- @function file.write
|
||||
-- @string filename The file path
|
||||
-- @string str The string
|
||||
file.write = utils.writefile
|
||||
|
||||
--- copy a file.
|
||||
-- @class function
|
||||
-- @name file.copy
|
||||
-- @param src source file
|
||||
-- @param dest destination file
|
||||
-- @param flag true if you want to force the copy (default)
|
||||
-- @function file.copy
|
||||
-- @string src source file
|
||||
-- @string dest destination file
|
||||
-- @bool flag true if you want to force the copy (default)
|
||||
-- @return true if operation succeeded
|
||||
file.copy = dir.copyfile
|
||||
|
||||
--- move a file.
|
||||
-- @class function
|
||||
-- @name file.move
|
||||
-- @param src source file
|
||||
-- @param dest destination file
|
||||
-- @function file.move
|
||||
-- @string src source file
|
||||
-- @string dest destination file
|
||||
-- @return true if operation succeeded, else false and the reason for the error.
|
||||
file.move = dir.movefile
|
||||
|
||||
--- Return the time of last access as the number of seconds since the epoch.
|
||||
-- @class function
|
||||
-- @name file.access_time
|
||||
-- @param path A file path
|
||||
-- @function file.access_time
|
||||
-- @string path A file path
|
||||
file.access_time = path.getatime
|
||||
|
||||
---Return when the file was created.
|
||||
-- @class function
|
||||
-- @name file.creation_time
|
||||
-- @param path A file path
|
||||
-- @function file.creation_time
|
||||
-- @string path A file path
|
||||
file.creation_time = path.getctime
|
||||
|
||||
--- Return the time of last modification
|
||||
-- @class function
|
||||
-- @name file.modified_time
|
||||
-- @param path A file path
|
||||
-- @function file.modified_time
|
||||
-- @string path A file path
|
||||
file.modified_time = path.getmtime
|
||||
|
||||
--- Delete a file
|
||||
-- @class function
|
||||
-- @name file.delete
|
||||
-- @param path A file path
|
||||
-- @function file.delete
|
||||
-- @string path A file path
|
||||
file.delete = os.remove
|
||||
|
||||
return file
|
||||
|
|
|
@ -19,11 +19,9 @@
|
|||
-- @module pl.func
|
||||
local type,select,setmetatable,getmetatable,rawset = type,select,setmetatable,getmetatable,rawset
|
||||
local concat,append = table.concat,table.insert
|
||||
local max = math.max
|
||||
local print,tostring = print,tostring
|
||||
local pairs,ipairs,loadstring,rawget,unpack = pairs,ipairs,loadstring,rawget,unpack
|
||||
local _G = _G
|
||||
local tostring = tostring
|
||||
local utils = require 'pl.utils'
|
||||
local pairs,ipairs,loadstring,rawget,unpack = pairs,ipairs,loadstring,rawget,utils.unpack
|
||||
local tablex = require 'pl.tablex'
|
||||
local map = tablex.map
|
||||
local _DEBUG = rawget(_G,'_DEBUG')
|
||||
|
@ -63,8 +61,8 @@ func._0 = P{op='X',repr='...',index=0}
|
|||
function func.Var (name)
|
||||
local ls = utils.split(name,'[%s,]+')
|
||||
local res = {}
|
||||
for _,n in ipairs(ls) do
|
||||
append(res,P{op='X',repr=n,index=0})
|
||||
for i = 1, #ls do
|
||||
append(res,P{op='X',repr=ls[i],index=0})
|
||||
end
|
||||
return unpack(res)
|
||||
end
|
||||
|
@ -124,8 +122,8 @@ end
|
|||
|
||||
--- wrap a table of functions. This makes them available for use in
|
||||
-- placeholder expressions.
|
||||
-- @param tname a table name
|
||||
-- @param context context to put results, defaults to environment of caller
|
||||
-- @string tname a table name
|
||||
-- @tab context context to put results, defaults to environment of caller
|
||||
function func.import(tname,context)
|
||||
assert_arg(1,tname,'string',is_global_table,'arg# 1: not a name of a global table')
|
||||
local t = _G[tname]
|
||||
|
@ -137,8 +135,8 @@ function func.import(tname,context)
|
|||
end
|
||||
|
||||
--- register a function for use in placeholder expressions.
|
||||
-- @param fun a function
|
||||
-- @param name an optional name
|
||||
-- @func fun a function
|
||||
-- @string[opt] name an optional name
|
||||
-- @return a placeholder functiond
|
||||
function func.register (fun,name)
|
||||
assert_arg(1,fun,'function')
|
||||
|
@ -182,7 +180,7 @@ binreg (_PEMT,{__add='+',__sub='-',__mul='*',__div='/',__mod='%',__pow='^',__con
|
|||
binreg (_PEMT,{__eq='=='})
|
||||
|
||||
--- all elements of a table except the first.
|
||||
-- @param ls a list-like table.
|
||||
-- @tab ls a list-like table.
|
||||
function func.tail (ls)
|
||||
assert_arg(1,ls,'table')
|
||||
local res = {}
|
||||
|
@ -243,14 +241,15 @@ function collect_values (e,vlist)
|
|||
if isPE(e) then
|
||||
if e.op ~= 'X' then
|
||||
local m = 0
|
||||
for i,subx in ipairs(e) do
|
||||
for i = 1,#e do
|
||||
local subx = e[i]
|
||||
local pe = isPE(subx)
|
||||
if pe then
|
||||
if subx.op == 'X' and subx.index == 'wrap' then
|
||||
subx = subx.repr
|
||||
pe = false
|
||||
else
|
||||
m = max(m,collect_values(subx,vlist))
|
||||
m = math.max(m,collect_values(subx,vlist))
|
||||
end
|
||||
end
|
||||
if not pe then
|
||||
|
@ -290,7 +289,7 @@ function func.instantiate (e)
|
|||
rep = repr(e)
|
||||
local fstr = ('return function(%s) return function(%s) return %s end end'):format(consts,parms,rep)
|
||||
if _DEBUG then print(fstr) end
|
||||
fun,err = loadstring(fstr,'fun')
|
||||
fun,err = utils.load(fstr,'fun')
|
||||
if not fun then return nil,err end
|
||||
fun = fun() -- get wrapper
|
||||
fun = fun(unpack(values)) -- call wrapper (values could be empty)
|
||||
|
@ -311,17 +310,17 @@ end
|
|||
utils.add_function_factory(_PEMT,func.I)
|
||||
|
||||
--- bind the first parameter of the function to a value.
|
||||
-- @class function
|
||||
-- @name func.curry
|
||||
-- @param fn a function of one or more arguments
|
||||
-- @function func.bind1
|
||||
-- @func fn a function of one or more arguments
|
||||
-- @param p a value
|
||||
-- @return a function of one less argument
|
||||
-- @usage (curry(math.max,10))(20) == math.max(10,20)
|
||||
func.curry = utils.bind1
|
||||
-- @usage (bind1(math.max,10))(20) == math.max(10,20)
|
||||
func.bind1 = utils.bind1
|
||||
func.curry = func.bind1
|
||||
|
||||
--- create a function which chains two functions.
|
||||
-- @param f a function of at least one argument
|
||||
-- @param g a function of at least one argument
|
||||
-- @func f a function of at least one argument
|
||||
-- @func g a function of at least one argument
|
||||
-- @return a function
|
||||
-- @usage printf = compose(io.write,string.format)
|
||||
function func.compose (f,g)
|
||||
|
@ -329,8 +328,8 @@ function func.compose (f,g)
|
|||
end
|
||||
|
||||
--- bind the arguments of a function to given values.
|
||||
-- bind(fn,v,_2) is equivalent to curry(fn,v).
|
||||
-- @param fn a function of at least one argument
|
||||
-- `bind(fn,v,_2)` is equivalent to `bind1(fn,v)`.
|
||||
-- @func fn a function of at least one argument
|
||||
-- @param ... values or placeholder variables
|
||||
-- @return a function
|
||||
-- @usage (bind(f,_1,a))(b) == f(a,b)
|
||||
|
@ -343,7 +342,7 @@ function func.bind(fn,...)
|
|||
local a = args[i]
|
||||
if isPE(a) and a.op == 'X' then
|
||||
append(holders,a.repr)
|
||||
maxplace = max(maxplace,a.index)
|
||||
maxplace = math.max(maxplace,a.index)
|
||||
if a.index == 0 then varargs = true end
|
||||
else
|
||||
local v = '_v'..nv
|
||||
|
@ -366,7 +365,7 @@ return function (%s)
|
|||
end
|
||||
]]):format(bvalues,parms,holders)
|
||||
if _DEBUG then print(fstr) end
|
||||
local res,err = loadstring(fstr)
|
||||
local res,err = utils.load(fstr)
|
||||
res = res()
|
||||
return res(fn,unpack(values))
|
||||
end
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
--------------
|
||||
-- PL loader, for loading all PL libraries, only on demand.
|
||||
-- Whenever a module is implicitly accesssed, the table will have the module automatically injected.
|
||||
-- (e.g. `_ENV.tablex`)
|
||||
-- then that module is dynamically loaded. The submodules are all brought into
|
||||
-- the table that is provided as the argument, or returned in a new table.
|
||||
-- If a table is provided, that table's metatable is clobbered, but the values are not.
|
||||
-- This module returns a single function, which is passed the environment.
|
||||
-- If this is `true`, then return a 'shadow table' as the module
|
||||
-- See @{01-introduction.md.To_Inject_or_not_to_Inject_|the Guide}
|
||||
|
||||
-- @module pl.import_into
|
||||
|
||||
return function(env)
|
||||
local mod
|
||||
if env == true then
|
||||
mod = {}
|
||||
env = {}
|
||||
end
|
||||
local env = env or {}
|
||||
|
||||
local modules = {
|
||||
utils = true,path=true,dir=true,tablex=true,stringio=true,sip=true,
|
||||
input=true,seq=true,lexer=true,stringx=true,
|
||||
config=true,pretty=true,data=true,func=true,text=true,
|
||||
operator=true,lapp=true,array2d=true,
|
||||
comprehension=true,xml=true,types=true,
|
||||
test = true, app = true, file = true, class = true, List = true,
|
||||
Map = true, Set = true, OrderedMap = true, MultiMap = true,
|
||||
Date = true,
|
||||
-- classes --
|
||||
}
|
||||
rawset(env,'utils',require 'pl.utils')
|
||||
|
||||
for name,klass in pairs(env.utils.stdmt) do
|
||||
klass.__index = function(t,key)
|
||||
return require ('pl.'..name)[key]
|
||||
end;
|
||||
end
|
||||
|
||||
-- ensure that we play nice with libraries that also attach a metatable
|
||||
-- to the global table; always forward to a custom __index if we don't
|
||||
-- match
|
||||
|
||||
local _hook,_prev_index
|
||||
local gmt = {}
|
||||
local prevenvmt = getmetatable(env)
|
||||
if prevenvmt then
|
||||
_prev_index = prevenvmt.__index
|
||||
if prevenvmt.__newindex then
|
||||
gmt.__index = prevenvmt.__newindex
|
||||
end
|
||||
end
|
||||
|
||||
function gmt.hook(handler)
|
||||
_hook = handler
|
||||
end
|
||||
|
||||
function gmt.__index(t,name)
|
||||
local found = modules[name]
|
||||
-- either true, or the name of the module containing this class.
|
||||
-- either way, we load the required module and make it globally available.
|
||||
if found then
|
||||
-- e..g pretty.dump causes pl.pretty to become available as 'pretty'
|
||||
rawset(env,name,require('pl.'..name))
|
||||
return env[name]
|
||||
else
|
||||
local res
|
||||
if _hook then
|
||||
res = _hook(t,name)
|
||||
if res then return res end
|
||||
end
|
||||
if _prev_index then
|
||||
return _prev_index(t,name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if mod then
|
||||
function gmt.__newindex(t,name,value)
|
||||
mod[name] = value
|
||||
rawset(t,name,value)
|
||||
end
|
||||
end
|
||||
|
||||
setmetatable(env,gmt)
|
||||
|
||||
return env,mod or env
|
||||
end
|
|
@ -1,68 +1,11 @@
|
|||
--------------
|
||||
-- Entry point for loading all PL libraries only on demand.
|
||||
-- Entry point for loading all PL libraries only on demand, into the global space.
|
||||
-- Requiring 'pl' means that whenever a module is implicitly accesssed
|
||||
-- (e.g. `utils.split`)
|
||||
-- then that module is dynamically loaded. The submodules are all brought into
|
||||
-- the global space.
|
||||
--Updated to use @{pl.import_into}
|
||||
-- @module pl
|
||||
|
||||
local modules = {
|
||||
utils = true,path=true,dir=true,tablex=true,stringio=true,sip=true,
|
||||
input=true,seq=true,lexer=true,stringx=true,
|
||||
config=true,pretty=true,data=true,func=true,text=true,
|
||||
operator=true,lapp=true,array2d=true,
|
||||
comprehension=true,xml=true,
|
||||
test = true, app = true, file = true, class = true, List = true,
|
||||
Map = true, Set = true, OrderedMap = true, MultiMap = true,
|
||||
Date = true,
|
||||
-- classes --
|
||||
}
|
||||
_G.utils = require 'pl.utils'
|
||||
|
||||
for name,klass in pairs(_G.utils.stdmt) do
|
||||
klass.__index = function(t,key)
|
||||
return require ('pl.'..name)[key]
|
||||
end;
|
||||
end
|
||||
|
||||
-- ensure that we play nice with libraries that also attach a metatable
|
||||
-- to the global table; always forward to a custom __index if we don't
|
||||
-- match
|
||||
|
||||
local _hook,_prev_index
|
||||
local gmt = {}
|
||||
local prev_gmt = getmetatable(_G)
|
||||
if prev_gmt then
|
||||
_prev_index = prev_gmt.__index
|
||||
if prev_gmt.__newindex then
|
||||
gmt.__index = prev_gmt.__newindex
|
||||
end
|
||||
end
|
||||
|
||||
function gmt.hook(handler)
|
||||
_hook = handler
|
||||
end
|
||||
|
||||
function gmt.__index(t,name)
|
||||
local found = modules[name]
|
||||
-- either true, or the name of the module containing this class.
|
||||
-- either way, we load the required module and make it globally available.
|
||||
if found then
|
||||
-- e..g pretty.dump causes pl.pretty to become available as 'pretty'
|
||||
rawset(_G,name,require('pl.'..name))
|
||||
return _G[name]
|
||||
else
|
||||
local res
|
||||
if _hook then
|
||||
res = _hook(t,name)
|
||||
if res then return res end
|
||||
end
|
||||
if _prev_index then
|
||||
return _prev_index(t,name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
setmetatable(_G,gmt)
|
||||
require'pl.import_into'(_G)
|
||||
|
||||
if rawget(_G,'PENLIGHT_STRICT') then require 'pl.strict' end
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
-- local total,n = seq.sum(input.numbers())
|
||||
-- print('average',total/n)
|
||||
--
|
||||
-- _source_ is defined as a string or a file-like object (i.e. has a read() method which returns the next line)
|
||||
--
|
||||
-- See @{06-data.md.Reading_Unstructured_Text_Data|here}
|
||||
--
|
||||
-- Dependencies: `pl.utils`
|
||||
|
@ -12,22 +14,19 @@ local strfind = string.find
|
|||
local strsub = string.sub
|
||||
local strmatch = string.match
|
||||
local utils = require 'pl.utils'
|
||||
local pairs,type,unpack,tonumber = pairs,type,unpack or table.unpack,tonumber
|
||||
local unpack = utils.unpack
|
||||
local pairs,type,tonumber = pairs,type,tonumber
|
||||
local patterns = utils.patterns
|
||||
local io = io
|
||||
local assert_arg = utils.assert_arg
|
||||
|
||||
--[[
|
||||
module ('pl.input',utils._module)
|
||||
]]
|
||||
|
||||
local input = {}
|
||||
|
||||
--- create an iterator over all tokens.
|
||||
-- based on allwords from PiL, 7.1
|
||||
-- @param getter any function that returns a line of text
|
||||
-- @param pattern
|
||||
-- @param fn Optionally can pass a function to process each token as it/s found.
|
||||
-- @func getter any function that returns a line of text
|
||||
-- @string pattern
|
||||
-- @string[opt] fn Optionally can pass a function to process each token as it's found.
|
||||
-- @return an iterator
|
||||
function input.alltokens (getter,pattern,fn)
|
||||
local line = getter() -- current line
|
||||
|
@ -57,23 +56,23 @@ local alltokens = input.alltokens
|
|||
-- @param f a string or a file-like object (i.e. has a read() method which returns the next line)
|
||||
-- @return a getter function
|
||||
function input.create_getter(f)
|
||||
if f then
|
||||
if type(f) == 'string' then
|
||||
local ls = utils.split(f,'\n')
|
||||
local i,n = 0,#ls
|
||||
return function()
|
||||
i = i + 1
|
||||
if i > n then return nil end
|
||||
return ls[i]
|
||||
end
|
||||
if f then
|
||||
if type(f) == 'string' then
|
||||
local ls = utils.split(f,'\n')
|
||||
local i,n = 0,#ls
|
||||
return function()
|
||||
i = i + 1
|
||||
if i > n then return nil end
|
||||
return ls[i]
|
||||
end
|
||||
else
|
||||
-- anything that supports the read() method!
|
||||
if not f.read then error('not a file-like object') end
|
||||
return function() return f:read() end
|
||||
end
|
||||
else
|
||||
-- anything that supports the read() method!
|
||||
if not f.read then error('not a file-like object') end
|
||||
return function() return f:read() end
|
||||
return io.read -- i.e. just read from stdin
|
||||
end
|
||||
else
|
||||
return io.read -- i.e. just read from stdin
|
||||
end
|
||||
end
|
||||
|
||||
--- generate a sequence of numbers from a source.
|
||||
|
@ -107,9 +106,9 @@ end
|
|||
--- parse an input source into fields.
|
||||
-- By default, will fail if it cannot convert a field to a number.
|
||||
-- @param ids a list of field indices, or a maximum field index
|
||||
-- @param delim delimiter to parse fields (default space)
|
||||
-- @string delim delimiter to parse fields (default space)
|
||||
-- @param f a source @see create_getter
|
||||
-- @param opts option table, {no_fail=true}
|
||||
-- @tab opts option table, `{no_fail=true}`
|
||||
-- @return an iterator with the field values
|
||||
-- @usage for x,y in fields {2,3} do print(x,y) end -- 2nd and 3rd fields from stdin
|
||||
function input.fields (ids,delim,f,opts)
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
-- Does some calculations
|
||||
-- -o,--offset (default 0.0) Offset to add to scaled number
|
||||
-- -s,--scale (number) Scaling factor
|
||||
-- <number> (number ) Number to be scaled
|
||||
-- <number>; (number ) Number to be scaled
|
||||
-- ]]
|
||||
--
|
||||
-- print(args.offset + args.scale * args.number)
|
||||
|
@ -15,7 +15,7 @@
|
|||
-- lines begining wih '<var>' are arguments. Anything in parens after
|
||||
-- the flag/argument is either a default, a type name or a range constraint.
|
||||
--
|
||||
-- >See @{08-additional.md.Command_line_Programs_with_Lapp|the Guide}
|
||||
-- See @{08-additional.md.Command_line_Programs_with_Lapp|the Guide}
|
||||
--
|
||||
-- Dependencies: `pl.sip`
|
||||
-- @module pl.lapp
|
||||
|
@ -51,8 +51,8 @@ local filetypes = {
|
|||
lapp.show_usage_error = true
|
||||
|
||||
--- quit this script immediately.
|
||||
-- @param msg optional message
|
||||
-- @param no_usage suppress 'usage' display
|
||||
-- @string msg optional message
|
||||
-- @bool no_usage suppress 'usage' display
|
||||
function lapp.quit(msg,no_usage)
|
||||
if no_usage == 'throw' then
|
||||
error(msg)
|
||||
|
@ -67,8 +67,8 @@ function lapp.quit(msg,no_usage)
|
|||
end
|
||||
|
||||
--- print an error to stderr and quit.
|
||||
-- @param msg a message
|
||||
-- @param no_usage suppress 'usage' display
|
||||
-- @string msg a message
|
||||
-- @bool no_usage suppress 'usage' display
|
||||
function lapp.error(msg,no_usage)
|
||||
if not lapp.show_usage_error then
|
||||
no_usage = true
|
||||
|
@ -80,8 +80,8 @@ end
|
|||
|
||||
--- open a file.
|
||||
-- This will quit on error, and keep a list of file objects for later cleanup.
|
||||
-- @param file filename
|
||||
-- @param opt same as second parameter of <code>io.open</code>
|
||||
-- @string file filename
|
||||
-- @string[opt] opt same as second parameter of `io.open`
|
||||
function lapp.open (file,opt)
|
||||
local val,err = io.open(file,opt)
|
||||
if not val then lapp.error(err,true) end
|
||||
|
@ -90,8 +90,8 @@ function lapp.open (file,opt)
|
|||
end
|
||||
|
||||
--- quit if the condition is false.
|
||||
-- @param condn a condition
|
||||
-- @param msg an optional message
|
||||
-- @bool condn a condition
|
||||
-- @string msg message text
|
||||
function lapp.assert(condn,msg)
|
||||
if not condn then
|
||||
lapp.error(msg)
|
||||
|
@ -108,7 +108,7 @@ local function xtonumber(s)
|
|||
return val
|
||||
end
|
||||
|
||||
local types
|
||||
local types = {}
|
||||
|
||||
local builtin_types = {string=true,number=true,['file-in']='file',['file-out']='file',boolean=true}
|
||||
|
||||
|
@ -121,7 +121,7 @@ local function convert_parameter(ps,val)
|
|||
elseif builtin_types[ps.type] == 'file' then
|
||||
val = lapp.open(val,(ps.type == 'file-in' and 'r') or 'w' )
|
||||
elseif ps.type == 'boolean' then
|
||||
val = true
|
||||
return val
|
||||
end
|
||||
if ps.constraint then
|
||||
ps.constraint(val)
|
||||
|
@ -131,9 +131,9 @@ end
|
|||
|
||||
--- add a new type to Lapp. These appear in parens after the value like
|
||||
-- a range constraint, e.g. '<ival> (integer) Process PID'
|
||||
-- @param name name of type
|
||||
-- @string name name of type
|
||||
-- @param converter either a function to convert values, or a Lua type name.
|
||||
-- @param constraint optional function to verify values, should use lapp.error
|
||||
-- @func[opt] constraint optional function to verify values, should use lapp.error
|
||||
-- if failed.
|
||||
function lapp.add_type (name,converter,constraint)
|
||||
types[name] = {converter=converter,constraint=constraint}
|
||||
|
@ -143,6 +143,7 @@ local function force_short(short)
|
|||
lapp.assert(#short==1,short..": short parameters should be one character")
|
||||
end
|
||||
|
||||
-- deducing type of variable from default value;
|
||||
local function process_default (sval,vtype)
|
||||
local val
|
||||
if not vtype or vtype == 'number' then
|
||||
|
@ -154,15 +155,18 @@ local function process_default (sval,vtype)
|
|||
local ft = filetypes[sval]
|
||||
return ft[1],ft[2]
|
||||
else
|
||||
if sval == 'true' and not vtype then
|
||||
return true, 'boolean'
|
||||
end
|
||||
if sval:match '^["\']' then sval = sval:sub(2,-2) end
|
||||
return sval,vtype or 'string'
|
||||
end
|
||||
end
|
||||
|
||||
--- process a Lapp options string.
|
||||
-- Usually called as lapp().
|
||||
-- @param str the options text
|
||||
-- @param args a table of arguments (default is `_G.arg`)
|
||||
-- Usually called as `lapp()`.
|
||||
-- @string str the options text
|
||||
-- @tparam {string} args a table of arguments (default is `_G.arg`)
|
||||
-- @return a table with parameter-value pairs
|
||||
function lapp.process_options_string(str,args)
|
||||
local results = {}
|
||||
|
@ -173,7 +177,6 @@ function lapp.process_options_string(str,args)
|
|||
parms = {}
|
||||
aliases = {}
|
||||
parmlist = {}
|
||||
types = {}
|
||||
|
||||
local function check_varargs(s)
|
||||
local res,cnt = s:gsub('^%.%.%.%s*','')
|
||||
|
@ -181,6 +184,7 @@ function lapp.process_options_string(str,args)
|
|||
end
|
||||
|
||||
local function set_result(ps,parm,val)
|
||||
parm = type(parm) == "string" and parm:gsub("%W", "_") or parm -- so foo-bar becomes foo_bar in Lua
|
||||
if not ps.varargs then
|
||||
results[parm] = val
|
||||
else
|
||||
|
@ -203,14 +207,14 @@ function lapp.process_options_string(str,args)
|
|||
end
|
||||
|
||||
-- flags: either '-<short>', '-<short>,--<long>' or '--<long>'
|
||||
if check '-$v{short}, --$v{long} $' or check '-$v{short} $' or check '--$X{long} $' then
|
||||
if check '-$v{short}, --$o{long} $' or check '-$v{short} $' or check '--$o{long} $' then
|
||||
if res.long then
|
||||
optparm = res.long:gsub('%W','_') -- so foo-bar becomes foo_bar in Lua
|
||||
optparm = res.long:gsub('[^%w%-]','_') -- I'm not sure the $o pattern will let anything else through?
|
||||
if res.short then aliases[res.short] = optparm end
|
||||
else
|
||||
optparm = res.short
|
||||
end
|
||||
if res.short then force_short(res.short) end
|
||||
if res.short and not lapp.slack then force_short(res.short) end
|
||||
res.rest, varargs = check_varargs(res.rest)
|
||||
elseif check '$<{name} $' then -- is it <parameter_name>?
|
||||
-- so <input file...> becomes input_file ...
|
||||
|
@ -223,11 +227,17 @@ function lapp.process_options_string(str,args)
|
|||
if res.rest then
|
||||
line = res.rest
|
||||
res = {}
|
||||
-- do we have ([<type>] [default <val>])?
|
||||
local optional
|
||||
-- do we have ([optional] [<type>] [default <val>])?
|
||||
if match('$({def} $',line,res) or match('$({def}',line,res) then
|
||||
local typespec = strip(res.def)
|
||||
local ftype, rest = typespec:match('^(%S+)(.*)$')
|
||||
rest = strip(rest)
|
||||
if ftype == 'optional' then
|
||||
ftype, rest = rest:match('^(%S+)(.*)$')
|
||||
rest = strip(rest)
|
||||
optional = true
|
||||
end
|
||||
local default
|
||||
if ftype == 'default' then
|
||||
default = true
|
||||
|
@ -262,7 +272,6 @@ function lapp.process_options_string(str,args)
|
|||
if default or match('default $r{rest}',typespec,res) then
|
||||
defval,vtype = process_default(res.rest,vtype)
|
||||
end
|
||||
--print('val',optparm,defval,vtype)
|
||||
else -- must be a plain flag, no extra parameter required
|
||||
defval = false
|
||||
vtype = 'boolean'
|
||||
|
@ -270,7 +279,7 @@ function lapp.process_options_string(str,args)
|
|||
local ps = {
|
||||
type = vtype,
|
||||
defval = defval,
|
||||
required = defval == nil,
|
||||
required = defval == nil and not optional,
|
||||
comment = res.rest or optparm,
|
||||
constraint = constraint,
|
||||
varargs = varargs
|
||||
|
@ -305,6 +314,10 @@ function lapp.process_options_string(str,args)
|
|||
return parm,eqi
|
||||
end
|
||||
|
||||
local function is_flag (parm)
|
||||
return parms[aliases[parm] or parm]
|
||||
end
|
||||
|
||||
while i <= #arg do
|
||||
local theArg = arg[i]
|
||||
local res = {}
|
||||
|
@ -312,13 +325,15 @@ function lapp.process_options_string(str,args)
|
|||
if match('--$S{long}',theArg,res) or match('-$S{short}',theArg,res) then
|
||||
if res.long then -- long option
|
||||
parm = check_parm(res.long)
|
||||
elseif #res.short == 1 then
|
||||
elseif #res.short == 1 or is_flag(res.short) then
|
||||
parm = res.short
|
||||
else
|
||||
local parmstr,eq = check_parm(res.short)
|
||||
if not eq then
|
||||
parm = at(parmstr,1)
|
||||
if isdigit(at(parmstr,2)) then
|
||||
local flag = is_flag(parm)
|
||||
if flag and flag.type ~= 'boolean' then
|
||||
--if isdigit(at(parmstr,2)) then
|
||||
-- a short option followed by a digit is an exception (for AW;))
|
||||
-- push ahead into the arg array
|
||||
tinsert(arg,i+1,parmstr:sub(2))
|
||||
|
@ -332,10 +347,10 @@ function lapp.process_options_string(str,args)
|
|||
parm = parmstr
|
||||
end
|
||||
end
|
||||
if parm == 'h' or parm == 'help' then
|
||||
if aliases[parm] then parm = aliases[parm] end
|
||||
if not parms[parm] and (parm == 'h' or parm == 'help') then
|
||||
lapp.quit()
|
||||
end
|
||||
if aliases[parm] then parm = aliases[parm] end
|
||||
else -- a parameter
|
||||
parm = parmlist[iparm]
|
||||
if not parm then
|
||||
|
@ -360,6 +375,8 @@ function lapp.process_options_string(str,args)
|
|||
val = arg[i]
|
||||
end
|
||||
lapp.assert(val,parm.." was expecting a value")
|
||||
else -- toggle boolean flags (usually false -> true)
|
||||
val = not ps.defval
|
||||
end
|
||||
ps.used = true
|
||||
val = convert_parameter(ps,val)
|
||||
|
@ -384,7 +401,10 @@ function lapp.process_options_string(str,args)
|
|||
end
|
||||
|
||||
if arg then
|
||||
script = arg[0]:gsub('.+[\\/]',''):gsub('%.%a+$','')
|
||||
script = arg[0]
|
||||
script = script or rawget(_G,"LAPP_SCRIPT") or "unknown"
|
||||
-- strip dir and extension to get current script name
|
||||
script = script:gsub('.+[\\/]',''):gsub('%.%a+$','')
|
||||
else
|
||||
script = "inter"
|
||||
end
|
||||
|
|
|
@ -68,9 +68,13 @@ local function sdump(tok,options)
|
|||
end
|
||||
|
||||
-- long Lua strings need extra work to get rid of the quotes
|
||||
local function sdump_l(tok,options)
|
||||
local function sdump_l(tok,options,findres)
|
||||
if options and options.string then
|
||||
tok = tok:sub(3,-3)
|
||||
local quotelen = 3
|
||||
if findres[3] then
|
||||
quotelen = quotelen + findres[3]:len()
|
||||
end
|
||||
tok = tok:sub(quotelen,-1 * quotelen)
|
||||
end
|
||||
return yield("string",tok)
|
||||
end
|
||||
|
@ -115,10 +119,10 @@ local function cpp_vdump(tok)
|
|||
end
|
||||
|
||||
--- create a plain token iterator from a string or file-like object.
|
||||
-- @param s the string
|
||||
-- @param matches an optional match table (set of pattern-action pairs)
|
||||
-- @param filter a table of token types to exclude, by default {space=true}
|
||||
-- @param options a table of options; by default, {number=true,string=true},
|
||||
-- @string s the string
|
||||
-- @tab matches an optional match table (set of pattern-action pairs)
|
||||
-- @tab[opt] filter a table of token types to exclude, by default `{space=true}`
|
||||
-- @tab[opt] options a table of options; by default, `{number=true,string=true}`,
|
||||
-- which means convert numbers and strip string quotes.
|
||||
function lexer.scan (s,matches,filter,options)
|
||||
--assert_arg(1,s,'string')
|
||||
|
@ -148,7 +152,8 @@ function lexer.scan (s,matches,filter,options)
|
|||
matches = plain_matches
|
||||
end
|
||||
local function lex ()
|
||||
local i1,i2,idx,res1,res2,tok,pat,fun,capt
|
||||
if type(s)=='string' and s=='' then return end
|
||||
local findres,i1,i2,idx,res1,res2,tok,pat,fun,capt
|
||||
local line = 1
|
||||
if file then s = file:read()..'\n' end
|
||||
local sz = #s
|
||||
|
@ -158,13 +163,15 @@ function lexer.scan (s,matches,filter,options)
|
|||
for _,m in ipairs(matches) do
|
||||
pat = m[1]
|
||||
fun = m[2]
|
||||
i1,i2 = strfind(s,pat,idx)
|
||||
findres = { strfind(s,pat,idx) }
|
||||
i1 = findres[1]
|
||||
i2 = findres[2]
|
||||
if i1 then
|
||||
tok = strsub(s,i1,i2)
|
||||
idx = i2 + 1
|
||||
if not (filter and filter[fun]) then
|
||||
lexer.finished = idx > sz
|
||||
res1,res2 = fun(tok,options)
|
||||
res1,res2 = fun(tok,options,findres)
|
||||
end
|
||||
if res1 then
|
||||
local tp = type(res1)
|
||||
|
@ -218,7 +225,7 @@ end
|
|||
-- @param tok a token stream
|
||||
-- @param a1 a string is the type, a table is a token list and
|
||||
-- a function is assumed to be a token-like iterator (returns type & value)
|
||||
-- @param a2 a string is the value
|
||||
-- @string a2 a string is the value
|
||||
function lexer.insert (tok,a1,a2)
|
||||
if not a1 then return end
|
||||
local ts
|
||||
|
@ -243,7 +250,7 @@ function lexer.getline (tok)
|
|||
return v
|
||||
end
|
||||
|
||||
--- get current line number. <br>
|
||||
--- get current line number.
|
||||
-- Only available if the input source is a file-like object.
|
||||
-- @param tok a token stream
|
||||
-- @return the line number and current column
|
||||
|
@ -260,7 +267,7 @@ function lexer.getrest (tok)
|
|||
end
|
||||
|
||||
--- get the Lua keywords as a set-like table.
|
||||
-- So <code>res["and"]</code> etc would be <code>true</code>.
|
||||
-- So `res["and"]` etc would be `true`.
|
||||
-- @return a table
|
||||
function lexer.get_keywords ()
|
||||
if not lua_keyword then
|
||||
|
@ -277,12 +284,11 @@ function lexer.get_keywords ()
|
|||
return lua_keyword
|
||||
end
|
||||
|
||||
|
||||
--- create a Lua token iterator from a string or file-like object.
|
||||
-- Will return the token type and value.
|
||||
-- @param s the string
|
||||
-- @param filter a table of token types to exclude, by default {space=true,comments=true}
|
||||
-- @param options a table of options; by default, {number=true,string=true},
|
||||
-- @string s the string
|
||||
-- @tab[opt] filter a table of token types to exclude, by default `{space=true,comments=true}`
|
||||
-- @tab[opt] options a table of options; by default, `{number=true,string=true}`,
|
||||
-- which means convert numbers and strip string quotes.
|
||||
function lexer.lua(s,filter,options)
|
||||
filter = filter or {space=true,comments=true}
|
||||
|
@ -297,9 +303,9 @@ function lexer.lua(s,filter,options)
|
|||
{STRING3,sdump},
|
||||
{STRING0,sdump},
|
||||
{STRING1,sdump},
|
||||
{'^%-%-%[%[.-%]%]',cdump},
|
||||
{'^%-%-%[(=*)%[.-%]%1%]',cdump},
|
||||
{'^%-%-.-\n',cdump},
|
||||
{'^%[%[.-%]%]',sdump_l},
|
||||
{'^%[(=*)%[.-%]%1%]',sdump_l},
|
||||
{'^==',tdump},
|
||||
{'^~=',tdump},
|
||||
{'^<=',tdump},
|
||||
|
@ -314,9 +320,9 @@ end
|
|||
|
||||
--- create a C/C++ token iterator from a string or file-like object.
|
||||
-- Will return the token type type and value.
|
||||
-- @param s the string
|
||||
-- @param filter a table of token types to exclude, by default {space=true,comments=true}
|
||||
-- @param options a table of options; by default, {number=true,string=true},
|
||||
-- @string s the string
|
||||
-- @tab[opt] filter a table of token types to exclude, by default `{space=true,comments=true}`
|
||||
-- @tab[opt] options a table of options; by default, `{number=true,string=true}`,
|
||||
-- which means convert numbers and strip string quotes.
|
||||
function lexer.cpp(s,filter,options)
|
||||
filter = filter or {comments=true}
|
||||
|
@ -372,8 +378,8 @@ end
|
|||
|
||||
--- get a list of parameters separated by a delimiter from a stream.
|
||||
-- @param tok the token stream
|
||||
-- @param endtoken end of list (default ')'). Can be '\n'
|
||||
-- @param delim separator (default ',')
|
||||
-- @string[opt=')'] endtoken end of list. Can be '\n'
|
||||
-- @string[opt=','] delim separator
|
||||
-- @return a list of token lists.
|
||||
function lexer.get_separated_list(tok,endtoken,delim)
|
||||
endtoken = endtoken or ')'
|
||||
|
@ -438,8 +444,8 @@ local skipws = lexer.skipws
|
|||
--- get the next token, which must be of the expected type.
|
||||
-- Throws an error if this type does not match!
|
||||
-- @param tok the token stream
|
||||
-- @param expected_type the token type
|
||||
-- @param no_skip_ws whether we should skip whitespace
|
||||
-- @string expected_type the token type
|
||||
-- @bool no_skip_ws whether we should skip whitespace
|
||||
function lexer.expecting (tok,expected_type,no_skip_ws)
|
||||
assert_arg(1,tok,'function')
|
||||
assert_arg(2,expected_type,'string')
|
||||
|
|
|
@ -16,152 +16,151 @@ local utils = require 'pl.utils'
|
|||
|
||||
local operator = {}
|
||||
|
||||
--- apply function to some arguments ()
|
||||
--- apply function to some arguments **()**
|
||||
-- @param fn a function or callable object
|
||||
-- @param ... arguments
|
||||
function operator.call(fn,...)
|
||||
return fn(...)
|
||||
end
|
||||
|
||||
--- get the indexed value from a table []
|
||||
--- get the indexed value from a table **[]**
|
||||
-- @param t a table or any indexable object
|
||||
-- @param k the key
|
||||
function operator.index(t,k)
|
||||
return t[k]
|
||||
end
|
||||
|
||||
--- returns true if arguments are equal ==
|
||||
--- returns true if arguments are equal **==**
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.eq(a,b)
|
||||
return a==b
|
||||
end
|
||||
|
||||
--- returns true if arguments are not equal ~=
|
||||
--- returns true if arguments are not equal **~=**
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.neq(a,b)
|
||||
return a~=b
|
||||
end
|
||||
|
||||
--- returns true if a is less than b <
|
||||
--- returns true if a is less than b **<**
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.lt(a,b)
|
||||
return a < b
|
||||
end
|
||||
|
||||
--- returns true if a is less or equal to b <=
|
||||
--- returns true if a is less or equal to b **<=**
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.le(a,b)
|
||||
return a <= b
|
||||
end
|
||||
|
||||
--- returns true if a is greater than b >
|
||||
--- returns true if a is greater than b **>**
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.gt(a,b)
|
||||
return a > b
|
||||
end
|
||||
|
||||
--- returns true if a is greater or equal to b >=
|
||||
--- returns true if a is greater or equal to b **>=**
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.ge(a,b)
|
||||
return a >= b
|
||||
end
|
||||
|
||||
--- returns length of string or table #
|
||||
--- returns length of string or table **#**
|
||||
-- @param a a string or a table
|
||||
function operator.len(a)
|
||||
return #a
|
||||
end
|
||||
|
||||
--- add two values +
|
||||
--- add two values **+**
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.add(a,b)
|
||||
return a+b
|
||||
end
|
||||
|
||||
--- subtract b from a -
|
||||
--- subtract b from a **-**
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.sub(a,b)
|
||||
return a-b
|
||||
end
|
||||
|
||||
--- multiply two values *
|
||||
--- multiply two values __*__
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.mul(a,b)
|
||||
return a*b
|
||||
end
|
||||
|
||||
--- divide first value by second /
|
||||
--- divide first value by second **/**
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.div(a,b)
|
||||
return a/b
|
||||
end
|
||||
|
||||
--- raise first to the power of second ^
|
||||
--- raise first to the power of second **^**
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.pow(a,b)
|
||||
return a^b
|
||||
end
|
||||
|
||||
--- modulo; remainder of a divided by b %
|
||||
--- modulo; remainder of a divided by b **%**
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.mod(a,b)
|
||||
return a%b
|
||||
end
|
||||
|
||||
--- concatenate two values (either strings or __concat defined) ..
|
||||
--- concatenate two values (either strings or `__concat` defined) **..**
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.concat(a,b)
|
||||
return a..b
|
||||
end
|
||||
|
||||
--- return the negative of a value -
|
||||
--- return the negative of a value **-**
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.unm(a)
|
||||
return -a
|
||||
end
|
||||
|
||||
--- false if value evaluates as true not
|
||||
--- false if value evaluates as true **not**
|
||||
-- @param a value
|
||||
function operator.lnot(a)
|
||||
return not a
|
||||
end
|
||||
|
||||
--- true if both values evaluate as true and
|
||||
--- true if both values evaluate as true **and**
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.land(a,b)
|
||||
return a and b
|
||||
end
|
||||
|
||||
--- true if either value evaluate as true or
|
||||
--- true if either value evaluate as true **or**
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.lor(a,b)
|
||||
return a or b
|
||||
end
|
||||
|
||||
--- make a table from the arguments {}
|
||||
--- make a table from the arguments **{}**
|
||||
-- @param ... non-nil arguments
|
||||
-- @return a table
|
||||
function operator.table (...)
|
||||
return {...}
|
||||
end
|
||||
|
||||
--- match two strings ~
|
||||
--- match two strings **~**.
|
||||
-- uses @{string.find}
|
||||
function operator.match (a,b)
|
||||
return strfind(a,b)~=nil
|
||||
|
@ -174,6 +173,17 @@ function operator.nop (...)
|
|||
return ...
|
||||
end
|
||||
|
||||
---- Map from operator symbol to function.
|
||||
-- Most of these map directly from operators;
|
||||
-- But note these extras
|
||||
--
|
||||
-- * __'()'__ `call`
|
||||
-- * __'[]'__ `index`
|
||||
-- * __'{}'__ `table`
|
||||
-- * __'~'__ `match`
|
||||
--
|
||||
-- @table optable
|
||||
-- @field operator
|
||||
operator.optable = {
|
||||
['+']=operator.add,
|
||||
['-']=operator.sub,
|
||||
|
|
|
@ -33,13 +33,26 @@ end
|
|||
attrib = attributes
|
||||
path.attrib = attrib
|
||||
path.link_attrib = link_attrib
|
||||
|
||||
--- Lua iterator over the entries of a given directory.
|
||||
-- Behaves like `lfs.dir`
|
||||
path.dir = lfs.dir
|
||||
|
||||
--- Creates a directory.
|
||||
path.mkdir = lfs.mkdir
|
||||
|
||||
--- Removes a directory.
|
||||
path.rmdir = lfs.rmdir
|
||||
|
||||
---- Get the working directory.
|
||||
path.currentdir = currentdir
|
||||
|
||||
--- Changes the working directory.
|
||||
path.chdir = lfs.chdir
|
||||
|
||||
|
||||
--- is this a directory?
|
||||
-- @param P A file path
|
||||
-- @string P A file path
|
||||
function path.isdir(P)
|
||||
assert_string(1,P)
|
||||
if P:match("\\$") then
|
||||
|
@ -49,14 +62,14 @@ function path.isdir(P)
|
|||
end
|
||||
|
||||
--- is this a file?.
|
||||
-- @param P A file path
|
||||
-- @string P A file path
|
||||
function path.isfile(P)
|
||||
assert_string(1,P)
|
||||
return attrib(P,'mode') == 'file'
|
||||
end
|
||||
|
||||
-- is this a symbolic link?
|
||||
-- @param P A file path
|
||||
-- @string P A file path
|
||||
function path.islink(P)
|
||||
assert_string(1,P)
|
||||
if link_attrib then
|
||||
|
@ -67,14 +80,14 @@ function path.islink(P)
|
|||
end
|
||||
|
||||
--- return size of a file.
|
||||
-- @param P A file path
|
||||
-- @string P A file path
|
||||
function path.getsize(P)
|
||||
assert_string(1,P)
|
||||
return attrib(P,'size')
|
||||
end
|
||||
|
||||
--- does a path exist?.
|
||||
-- @param P A file path
|
||||
-- @string P A file path
|
||||
-- @return the file path if it exists, nil otherwise
|
||||
function path.exists(P)
|
||||
assert_string(1,P)
|
||||
|
@ -82,20 +95,20 @@ function path.exists(P)
|
|||
end
|
||||
|
||||
--- Return the time of last access as the number of seconds since the epoch.
|
||||
-- @param P A file path
|
||||
-- @string P A file path
|
||||
function path.getatime(P)
|
||||
assert_string(1,P)
|
||||
return attrib(P,'access')
|
||||
end
|
||||
|
||||
--- Return the time of last modification
|
||||
-- @param P A file path
|
||||
-- @string P A file path
|
||||
function path.getmtime(P)
|
||||
return attrib(P,'modification')
|
||||
end
|
||||
|
||||
---Return the system's ctime.
|
||||
-- @param P A file path
|
||||
-- @string P A file path
|
||||
function path.getctime(P)
|
||||
assert_string(1,P)
|
||||
return path.attrib(P,'change')
|
||||
|
@ -133,7 +146,7 @@ local sep,dirsep = path.sep,path.dirsep
|
|||
|
||||
--- given a path, return the directory part and a file part.
|
||||
-- if there's no directory part, the first value will be empty
|
||||
-- @param P A file path
|
||||
-- @string P A file path
|
||||
function path.splitpath(P)
|
||||
assert_string(1,P)
|
||||
local i = #P
|
||||
|
@ -150,8 +163,8 @@ function path.splitpath(P)
|
|||
end
|
||||
|
||||
--- return an absolute path.
|
||||
-- @param P A file path
|
||||
-- @param pwd optional start path to use (default is current dir)
|
||||
-- @string P A file path
|
||||
-- @string[opt] pwd optional start path to use (default is current dir)
|
||||
function path.abspath(P,pwd)
|
||||
assert_string(1,P)
|
||||
if pwd then assert_string(2,pwd) end
|
||||
|
@ -169,7 +182,9 @@ end
|
|||
|
||||
--- given a path, return the root part and the extension part.
|
||||
-- if there's no extension part, the second value will be empty
|
||||
-- @param P A file path
|
||||
-- @string P A file path
|
||||
-- @treturn string root part
|
||||
-- @treturn string extension part (maybe empty)
|
||||
function path.splitext(P)
|
||||
assert_string(1,P)
|
||||
local i = #P
|
||||
|
@ -189,7 +204,7 @@ function path.splitext(P)
|
|||
end
|
||||
|
||||
--- return the directory part of a path
|
||||
-- @param P A file path
|
||||
-- @string P A file path
|
||||
function path.dirname(P)
|
||||
assert_string(1,P)
|
||||
local p1,p2 = path.splitpath(P)
|
||||
|
@ -197,7 +212,7 @@ function path.dirname(P)
|
|||
end
|
||||
|
||||
--- return the file part of a path
|
||||
-- @param P A file path
|
||||
-- @string P A file path
|
||||
function path.basename(P)
|
||||
assert_string(1,P)
|
||||
local p1,p2 = path.splitpath(P)
|
||||
|
@ -205,7 +220,7 @@ function path.basename(P)
|
|||
end
|
||||
|
||||
--- get the extension part of a path.
|
||||
-- @param P A file path
|
||||
-- @string P A file path
|
||||
function path.extension(P)
|
||||
assert_string(1,P)
|
||||
local p1,p2 = path.splitext(P)
|
||||
|
@ -213,7 +228,7 @@ function path.extension(P)
|
|||
end
|
||||
|
||||
--- is this an absolute path?.
|
||||
-- @param P A file path
|
||||
-- @string P A file path
|
||||
function path.isabs(P)
|
||||
assert_string(1,P)
|
||||
if path.is_windows then
|
||||
|
@ -224,10 +239,11 @@ function path.isabs(P)
|
|||
end
|
||||
|
||||
--- return the path resulting from combining the individual paths.
|
||||
-- if the second path is absolute, we return that path.
|
||||
-- @param p1 A file path
|
||||
-- @param p2 A file path
|
||||
-- @param ... more file paths
|
||||
-- if the second (or later) path is absolute, we return the last absolute path (joined with any non-absolute paths following).
|
||||
-- empty elements (except the last) will be ignored.
|
||||
-- @string p1 A file path
|
||||
-- @string p2 A file path
|
||||
-- @string ... more file paths
|
||||
function path.join(p1,p2,...)
|
||||
assert_string(1,p1)
|
||||
assert_string(2,p2)
|
||||
|
@ -242,7 +258,7 @@ function path.join(p1,p2,...)
|
|||
end
|
||||
if path.isabs(p2) then return p2 end
|
||||
local endc = at(p1,#p1)
|
||||
if endc ~= path.sep and endc ~= other_sep then
|
||||
if endc ~= path.sep and endc ~= other_sep and endc ~= "" then
|
||||
p1 = p1..path.sep
|
||||
end
|
||||
return p1..p2
|
||||
|
@ -251,7 +267,7 @@ end
|
|||
--- normalize the case of a pathname. On Unix, this returns the path unchanged;
|
||||
-- for Windows, it converts the path to lowercase, and it also converts forward slashes
|
||||
-- to backward slashes.
|
||||
-- @param P A file path
|
||||
-- @string P A file path
|
||||
function path.normcase(P)
|
||||
assert_string(1,P)
|
||||
if path.is_windows then
|
||||
|
@ -261,12 +277,12 @@ function path.normcase(P)
|
|||
end
|
||||
end
|
||||
|
||||
local np_gen1,np_gen2 = '[^SEP]+SEP%.%.SEP?','SEP+%.?SEP'
|
||||
local np_gen1,np_gen2 = '([^SEP]+)SEP(%.%.SEP?)','SEP+%.?SEP'
|
||||
local np_pat1, np_pat2
|
||||
|
||||
--- normalize a path name.
|
||||
-- A//B, A/./B and A/foo/../B all become A/B.
|
||||
-- @param P a file path
|
||||
-- @string P a file path
|
||||
function path.normpath(P)
|
||||
assert_string(1,P)
|
||||
if path.is_windows then
|
||||
|
@ -284,8 +300,13 @@ function path.normpath(P)
|
|||
P,k = P:gsub(np_pat2,sep)
|
||||
until k == 0
|
||||
repeat -- A/../ -> (empty)
|
||||
P,k = P:gsub(np_pat1,'')
|
||||
until k == 0
|
||||
local oldP = P
|
||||
P,k = P:gsub(np_pat1,function(D, up)
|
||||
if D == '..' then return nil end
|
||||
if D == '.' then return up end
|
||||
return ''
|
||||
end)
|
||||
until k == 0 or oldP == P
|
||||
if P == '' then P = '.' end
|
||||
return P
|
||||
end
|
||||
|
@ -298,8 +319,8 @@ local function ATS (P)
|
|||
end
|
||||
|
||||
--- relative path from current directory or optional start point
|
||||
-- @param P a path
|
||||
-- @param start optional start point (default current directory)
|
||||
-- @string P a path
|
||||
-- @string[opt] start optional start point (default current directory)
|
||||
function path.relpath (P,start)
|
||||
assert_string(1,P)
|
||||
if start then assert_string(2,start) end
|
||||
|
@ -328,7 +349,7 @@ end
|
|||
--- Replace a starting '~' with the user's home directory.
|
||||
-- In windows, if HOME isn't set, then USERPROFILE is used in preference to
|
||||
-- HOMEDRIVE HOMEPATH. This is guaranteed to be writeable on all versions of Windows.
|
||||
-- @param P A file path
|
||||
-- @string P A file path
|
||||
function path.expanduser(P)
|
||||
assert_string(1,P)
|
||||
if at(P,1) == '~' then
|
||||
|
@ -344,16 +365,16 @@ end
|
|||
|
||||
|
||||
---Return a suitable full path to a new temporary file name.
|
||||
-- unlike os.tmpnam(), it always gives you a writeable path (uses %TMP% on Windows)
|
||||
-- unlike os.tmpnam(), it always gives you a writeable path (uses TEMP environment variable on Windows)
|
||||
function path.tmpname ()
|
||||
local res = tmpnam()
|
||||
if path.is_windows then res = getenv('TMP')..res end
|
||||
if path.is_windows then res = getenv('TEMP')..res end
|
||||
return res
|
||||
end
|
||||
|
||||
--- return the largest common prefix path of two paths.
|
||||
-- @param path1 a file path
|
||||
-- @param path2 a file path
|
||||
-- @string path1 a file path
|
||||
-- @string path2 a file path
|
||||
function path.common_prefix (path1,path2)
|
||||
assert_string(1,path1)
|
||||
assert_string(2,path2)
|
||||
|
@ -375,11 +396,10 @@ function path.common_prefix (path1,path2)
|
|||
--return ''
|
||||
end
|
||||
|
||||
|
||||
--- return the full path where a particular Lua module would be found.
|
||||
-- Both package.path and package.cpath is searched, so the result may
|
||||
-- either be a Lua file or a shared libarary.
|
||||
-- @param mod name of the module
|
||||
-- either be a Lua file or a shared library.
|
||||
-- @string mod name of the module
|
||||
-- @return on success: path of module, lua or binary
|
||||
-- @return on error: nil,error string
|
||||
function path.package_path(mod)
|
||||
|
|
|
@ -1,101 +0,0 @@
|
|||
-- experimental support for LuaJava
|
||||
--
|
||||
local path = {}
|
||||
|
||||
|
||||
path.link_attrib = nil
|
||||
|
||||
local File = luajava.bindClass("java.io.File")
|
||||
local Array = luajava.bindClass('java.lang.reflect.Array')
|
||||
|
||||
local function file(s)
|
||||
return luajava.new(File,s)
|
||||
end
|
||||
|
||||
function path.dir(P)
|
||||
local ls = file(P):list()
|
||||
print(ls)
|
||||
local idx,n = -1,Array:getLength(ls)
|
||||
return function ()
|
||||
idx = idx + 1
|
||||
if idx == n then return nil
|
||||
else
|
||||
return Array:get(ls,idx)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function path.mkdir(P)
|
||||
return file(P):mkdir()
|
||||
end
|
||||
|
||||
function path.rmdir(P)
|
||||
return file(P):delete()
|
||||
end
|
||||
|
||||
--- is this a directory?
|
||||
-- @param P A file path
|
||||
function path.isdir(P)
|
||||
if P:match("\\$") then
|
||||
P = P:sub(1,-2)
|
||||
end
|
||||
return file(P):isDirectory()
|
||||
end
|
||||
|
||||
--- is this a file?.
|
||||
-- @param P A file path
|
||||
function path.isfile(P)
|
||||
return file(P):isFile()
|
||||
end
|
||||
|
||||
-- is this a symbolic link?
|
||||
-- Direct support for symbolic links is not provided.
|
||||
-- see http://stackoverflow.com/questions/813710/java-1-6-determine-symbolic-links
|
||||
-- and the caveats therein.
|
||||
-- @param P A file path
|
||||
function path.islink(P)
|
||||
local f = file(P)
|
||||
local canon
|
||||
local parent = f:getParent()
|
||||
if not parent then
|
||||
canon = f
|
||||
else
|
||||
parent = f.getParentFile():getCanonicalFile()
|
||||
canon = luajava.new(File,parent,f:getName())
|
||||
end
|
||||
return canon:getCanonicalFile() ~= canon:getAbsoluteFile()
|
||||
end
|
||||
|
||||
--- return size of a file.
|
||||
-- @param P A file path
|
||||
function path.getsize(P)
|
||||
return file(P):length()
|
||||
end
|
||||
|
||||
--- does a path exist?.
|
||||
-- @param P A file path
|
||||
-- @return the file path if it exists, nil otherwise
|
||||
function path.exists(P)
|
||||
return file(P):exists() and P
|
||||
end
|
||||
|
||||
--- Return the time of last access as the number of seconds since the epoch.
|
||||
-- @param P A file path
|
||||
function path.getatime(P)
|
||||
return path.getmtime(P)
|
||||
end
|
||||
|
||||
--- Return the time of last modification
|
||||
-- @param P A file path
|
||||
function path.getmtime(P)
|
||||
-- Java time is no. of millisec since the epoch
|
||||
return file(P):lastModified()/1000
|
||||
end
|
||||
|
||||
---Return the system's ctime.
|
||||
-- @param P A file path
|
||||
function path.getctime(P)
|
||||
return path.getmtime(P)
|
||||
end
|
||||
|
||||
return path
|
|
@ -9,8 +9,25 @@ local append = table.insert
|
|||
local concat = table.concat
|
||||
local utils = require 'pl.utils'
|
||||
local lexer = require 'pl.lexer'
|
||||
local quote_string = require'pl.stringx'.quote_string
|
||||
local assert_arg = utils.assert_arg
|
||||
|
||||
--AAS
|
||||
--Perhaps this could be evolved into part of a "Compat5.3" library some day.
|
||||
--I didn't think that it was time for that, however.
|
||||
local tostring = tostring
|
||||
if _VERSION == "Lua 5.3" then
|
||||
local _tostring = tostring
|
||||
tostring = function(s)
|
||||
if type(s) == "number" then
|
||||
return ("%.f"):format(s)
|
||||
else
|
||||
return _tostring(s)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
local pretty = {}
|
||||
|
||||
local function save_string_index ()
|
||||
|
@ -35,15 +52,15 @@ end
|
|||
-- An empty environment is used, and
|
||||
-- any occurance of the keyword 'function' will be considered a problem.
|
||||
-- in the given environment - the return value may be `nil`.
|
||||
-- @param s {string} string of the form '{...}', with perhaps some whitespace
|
||||
-- before or after the curly braces.
|
||||
-- @string s string of the form '{...}', with perhaps some whitespace
|
||||
-- before or after the curly braces.
|
||||
-- @return a table
|
||||
function pretty.read(s)
|
||||
assert_arg(1,s,'string')
|
||||
if s:find '^%s*%-%-' then -- may start with a comment..
|
||||
s = s:gsub('%-%-.-\n','')
|
||||
end
|
||||
if not s:find '^%s*%b{}%s*$' then return nil,"not a Lua table" end
|
||||
if not s:find '^%s*{' then return nil,"not a Lua table" end
|
||||
if s:find '[^\'"%w_]function[^\'"%w_]' then
|
||||
local tok = lexer.lua(s)
|
||||
for t,v in tok do
|
||||
|
@ -65,9 +82,9 @@ function pretty.read(s)
|
|||
end
|
||||
|
||||
--- read a Lua chunk.
|
||||
-- @param s Lua code
|
||||
-- @string s Lua code
|
||||
-- @param env optional environment
|
||||
-- @param paranoid prevent any looping constructs and disable string methods
|
||||
-- @bool paranoid prevent any looping constructs and disable string methods
|
||||
-- @return the environment
|
||||
function pretty.load (s, env, paranoid)
|
||||
env = env or {}
|
||||
|
@ -93,7 +110,8 @@ end
|
|||
local function quote_if_necessary (v)
|
||||
if not v then return ''
|
||||
else
|
||||
if v:find ' ' then v = '"'..v..'"' end
|
||||
--AAS
|
||||
if v:find ' ' then v = quote_string(v) end
|
||||
end
|
||||
return v
|
||||
end
|
||||
|
@ -108,12 +126,17 @@ local function quote (s)
|
|||
if type(s) == 'table' then
|
||||
return pretty.write(s,'')
|
||||
else
|
||||
return ('%q'):format(tostring(s))
|
||||
--AAS
|
||||
return quote_string(s)-- ('%q'):format(tostring(s))
|
||||
end
|
||||
end
|
||||
|
||||
local function index (numkey,key)
|
||||
if not numkey then key = quote(key) end
|
||||
--AAS
|
||||
if not numkey then
|
||||
key = quote(key)
|
||||
key = key:find("^%[") and (" " .. key .. " ") or key
|
||||
end
|
||||
return '['..key..']'
|
||||
end
|
||||
|
||||
|
@ -123,17 +146,17 @@ end
|
|||
-- extra value. Normally puts out one item per line, using
|
||||
-- the provided indent; set the second parameter to '' if
|
||||
-- you want output on one line.
|
||||
-- @param tbl {table} Table to serialize to a string.
|
||||
-- @param space {string} (optional) The indent to use.
|
||||
-- Defaults to two spaces; make it the empty string for no indentation
|
||||
-- @param not_clever {bool} (optional) Use for plain output, e.g {['key']=1}.
|
||||
-- Defaults to false.
|
||||
-- @tab tbl Table to serialize to a string.
|
||||
-- @string space (optional) The indent to use.
|
||||
-- Defaults to two spaces; make it the empty string for no indentation
|
||||
-- @bool not_clever (optional) Use for plain output, e.g {['key']=1}.
|
||||
-- Defaults to false.
|
||||
-- @return a string
|
||||
-- @return a possible error message
|
||||
function pretty.write (tbl,space,not_clever)
|
||||
if type(tbl) ~= 'table' then
|
||||
local res = tostring(tbl)
|
||||
if type(tbl) == 'string' then res = '"'..res..'"' end
|
||||
if type(tbl) == 'string' then return quote(tbl) end
|
||||
return res, 'not a table'
|
||||
end
|
||||
if not keywords then
|
||||
|
@ -178,11 +201,13 @@ function pretty.write (tbl,space,not_clever)
|
|||
if tp ~= 'string' and tp ~= 'table' then
|
||||
putln(quote_if_necessary(tostring(t))..',')
|
||||
elseif tp == 'string' then
|
||||
if t:find('\n') then
|
||||
putln('[[\n'..t..']],')
|
||||
else
|
||||
putln(quote(t)..',')
|
||||
end
|
||||
-- if t:find('\n') then
|
||||
-- putln('[[\n'..t..']],')
|
||||
-- else
|
||||
-- putln(quote(t)..',')
|
||||
-- end
|
||||
--AAS
|
||||
putln(quote_string(t) ..",")
|
||||
elseif tp == 'table' then
|
||||
if tables[t] then
|
||||
putln('<cycle>,')
|
||||
|
@ -215,6 +240,7 @@ function pretty.write (tbl,space,not_clever)
|
|||
end
|
||||
end
|
||||
end
|
||||
tables[t] = nil
|
||||
eat_last_comma()
|
||||
putln(oldindent..'},')
|
||||
else
|
||||
|
@ -229,7 +255,7 @@ end
|
|||
--- Dump a Lua table out to a file or stdout.
|
||||
-- @param t {table} The table to write to a file or stdout.
|
||||
-- @param ... {string} (optional) File name to write too. Defaults to writing
|
||||
-- to stdout.
|
||||
-- to stdout.
|
||||
function pretty.dump (t,...)
|
||||
if select('#',...)==0 then
|
||||
print(pretty.write(t))
|
||||
|
@ -244,7 +270,9 @@ local memp,nump = {'B','KiB','MiB','GiB'},{'','K','M','B'}
|
|||
local comma
|
||||
function comma (val)
|
||||
local thou = math.floor(val/1000)
|
||||
if thou > 0 then return comma(thou)..','..(val % 1000)
|
||||
--AAS
|
||||
if thou > 0 then return comma(tostring(thou))..','.. tostring(val % 1000)
|
||||
-- if thou > 0 then return comma(thou)..','..(val % 1000)
|
||||
else return tostring(val) end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
--- Manipulating iterators as sequences.
|
||||
-- See @{07-functional.md.Sequences|The Guide}
|
||||
--
|
||||
-- Dependencies: `pl.utils`, `debug`
|
||||
-- Dependencies: `pl.utils`, `pl.types`, `debug`
|
||||
-- @module pl.seq
|
||||
|
||||
local next,assert,type,pairs,tonumber,type,setmetatable,getmetatable,_G = next,assert,type,pairs,tonumber,type,setmetatable,getmetatable,_G
|
||||
|
@ -10,6 +10,7 @@ local mrandom = math.random
|
|||
local remove,tsort,tappend = table.remove,table.sort,table.insert
|
||||
local io = io
|
||||
local utils = require 'pl.utils'
|
||||
local callable = require 'pl.types'.is_callable
|
||||
local function_arg = utils.function_arg
|
||||
local _List = utils.stdmt.List
|
||||
local _Map = utils.stdmt.Map
|
||||
|
@ -363,7 +364,7 @@ function seq.filter (iter,pred,arg)
|
|||
end
|
||||
|
||||
--- 'reduce' a sequence using a binary function.
|
||||
-- @param fun a function of two arguments
|
||||
-- @func fun a function of two arguments
|
||||
-- @param iter a sequence
|
||||
-- @param oldval optional initial value
|
||||
-- @usage seq.reduce(operator.add,seq.list{1,2,3,4}) == 10
|
||||
|
@ -465,7 +466,6 @@ end
|
|||
---------------------- Sequence Adapters ---------------------
|
||||
|
||||
local SMT
|
||||
local callable = utils.is_callable
|
||||
|
||||
local function SW (iter,...)
|
||||
if callable(iter) then
|
||||
|
|
|
@ -19,13 +19,10 @@
|
|||
--
|
||||
-- @module pl.sip
|
||||
|
||||
if not rawget(_G,'loadstring') then -- Lua 5.2 full compatibility
|
||||
loadstring = load
|
||||
unpack = table.unpack
|
||||
end
|
||||
local loadstring = rawget(_G,'loadstring') or load
|
||||
local unpack = rawget(_G,'unpack') or rawget(table,'unpack')
|
||||
|
||||
local append,concat = table.insert,table.concat
|
||||
local concat = table.concat
|
||||
local ipairs,loadstring,type,unpack = ipairs,loadstring,type,unpack
|
||||
local io,_G = io,_G
|
||||
local print,rawget = print,rawget
|
||||
|
@ -34,7 +31,8 @@ local patterns = {
|
|||
FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*',
|
||||
INTEGER = '[+%-%d]%d*',
|
||||
IDEN = '[%a_][%w_]*',
|
||||
FILE = '[%a%.\\][:%][%w%._%-\\]*'
|
||||
FILE = '[%a%.\\][:%][%w%._%-\\]*',
|
||||
OPTION = '[%a_][%w_%-]*',
|
||||
}
|
||||
|
||||
local function assert_arg(idx,val,tp)
|
||||
|
@ -92,6 +90,7 @@ local pattern_map = {
|
|||
v = group(patterns.IDEN),
|
||||
i = group(patterns.INTEGER),
|
||||
f = group(patterns.FLOAT),
|
||||
o = group(patterns.OPTION),
|
||||
r = '(%S.*)',
|
||||
p = '([%a]?[:]?[\\/%.%w_]+)'
|
||||
}
|
||||
|
@ -301,8 +300,8 @@ function sip.fields (spec,f)
|
|||
end
|
||||
|
||||
--- register a match which will be used in the read function.
|
||||
-- @param spec a SIP pattern
|
||||
-- @param fun a function to be called with the results of the match
|
||||
-- @string spec a SIP pattern
|
||||
-- @func fun a function to be called with the results of the match
|
||||
-- @see read
|
||||
function sip.pattern (spec,fun)
|
||||
assert_arg(1,spec,'string')
|
||||
|
|
|
@ -1,70 +1,123 @@
|
|||
--- Checks uses of undeclared global variables.
|
||||
-- All global variables must be 'declared' through a regular assignment
|
||||
-- (even assigning nil will do) in a main chunk before being used
|
||||
-- anywhere or assigned to inside a function.
|
||||
-- (even assigning `nil` will do) in a main chunk before being used
|
||||
-- anywhere or assigned to inside a function. Existing metatables `__newindex` and `__index`
|
||||
-- metamethods are respected.
|
||||
--
|
||||
-- You can set any table to have strict behaviour using `strict.module`. Creating a new
|
||||
-- module with `strict.closed_module` makes the module immune to monkey-patching, if
|
||||
-- you don't wish to encourage monkey business.
|
||||
--
|
||||
-- If the global `PENLIGHT_NO_GLOBAL_STRICT` is defined, then this module won't make the
|
||||
-- global environment strict - if you just want to explicitly set table strictness.
|
||||
--
|
||||
-- @module pl.strict
|
||||
|
||||
require 'debug'
|
||||
require 'debug' -- for Lua 5.2
|
||||
local getinfo, error, rawset, rawget = debug.getinfo, error, rawset, rawget
|
||||
local handler,hooked
|
||||
|
||||
local mt = getmetatable(_G)
|
||||
if mt == nil then
|
||||
mt = {}
|
||||
setmetatable(_G, mt)
|
||||
elseif mt.hook then
|
||||
hooked = true
|
||||
end
|
||||
|
||||
-- predeclaring _PROMPT keeps the Lua Interpreter happy
|
||||
mt.__declared = {_PROMPT=true}
|
||||
local strict = {}
|
||||
|
||||
local function what ()
|
||||
local d = getinfo(3, "S")
|
||||
return d and d.what or "C"
|
||||
local d = getinfo(3, "S")
|
||||
return d and d.what or "C"
|
||||
end
|
||||
|
||||
mt.__newindex = function (t, n, v)
|
||||
if not mt.__declared[n] then
|
||||
local w = what()
|
||||
if w ~= "main" and w ~= "C" then
|
||||
error("assign to undeclared variable '"..n.."'", 2)
|
||||
--- make an existing table strict.
|
||||
-- @string name name of table (optional)
|
||||
-- @tab[opt] mod table - if `nil` then we'll return a new table
|
||||
-- @tab[opt] predeclared - table of variables that are to be considered predeclared.
|
||||
-- @return the given table, or a new table
|
||||
function strict.module (name,mod,predeclared)
|
||||
local mt, old_newindex, old_index, old_index_type, global, closed
|
||||
if predeclared then
|
||||
global = predeclared.__global
|
||||
closed = predeclared.__closed
|
||||
end
|
||||
if type(mod) == 'table' then
|
||||
mt = getmetatable(mod)
|
||||
if mt and rawget(mt,'__declared') then return end -- already patched...
|
||||
else
|
||||
mod = {}
|
||||
end
|
||||
mt.__declared[n] = true
|
||||
end
|
||||
rawset(t, n, v)
|
||||
end
|
||||
|
||||
handler = function(t,n)
|
||||
if not mt.__declared[n] and what() ~= "C" then
|
||||
error("variable '"..n.."' is not declared", 2)
|
||||
end
|
||||
return rawget(t, n)
|
||||
end
|
||||
|
||||
function package.strict (mod)
|
||||
local mt = getmetatable(mod)
|
||||
if mt == nil then
|
||||
mt = {}
|
||||
setmetatable(mod, mt)
|
||||
mt = {}
|
||||
setmetatable(mod, mt)
|
||||
else
|
||||
old_newindex = mt.__newindex
|
||||
old_index = mt.__index
|
||||
old_index_type = type(old_index)
|
||||
end
|
||||
mt.__declared = {}
|
||||
mt.__declared = predeclared or {}
|
||||
mt.__newindex = function(t, n, v)
|
||||
mt.__declared[n] = true
|
||||
if old_newindex then
|
||||
old_newindex(t, n, v)
|
||||
if rawget(t,n)~=nil then return end
|
||||
end
|
||||
if not mt.__declared[n] then
|
||||
if global then
|
||||
local w = what()
|
||||
if w ~= "main" and w ~= "C" then
|
||||
error("assign to undeclared global '"..n.."'", 2)
|
||||
end
|
||||
end
|
||||
mt.__declared[n] = true
|
||||
end
|
||||
rawset(t, n, v)
|
||||
end
|
||||
mt.__index = function(t,n)
|
||||
if not mt.__declared[n] then
|
||||
error("variable '"..n.."' is not declared", 2)
|
||||
end
|
||||
return rawget(t, n)
|
||||
if not mt.__declared[n] and what() ~= "C" then
|
||||
if old_index then
|
||||
if old_index_type == "table" then
|
||||
local fallback = old_index[n]
|
||||
if fallback ~= nil then
|
||||
return fallback
|
||||
end
|
||||
else
|
||||
local res = old_index(t, n)
|
||||
if res then return res end
|
||||
end
|
||||
end
|
||||
local msg = "variable '"..n.."' is not declared"
|
||||
if name then
|
||||
msg = msg .. " in '"..name.."'"
|
||||
end
|
||||
error(msg, 2)
|
||||
end
|
||||
return rawget(t, n)
|
||||
end
|
||||
return mod
|
||||
end
|
||||
|
||||
--- make all tables in a table strict.
|
||||
-- So `strict.make_all_strict(_G)` prevents monkey-patching
|
||||
-- of any global table
|
||||
-- @tab T
|
||||
function strict.make_all_strict (T)
|
||||
for k,v in pairs(T) do
|
||||
if type(v) == 'table' and v ~= T then
|
||||
strict.module(k,v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not hooked then
|
||||
mt.__index = handler
|
||||
else
|
||||
mt.hook(handler)
|
||||
--- make a new module table which is closed to further changes.
|
||||
function strict.closed_module (mod,name)
|
||||
local M = {}
|
||||
mod = mod or {}
|
||||
local mt = getmetatable(mod)
|
||||
if not mt then
|
||||
mt = {}
|
||||
setmetatable(mod,mt)
|
||||
end
|
||||
mt.__newindex = function(t,k,v)
|
||||
M[k] = v
|
||||
end
|
||||
return strict.module(name,M)
|
||||
end
|
||||
|
||||
if not rawget(_G,'PENLIGHT_NO_GLOBAL_STRICT') then
|
||||
strict.module(nil,_G,{_PROMPT=true,__global=true})
|
||||
end
|
||||
|
||||
return strict
|
||||
|
||||
|
|
|
@ -12,16 +12,13 @@
|
|||
-- See @{03-strings.md.File_style_I_O_on_Strings|the Guide}.
|
||||
-- @module pl.stringio
|
||||
|
||||
if not rawget(_G,'loadstring') then -- Lua 5.2 full compatibility
|
||||
unpack = table.unpack
|
||||
end
|
||||
|
||||
local getmetatable,tostring,unpack,tonumber = getmetatable,tostring,unpack,tonumber
|
||||
local unpack = rawget(_G,'unpack') or rawget(table,'unpack')
|
||||
local getmetatable,tostring,tonumber = getmetatable,tostring,tonumber
|
||||
local concat,append = table.concat,table.insert
|
||||
|
||||
local stringio = {}
|
||||
|
||||
--- Writer class
|
||||
-- Writer class
|
||||
local SW = {}
|
||||
SW.__index = SW
|
||||
|
||||
|
@ -58,7 +55,7 @@ end
|
|||
function SW:seek()
|
||||
end
|
||||
|
||||
--- Reader class
|
||||
-- Reader class
|
||||
local SR = {}
|
||||
SR.__index = SR
|
||||
|
||||
|
@ -138,15 +135,18 @@ function SR:close() -- for compatibility only
|
|||
end
|
||||
|
||||
--- create a file-like object which can be used to construct a string.
|
||||
-- The resulting object has an extra <code>value()</code> method for
|
||||
-- retrieving the string value.
|
||||
-- The resulting object has an extra `value()` method for
|
||||
-- retrieving the string value. Implements `file:write`, `file:seek`, `file:lines`,
|
||||
-- plus an extra `writef` method which works like `utils.printf`.
|
||||
-- @usage f = create(); f:write('hello, dolly\n'); print(f:value())
|
||||
function stringio.create()
|
||||
return setmetatable({tbl={}},SW)
|
||||
end
|
||||
|
||||
--- create a file-like object for reading from a given string.
|
||||
-- @param s The input string.
|
||||
-- Implements `file:read`.
|
||||
-- @string s The input string.
|
||||
-- @usage fs = open '20 10'; x,y = f:read ('*n','*n'); assert(x == 20 and y == 10)
|
||||
function stringio.open(s)
|
||||
return setmetatable({str=s,i=1},SR)
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
local utils = require 'pl.utils'
|
||||
local string = string
|
||||
local find = string.find
|
||||
local type,setmetatable,getmetatable,ipairs,unpack = type,setmetatable,getmetatable,ipairs,unpack
|
||||
local type,setmetatable,getmetatable,ipairs,unpack = type,setmetatable,getmetatable,ipairs,utils.unpack
|
||||
local error,tostring = error,tostring
|
||||
local gsub = string.gsub
|
||||
local rep = string.rep
|
||||
|
@ -37,60 +37,55 @@ end
|
|||
|
||||
local stringx = {}
|
||||
|
||||
------------------
|
||||
-- String Predicates
|
||||
-- @section predicates
|
||||
|
||||
--- does s only contain alphabetic characters?.
|
||||
-- @param s a string
|
||||
-- @string s a string
|
||||
function stringx.isalpha(s)
|
||||
assert_string(1,s)
|
||||
return find(s,'^%a+$') == 1
|
||||
end
|
||||
|
||||
--- does s only contain digits?.
|
||||
-- @param s a string
|
||||
-- @string s a string
|
||||
function stringx.isdigit(s)
|
||||
assert_string(1,s)
|
||||
return find(s,'^%d+$') == 1
|
||||
end
|
||||
|
||||
--- does s only contain alphanumeric characters?.
|
||||
-- @param s a string
|
||||
-- @string s a string
|
||||
function stringx.isalnum(s)
|
||||
assert_string(1,s)
|
||||
return find(s,'^%w+$') == 1
|
||||
end
|
||||
|
||||
--- does s only contain spaces?.
|
||||
-- @param s a string
|
||||
-- @string s a string
|
||||
function stringx.isspace(s)
|
||||
assert_string(1,s)
|
||||
return find(s,'^%s+$') == 1
|
||||
end
|
||||
|
||||
--- does s only contain lower case characters?.
|
||||
-- @param s a string
|
||||
-- @string s a string
|
||||
function stringx.islower(s)
|
||||
assert_string(1,s)
|
||||
return find(s,'^[%l%s]+$') == 1
|
||||
end
|
||||
|
||||
--- does s only contain upper case characters?.
|
||||
-- @param s a string
|
||||
-- @string s a string
|
||||
function stringx.isupper(s)
|
||||
assert_string(1,s)
|
||||
return find(s,'^[%u%s]+$') == 1
|
||||
end
|
||||
|
||||
--- concatenate the strings using this string as a delimiter.
|
||||
-- @param self the string
|
||||
-- @param seq a table of strings or numbers
|
||||
-- @usage (' '):join {1,2,3} == '1 2 3'
|
||||
function stringx.join (self,seq)
|
||||
assert_string(1,self)
|
||||
return concat(seq,self)
|
||||
end
|
||||
|
||||
--- does string start with the substring?.
|
||||
-- @param self the string
|
||||
-- @param s2 a string
|
||||
-- @string self the string
|
||||
-- @string s2 a string
|
||||
function stringx.startswith(self,s2)
|
||||
assert_string(1,self)
|
||||
assert_string(2,s2)
|
||||
|
@ -103,16 +98,16 @@ local function _find_all(s,sub,first,last)
|
|||
local res
|
||||
local k = 0
|
||||
while i1 do
|
||||
if last and i1 > last then break end
|
||||
res = i1
|
||||
k = k + 1
|
||||
i1,i2 = find(s,sub,i2+1,true)
|
||||
if last and i1 > last then break end
|
||||
end
|
||||
return res,k
|
||||
end
|
||||
|
||||
--- does string end with the given substring?.
|
||||
-- @param s a string
|
||||
-- @string s a string
|
||||
-- @param send a substring or a table of suffixes
|
||||
function stringx.endswith(s,send)
|
||||
assert_string(1,s)
|
||||
|
@ -129,8 +124,20 @@ function stringx.endswith(s,send)
|
|||
end
|
||||
end
|
||||
|
||||
-- break string into a list of lines
|
||||
-- @param self the string
|
||||
--- Strings and Lists
|
||||
-- @section lists
|
||||
|
||||
--- concatenate the strings using this string as a delimiter.
|
||||
-- @string self the string
|
||||
-- @param seq a table of strings or numbers
|
||||
-- @usage (' '):join {1,2,3} == '1 2 3'
|
||||
function stringx.join (self,seq)
|
||||
assert_string(1,self)
|
||||
return concat(seq,self)
|
||||
end
|
||||
|
||||
--- break string into a list of lines
|
||||
-- @string self the string
|
||||
-- @param keepends (currently not used)
|
||||
function stringx.splitlines (self,keepends)
|
||||
assert_string(1,self)
|
||||
|
@ -140,72 +147,11 @@ function stringx.splitlines (self,keepends)
|
|||
return setmetatable(res,list_MT)
|
||||
end
|
||||
|
||||
local function tab_expand (self,n)
|
||||
return (gsub(self,'([^\t]*)\t', function(s)
|
||||
return s..(' '):rep(n - #s % n)
|
||||
end))
|
||||
end
|
||||
|
||||
--- replace all tabs in s with n spaces. If not specified, n defaults to 8.
|
||||
-- with 0.9.5 this now correctly expands to the next tab stop (if you really
|
||||
-- want to just replace tabs, use :gsub('\t',' ') etc)
|
||||
-- @param self the string
|
||||
-- @param n number of spaces to expand each tab, (default 8)
|
||||
function stringx.expandtabs(self,n)
|
||||
assert_string(1,self)
|
||||
n = n or 8
|
||||
if not self:find '\n' then return tab_expand(self,n) end
|
||||
local res,i = {},1
|
||||
for line in stringx.lines(self) do
|
||||
res[i] = tab_expand(line,n)
|
||||
i = i + 1
|
||||
end
|
||||
return table.concat(res,'\n')
|
||||
end
|
||||
|
||||
--- find index of first instance of sub in s from the left.
|
||||
-- @param self the string
|
||||
-- @param sub substring
|
||||
-- @param i1 start index
|
||||
function stringx.lfind(self,sub,i1)
|
||||
assert_string(1,self)
|
||||
assert_string(2,sub)
|
||||
local idx = find(self,sub,i1,true)
|
||||
if idx then return idx else return nil end
|
||||
end
|
||||
|
||||
--- find index of first instance of sub in s from the right.
|
||||
-- @param self the string
|
||||
-- @param sub substring
|
||||
-- @param first first index
|
||||
-- @param last last index
|
||||
function stringx.rfind(self,sub,first,last)
|
||||
assert_string(1,self)
|
||||
assert_string(2,sub)
|
||||
local idx = _find_all(self,sub,first,last)
|
||||
if idx then return idx else return nil end
|
||||
end
|
||||
|
||||
--- replace up to n instances of old by new in the string s.
|
||||
-- if n is not present, replace all instances.
|
||||
-- @param s the string
|
||||
-- @param old the target substring
|
||||
-- @param new the substitution
|
||||
-- @param n optional maximum number of substitutions
|
||||
-- @return result string
|
||||
-- @return the number of substitutions
|
||||
function stringx.replace(s,old,new,n)
|
||||
assert_string(1,s)
|
||||
assert_string(1,old)
|
||||
return (gsub(s,escape(old),new:gsub('%%','%%%%'),n))
|
||||
end
|
||||
|
||||
--- split a string into a list of strings using a delimiter.
|
||||
-- @class function
|
||||
-- @name split
|
||||
-- @param self the string
|
||||
-- @param re a delimiter (defaults to whitespace)
|
||||
-- @param n maximum number of results
|
||||
-- @function split
|
||||
-- @string self the string
|
||||
-- @string[opt] re a delimiter (defaults to whitespace)
|
||||
-- @int n maximum number of results
|
||||
-- @usage #(('one two'):split()) == 2
|
||||
-- @usage ('one,two,three'):split(',') == List{'one','two','three'}
|
||||
-- @usage ('one,two,three'):split(',',2) == List{'one','two,three'}
|
||||
|
@ -223,14 +169,67 @@ function stringx.split(self,re,n)
|
|||
return setmetatable(res,list_MT)
|
||||
end
|
||||
|
||||
--- split a string using a pattern. Note that at least one value will be returned!
|
||||
-- @param self the string
|
||||
-- @param re a Lua string pattern (defaults to whitespace)
|
||||
-- @return the parts of the string
|
||||
-- @usage a,b = line:splitv('=')
|
||||
function stringx.splitv (self,re)
|
||||
local function tab_expand (self,n)
|
||||
return (gsub(self,'([^\t]*)\t', function(s)
|
||||
return s..(' '):rep(n - #s % n)
|
||||
end))
|
||||
end
|
||||
|
||||
--- replace all tabs in s with n spaces. If not specified, n defaults to 8.
|
||||
-- with 0.9.5 this now correctly expands to the next tab stop (if you really
|
||||
-- want to just replace tabs, use :gsub('\t',' ') etc)
|
||||
-- @string self the string
|
||||
-- @int n number of spaces to expand each tab, (default 8)
|
||||
function stringx.expandtabs(self,n)
|
||||
assert_string(1,self)
|
||||
return utils.splitv(self,re)
|
||||
n = n or 8
|
||||
if not self:find '\n' then return tab_expand(self,n) end
|
||||
local res,i = {},1
|
||||
for line in stringx.lines(self) do
|
||||
res[i] = tab_expand(line,n)
|
||||
i = i + 1
|
||||
end
|
||||
return table.concat(res,'\n')
|
||||
end
|
||||
|
||||
--- Finding and Replacing
|
||||
-- @section find
|
||||
|
||||
--- find index of first instance of sub in s from the left.
|
||||
-- @string self the string
|
||||
-- @string sub substring
|
||||
-- @int i1 start index
|
||||
function stringx.lfind(self,sub,i1)
|
||||
assert_string(1,self)
|
||||
assert_string(2,sub)
|
||||
local idx = find(self,sub,i1,true)
|
||||
if idx then return idx else return nil end
|
||||
end
|
||||
|
||||
--- find index of first instance of sub in s from the right.
|
||||
-- @string self the string
|
||||
-- @string sub substring
|
||||
-- @int first first index
|
||||
-- @int last last index
|
||||
function stringx.rfind(self,sub,first,last)
|
||||
assert_string(1,self)
|
||||
assert_string(2,sub)
|
||||
local idx = _find_all(self,sub,first,last)
|
||||
if idx then return idx else return nil end
|
||||
end
|
||||
|
||||
--- replace up to n instances of old by new in the string s.
|
||||
-- if n is not present, replace all instances.
|
||||
-- @string s the string
|
||||
-- @string old the target substring
|
||||
-- @string new the substitution
|
||||
-- @int[opt] n optional maximum number of substitutions
|
||||
-- @return result string
|
||||
-- @return the number of substitutions
|
||||
function stringx.replace(s,old,new,n)
|
||||
assert_string(1,s)
|
||||
assert_string(1,old)
|
||||
return (gsub(s,escape(old),new:gsub('%%','%%%%'),n))
|
||||
end
|
||||
|
||||
local function copy(self)
|
||||
|
@ -238,14 +237,17 @@ local function copy(self)
|
|||
end
|
||||
|
||||
--- count all instances of substring in string.
|
||||
-- @param self the string
|
||||
-- @param sub substring
|
||||
-- @string self the string
|
||||
-- @string sub substring
|
||||
function stringx.count(self,sub)
|
||||
assert_string(1,self)
|
||||
local i,k = _find_all(self,sub,1)
|
||||
return k
|
||||
end
|
||||
|
||||
--- Stripping and Justifying
|
||||
-- @section strip
|
||||
|
||||
local function _just(s,w,ch,left,right)
|
||||
local n = #s
|
||||
if w > n then
|
||||
|
@ -270,9 +272,9 @@ local function _just(s,w,ch,left,right)
|
|||
end
|
||||
|
||||
--- left-justify s with width w.
|
||||
-- @param self the string
|
||||
-- @param w width of justification
|
||||
-- @param ch padding character, default ' '
|
||||
-- @string self the string
|
||||
-- @int w width of justification
|
||||
-- @string[opt=''] ch padding character
|
||||
function stringx.ljust(self,w,ch)
|
||||
assert_string(1,self)
|
||||
assert_arg(2,w,'number')
|
||||
|
@ -280,9 +282,9 @@ function stringx.ljust(self,w,ch)
|
|||
end
|
||||
|
||||
--- right-justify s with width w.
|
||||
-- @param s the string
|
||||
-- @param w width of justification
|
||||
-- @param ch padding character, default ' '
|
||||
-- @string s the string
|
||||
-- @int w width of justification
|
||||
-- @string[opt=''] ch padding character
|
||||
function stringx.rjust(s,w,ch)
|
||||
assert_string(1,s)
|
||||
assert_arg(2,w,'number')
|
||||
|
@ -290,9 +292,9 @@ function stringx.rjust(s,w,ch)
|
|||
end
|
||||
|
||||
--- center-justify s with width w.
|
||||
-- @param s the string
|
||||
-- @param w width of justification
|
||||
-- @param ch padding character, default ' '
|
||||
-- @string s the string
|
||||
-- @int w width of justification
|
||||
-- @string[opt=''] ch padding character
|
||||
function stringx.center(s,w,ch)
|
||||
assert_string(1,s)
|
||||
assert_arg(2,w,'number')
|
||||
|
@ -321,8 +323,9 @@ local function _strip(s,left,right,chrs)
|
|||
end
|
||||
|
||||
--- trim any whitespace on the left of s.
|
||||
-- @param self the string
|
||||
-- @param chrs default space, can be a string of characters to be trimmed
|
||||
-- @string self the string
|
||||
-- @string[opt='%x'] chrs default any whitespace character,
|
||||
-- but can be a string of characters to be trimmed
|
||||
function stringx.lstrip(self,chrs)
|
||||
assert_string(1,self)
|
||||
return _strip(self,true,false,chrs)
|
||||
|
@ -330,21 +333,36 @@ end
|
|||
lstrip = stringx.lstrip
|
||||
|
||||
--- trim any whitespace on the right of s.
|
||||
-- @param s the string
|
||||
-- @param chrs default space, can be a string of characters to be trimmed
|
||||
-- @string s the string
|
||||
-- @string[opt='%x'] chrs default any whitespace character,
|
||||
-- but can be a string of characters to be trimmed
|
||||
function stringx.rstrip(s,chrs)
|
||||
assert_string(1,s)
|
||||
return _strip(s,false,true,chrs)
|
||||
end
|
||||
|
||||
--- trim any whitespace on both left and right of s.
|
||||
-- @param self the string
|
||||
-- @param chrs default space, can be a string of characters to be trimmed
|
||||
-- @string self the string
|
||||
-- @string[opt='%x'] chrs default any whitespace character,
|
||||
-- but can be a string of characters to be trimmed
|
||||
function stringx.strip(self,chrs)
|
||||
assert_string(1,self)
|
||||
return _strip(self,true,true,chrs)
|
||||
end
|
||||
|
||||
--- Partioning Strings
|
||||
-- @section partioning
|
||||
|
||||
--- split a string using a pattern. Note that at least one value will be returned!
|
||||
-- @string self the string
|
||||
-- @string[opt='%s'] re a Lua string pattern (defaults to whitespace)
|
||||
-- @return the parts of the string
|
||||
-- @usage a,b = line:splitv('=')
|
||||
function stringx.splitv (self,re)
|
||||
assert_string(1,self)
|
||||
return utils.splitv(self,re)
|
||||
end
|
||||
|
||||
-- The partition functions split a string using a delimiter into three parts:
|
||||
-- the part before, the delimiter itself, and the part afterwards
|
||||
local function _partition(p,delim,fn)
|
||||
|
@ -358,8 +376,8 @@ local function _partition(p,delim,fn)
|
|||
end
|
||||
|
||||
--- partition the string using first occurance of a delimiter
|
||||
-- @param self the string
|
||||
-- @param ch delimiter
|
||||
-- @string self the string
|
||||
-- @string ch delimiter
|
||||
-- @return part before ch
|
||||
-- @return ch
|
||||
-- @return part after ch
|
||||
|
@ -370,8 +388,8 @@ function stringx.partition(self,ch)
|
|||
end
|
||||
|
||||
--- partition the string p using last occurance of a delimiter
|
||||
-- @param self the string
|
||||
-- @param ch delimiter
|
||||
-- @string self the string
|
||||
-- @string ch delimiter
|
||||
-- @return part before ch
|
||||
-- @return ch
|
||||
-- @return part after ch
|
||||
|
@ -382,8 +400,8 @@ function stringx.rpartition(self,ch)
|
|||
end
|
||||
|
||||
--- return the 'character' at the index.
|
||||
-- @param self the string
|
||||
-- @param idx an index (can be negative)
|
||||
-- @string self the string
|
||||
-- @int idx an index (can be negative)
|
||||
-- @return a substring of length 1 if successful, empty string otherwise.
|
||||
function stringx.at(self,idx)
|
||||
assert_string(1,self)
|
||||
|
@ -391,8 +409,11 @@ function stringx.at(self,idx)
|
|||
return sub(self,idx,idx)
|
||||
end
|
||||
|
||||
--- Miscelaneous
|
||||
-- @section misc
|
||||
|
||||
--- return an interator over all lines in a string
|
||||
-- @param self the string
|
||||
-- @string self the string
|
||||
-- @return an iterator
|
||||
function stringx.lines (self)
|
||||
assert_string(1,self)
|
||||
|
@ -403,7 +424,7 @@ end
|
|||
|
||||
--- iniital word letters uppercase ('title case').
|
||||
-- Here 'words' mean chunks of non-space characters.
|
||||
-- @param self the string
|
||||
-- @string self the string
|
||||
-- @return a string with each word's first letter uppercase
|
||||
function stringx.title(self)
|
||||
return (self:gsub('(%S)(%S*)',function(f,r)
|
||||
|
@ -417,9 +438,9 @@ local elipsis = '...'
|
|||
local n_elipsis = #elipsis
|
||||
|
||||
--- return a shorted version of a string.
|
||||
-- @param self the string
|
||||
-- @param sz the maxinum size allowed
|
||||
-- @param tail true if we want to show the end of the string (head otherwise)
|
||||
-- @string self the string
|
||||
-- @int sz the maxinum size allowed
|
||||
-- @bool tail true if we want to show the end of the string (head otherwise)
|
||||
function stringx.shorten(self,sz,tail)
|
||||
if #self > sz then
|
||||
if sz < n_elipsis then return elipsis:sub(1,sz) end
|
||||
|
@ -433,6 +454,54 @@ function stringx.shorten(self,sz,tail)
|
|||
return self
|
||||
end
|
||||
|
||||
--- Utility function that finds any patterns that match a long string's an open or close.
|
||||
-- Note that having this function use the least number of equal signs that is possible is a harder algorithm to come up with.
|
||||
-- Right now, it simply returns the greatest number of them found.
|
||||
-- @param s The string
|
||||
-- @return 'nil' if not found. If found, the maximum number of equal signs found within all matches.
|
||||
local function has_lquote(s)
|
||||
local lstring_pat = '([%[%]])(=*)%1'
|
||||
local start, finish, bracket, equals, next_equals = nil, 0, nil, nil, nil
|
||||
-- print("checking lquote for", s)
|
||||
repeat
|
||||
start, finish, bracket, next_equals = s:find(lstring_pat, finish + 1)
|
||||
if start then
|
||||
-- print("found start", start, finish, bracket, next_equals)
|
||||
--length of captured =. Ex: [==[ is 2, ]] is 0.
|
||||
next_equals = #next_equals
|
||||
equals = next_equals >= (equals or 0) and next_equals or equals
|
||||
end
|
||||
until not start
|
||||
--next_equals will be nil if there was no match.
|
||||
return equals
|
||||
end
|
||||
|
||||
--- Quote the given string and preserve any control or escape characters, such that reloading the string in Lua returns the same result.
|
||||
-- @param s The string to be quoted.
|
||||
-- @return The quoted string.
|
||||
function stringx.quote_string(s)
|
||||
--find out if there are any embedded long-quote
|
||||
--sequences that may cause issues.
|
||||
--This is important when strings are embedded within strings, like when serializing.
|
||||
local equal_signs = has_lquote(s)
|
||||
if s:find("\n") or equal_signs then
|
||||
-- print("going with long string:", s)
|
||||
equal_signs = ("="):rep((equal_signs or -1) + 1)
|
||||
--long strings strip out leading \n. We want to retain that, when quoting.
|
||||
if s:find("^\n") then s = "\n" .. s end
|
||||
--if there is an embedded sequence that matches a long quote, then
|
||||
--find the one with the maximum number of = signs and add one to that number
|
||||
local lbracket, rbracket =
|
||||
"[" .. equal_signs .. "[",
|
||||
"]" .. equal_signs .. "]"
|
||||
s = lbracket .. s .. rbracket
|
||||
else
|
||||
--Escape funny stuff.
|
||||
s = ("%q"):format(s)
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
function stringx.import(dont_overload)
|
||||
utils.import(stringx,string)
|
||||
end
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
--
|
||||
-- See @{02-arrays.md.Useful_Operations_on_Tables|the Guide}
|
||||
--
|
||||
-- Dependencies: `pl.utils`
|
||||
-- Dependencies: `pl.utils`, `pl.types`
|
||||
-- @module pl.tablex
|
||||
local utils = require ('pl.utils')
|
||||
local types = require ('pl.types')
|
||||
local getmetatable,setmetatable,require = getmetatable,setmetatable,require
|
||||
local append,remove = table.insert,table.remove
|
||||
local tsort,append,remove = table.sort,table.insert,table.remove
|
||||
local min,max = math.min,math.max
|
||||
local pairs,type,unpack,next,select,tostring = pairs,type,unpack,next,select,tostring
|
||||
local pairs,type,unpack,next,select,tostring = pairs,type,utils.unpack,next,select,tostring
|
||||
local function_arg = utils.function_arg
|
||||
local Set = utils.stdmt.Set
|
||||
local List = utils.stdmt.List
|
||||
|
@ -29,43 +30,32 @@ local function makelist (res)
|
|||
return setmetatable(res,List)
|
||||
end
|
||||
|
||||
local function check_meta (val)
|
||||
if type(val) == 'table' then return true end
|
||||
return getmetatable(val)
|
||||
end
|
||||
|
||||
local function complain (idx,msg)
|
||||
error(('argument %d is not %s'):format(idx,msg),3)
|
||||
end
|
||||
|
||||
local function assert_arg_indexable (idx,val)
|
||||
local mt = check_meta(val)
|
||||
if mt == true then return end
|
||||
if not(mt and mt.__len and mt.__index) then
|
||||
if not types.is_indexable(val) then
|
||||
complain(idx,"indexable")
|
||||
end
|
||||
end
|
||||
|
||||
local function assert_arg_iterable (idx,val)
|
||||
local mt = check_meta(val)
|
||||
if mt == true then return end
|
||||
if not(mt and mt.__pairs) then
|
||||
if not types.is_iterable(val) then
|
||||
complain(idx,"iterable")
|
||||
end
|
||||
end
|
||||
|
||||
local function assert_arg_writeable (idx,val)
|
||||
local mt = check_meta(val)
|
||||
if mt == true then return end
|
||||
if not(mt and mt.__newindex) then
|
||||
if not types.is_writeable(val) then
|
||||
complain(idx,"writeable")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- copy a table into another, in-place.
|
||||
-- @param t1 destination table
|
||||
-- @param t2 source (any iterable object)
|
||||
-- @within Copying
|
||||
-- @tab t1 destination table
|
||||
-- @tab t2 source (actually any iterable object)
|
||||
-- @return first table
|
||||
function tablex.update (t1,t2)
|
||||
assert_arg_writeable(1,t1)
|
||||
|
@ -82,7 +72,7 @@ end
|
|||
-- be greater or equal. The difference gives the size of
|
||||
-- the hash part, for practical purposes. Works for any
|
||||
-- object with a __pairs metamethod.
|
||||
-- @param t a table
|
||||
-- @tab t a table
|
||||
-- @return the size
|
||||
function tablex.size (t)
|
||||
assert_arg_iterable(1,t)
|
||||
|
@ -92,7 +82,8 @@ function tablex.size (t)
|
|||
end
|
||||
|
||||
--- make a shallow copy of a table
|
||||
-- @param t an iterable source
|
||||
-- @within Copying
|
||||
-- @tab t an iterable source
|
||||
-- @return new table
|
||||
function tablex.copy (t)
|
||||
assert_arg_iterable(1,t)
|
||||
|
@ -105,7 +96,8 @@ end
|
|||
|
||||
--- make a deep copy of a table, recursively copying all the keys and fields.
|
||||
-- This will also set the copied table's metatable to that of the original.
|
||||
-- @param t A table
|
||||
-- @within Copying
|
||||
-- @tab t A table
|
||||
-- @return new table
|
||||
function tablex.deepcopy(t)
|
||||
if type(t) ~= 'table' then return t end
|
||||
|
@ -126,10 +118,11 @@ local abs, deepcompare = math.abs
|
|||
|
||||
--- compare two values.
|
||||
-- if they are tables, then compare their keys and fields recursively.
|
||||
-- @within Comparing
|
||||
-- @param t1 A value
|
||||
-- @param t2 A value
|
||||
-- @param ignore_mt if true, ignore __eq metamethod (default false)
|
||||
-- @param eps if defined, then used for any number comparisons
|
||||
-- @bool[opt] ignore_mt if true, ignore __eq metamethod (default false)
|
||||
-- @number[opt] eps if defined, then used for any number comparisons
|
||||
-- @return true or false
|
||||
function tablex.deepcompare(t1,t2,ignore_mt,eps)
|
||||
local ty1 = type(t1)
|
||||
|
@ -143,23 +136,27 @@ function tablex.deepcompare(t1,t2,ignore_mt,eps)
|
|||
-- as well as tables which have the metamethod __eq
|
||||
local mt = getmetatable(t1)
|
||||
if not ignore_mt and mt and mt.__eq then return t1 == t2 end
|
||||
for k1 in pairs(t1) do
|
||||
if t2[k1]==nil then return false end
|
||||
end
|
||||
for k2 in pairs(t2) do
|
||||
if t1[k2]==nil then return false end
|
||||
end
|
||||
for k1,v1 in pairs(t1) do
|
||||
local v2 = t2[k1]
|
||||
if v2 == nil or not deepcompare(v1,v2,ignore_mt,eps) then return false end
|
||||
end
|
||||
for k2,v2 in pairs(t2) do
|
||||
local v1 = t1[k2]
|
||||
if v1 == nil or not deepcompare(v1,v2,ignore_mt,eps) then return false end
|
||||
if not deepcompare(v1,v2,ignore_mt,eps) then return false end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
deepcompare = tablex.deepcompare
|
||||
|
||||
--- compare two arrays using a predicate.
|
||||
-- @param t1 an array
|
||||
-- @param t2 an array
|
||||
-- @param cmp A comparison function
|
||||
-- @within Comparing
|
||||
-- @array t1 an array
|
||||
-- @array t2 an array
|
||||
-- @func cmp A comparison function
|
||||
function tablex.compare (t1,t2,cmp)
|
||||
assert_arg_indexable(1,t1)
|
||||
assert_arg_indexable(2,t2)
|
||||
|
@ -172,8 +169,9 @@ function tablex.compare (t1,t2,cmp)
|
|||
end
|
||||
|
||||
--- compare two list-like tables using an optional predicate, without regard for element order.
|
||||
-- @param t1 a list-like table
|
||||
-- @param t2 a list-like table
|
||||
-- @within Comparing
|
||||
-- @array t1 a list-like table
|
||||
-- @array t2 a list-like table
|
||||
-- @param cmp A comparison function (may be nil)
|
||||
function tablex.compare_no_order (t1,t2,cmp)
|
||||
assert_arg_indexable(1,t1)
|
||||
|
@ -202,9 +200,10 @@ end
|
|||
--- return the index of a value in a list.
|
||||
-- Like string.find, there is an optional index to start searching,
|
||||
-- which can be negative.
|
||||
-- @param t A list-like table (i.e. with numerical indices)
|
||||
-- @within Finding
|
||||
-- @array t A list-like table
|
||||
-- @param val A value
|
||||
-- @param idx index to start; -1 means last element,etc (default 1)
|
||||
-- @int idx index to start; -1 means last element,etc (default 1)
|
||||
-- @return index of value or nil if not found
|
||||
-- @usage find({10,20,30},20) == 2
|
||||
-- @usage find({'a','b','a','c'},'a',2) == 3
|
||||
|
@ -221,7 +220,8 @@ end
|
|||
--- return the index of a value in a list, searching from the end.
|
||||
-- Like string.find, there is an optional index to start searching,
|
||||
-- which can be negative.
|
||||
-- @param t A list-like table (i.e. with numerical indices)
|
||||
-- @within Finding
|
||||
-- @array t A list-like table
|
||||
-- @param val A value
|
||||
-- @param idx index to start; -1 means last element,etc (default 1)
|
||||
-- @return index of value or nil if not found
|
||||
|
@ -238,8 +238,9 @@ end
|
|||
|
||||
|
||||
--- return the index (or key) of a value in a table using a comparison function.
|
||||
-- @param t A table
|
||||
-- @param cmp A comparison function
|
||||
-- @within Finding
|
||||
-- @tab t A table
|
||||
-- @func cmp A comparison function
|
||||
-- @param arg an optional second argument to the function
|
||||
-- @return index of value, or nil if not found
|
||||
-- @return value returned by comparison function
|
||||
|
@ -254,8 +255,8 @@ function tablex.find_if(t,cmp,arg)
|
|||
end
|
||||
|
||||
--- return a list of all values in a table indexed by another list.
|
||||
-- @param tbl a table
|
||||
-- @param idx an index table (a list of keys)
|
||||
-- @tab tbl a table
|
||||
-- @array idx an index table (a list of keys)
|
||||
-- @return a list-like table
|
||||
-- @usage index_by({10,20,30,40},{2,4}) == {20,40}
|
||||
-- @usage index_by({one=1,two=2,three=3},{'one','three'}) == {1,3}
|
||||
|
@ -272,8 +273,9 @@ end
|
|||
--- apply a function to all values of a table.
|
||||
-- This returns a table of the results.
|
||||
-- Any extra arguments are passed to the function.
|
||||
-- @param fun A function that takes at least one argument
|
||||
-- @param t A table
|
||||
-- @within MappingAndFiltering
|
||||
-- @func fun A function that takes at least one argument
|
||||
-- @tab t A table
|
||||
-- @param ... optional arguments
|
||||
-- @usage map(function(v) return v*v end, {10,20,30,fred=2}) is {100,400,900,fred=4}
|
||||
function tablex.map(fun,t,...)
|
||||
|
@ -289,8 +291,9 @@ end
|
|||
--- apply a function to all values of a list.
|
||||
-- This returns a table of the results.
|
||||
-- Any extra arguments are passed to the function.
|
||||
-- @param fun A function that takes at least one argument
|
||||
-- @param t a table (applies to array part)
|
||||
-- @within MappingAndFiltering
|
||||
-- @func fun A function that takes at least one argument
|
||||
-- @array t a table (applies to array part)
|
||||
-- @param ... optional arguments
|
||||
-- @return a list-like table
|
||||
-- @usage imap(function(v) return v*v end, {10,20,30,fred=2}) is {100,400,900}
|
||||
|
@ -305,8 +308,9 @@ function tablex.imap(fun,t,...)
|
|||
end
|
||||
|
||||
--- apply a named method to values from a table.
|
||||
-- @param name the method name
|
||||
-- @param t a list-like table
|
||||
-- @within MappingAndFiltering
|
||||
-- @string name the method name
|
||||
-- @array t a list-like table
|
||||
-- @param ... any extra arguments to the method
|
||||
function tablex.map_named_method (name,t,...)
|
||||
utils.assert_string(1,name)
|
||||
|
@ -320,41 +324,44 @@ function tablex.map_named_method (name,t,...)
|
|||
return setmeta(res,t,List)
|
||||
end
|
||||
|
||||
|
||||
--- apply a function to all values of a table, in-place.
|
||||
-- Any extra arguments are passed to the function.
|
||||
-- @param fun A function that takes at least one argument
|
||||
-- @param t a table
|
||||
-- @func fun A function that takes at least one argument
|
||||
-- @tab t a table
|
||||
-- @param ... extra arguments
|
||||
function tablex.transform (fun,t,...)
|
||||
assert_arg_iterable(1,t)
|
||||
fun = function_arg(1,fun)
|
||||
for k,v in pairs(t) do
|
||||
t[v] = fun(v,...)
|
||||
t[k] = fun(v,...)
|
||||
end
|
||||
end
|
||||
|
||||
--- generate a table of all numbers in a range
|
||||
-- @param start number
|
||||
-- @param finish number
|
||||
-- @param step optional increment (default 1 for increasing, -1 for decreasing)
|
||||
--- generate a table of all numbers in a range.
|
||||
-- This is consistent with a numerical for loop.
|
||||
-- @int start number
|
||||
-- @int finish number
|
||||
-- @int[opt=1] step make this negative for start < finish
|
||||
function tablex.range (start,finish,step)
|
||||
if start == finish then return {start}
|
||||
elseif start > finish then return {}
|
||||
local res
|
||||
step = step or 1
|
||||
if start == finish then
|
||||
res = {start}
|
||||
elseif (start > finish and step > 0) or (finish > start and step < 0) then
|
||||
res = {}
|
||||
else
|
||||
local k = 1
|
||||
res = {}
|
||||
for i=start,finish,step do res[k]=i; k=k+1 end
|
||||
end
|
||||
local res = {}
|
||||
local k = 1
|
||||
if not step then
|
||||
if finish > start then step = finish > start and 1 or -1 end
|
||||
end
|
||||
for i=start,finish,step do res[k]=i; k=k+1 end
|
||||
return res
|
||||
return makelist(res)
|
||||
end
|
||||
|
||||
--- apply a function to values from two tables.
|
||||
-- @param fun a function of at least two arguments
|
||||
-- @param t1 a table
|
||||
-- @param t2 a table
|
||||
-- @within MappingAndFiltering
|
||||
-- @func fun a function of at least two arguments
|
||||
-- @tab t1 a table
|
||||
-- @tab t2 a table
|
||||
-- @param ... extra arguments
|
||||
-- @return a table
|
||||
-- @usage map2('+',{1,2,3,m=4},{10,20,30,m=40}) is {11,22,23,m=44}
|
||||
|
@ -371,9 +378,10 @@ end
|
|||
|
||||
--- apply a function to values from two arrays.
|
||||
-- The result will be the length of the shortest array.
|
||||
-- @param fun a function of at least two arguments
|
||||
-- @param t1 a list-like table
|
||||
-- @param t2 a list-like table
|
||||
-- @within MappingAndFiltering
|
||||
-- @func fun a function of at least two arguments
|
||||
-- @array t1 a list-like table
|
||||
-- @array t2 a list-like table
|
||||
-- @param ... extra arguments
|
||||
-- @usage imap2('+',{1,2,3,m=4},{10,20,30,m=40}) is {11,22,23}
|
||||
function tablex.imap2 (fun,t1,t2,...)
|
||||
|
@ -388,8 +396,8 @@ function tablex.imap2 (fun,t1,t2,...)
|
|||
end
|
||||
|
||||
--- 'reduce' a list using a binary function.
|
||||
-- @param fun a function of two arguments
|
||||
-- @param t a list-like table
|
||||
-- @func fun a function of two arguments
|
||||
-- @array t a list-like table
|
||||
-- @return the result of the function
|
||||
-- @usage reduce('+',{1,2,3,4}) == 10
|
||||
function tablex.reduce (fun,t)
|
||||
|
@ -405,10 +413,11 @@ end
|
|||
|
||||
--- apply a function to all elements of a table.
|
||||
-- The arguments to the function will be the value,
|
||||
-- the key and <i>finally</i> any extra arguments passed to this function.
|
||||
-- Note that the Lua 5.0 function table.foreach passed the <i>key</i> first.
|
||||
-- @param t a table
|
||||
-- @param fun a function with at least one argument
|
||||
-- the key and _finally_ any extra arguments passed to this function.
|
||||
-- Note that the Lua 5.0 function table.foreach passed the _key_ first.
|
||||
-- @within Iterating
|
||||
-- @tab t a table
|
||||
-- @func fun a function with at least one argument
|
||||
-- @param ... extra arguments
|
||||
function tablex.foreach(t,fun,...)
|
||||
assert_arg_iterable(1,t)
|
||||
|
@ -420,9 +429,10 @@ end
|
|||
|
||||
--- apply a function to all elements of a list-like table in order.
|
||||
-- The arguments to the function will be the value,
|
||||
-- the index and <i>finally</i> any extra arguments passed to this function
|
||||
-- @param t a table
|
||||
-- @param fun a function with at least one argument
|
||||
-- the index and _finally_ any extra arguments passed to this function
|
||||
-- @within Iterating
|
||||
-- @array t a table
|
||||
-- @func fun a function with at least one argument
|
||||
-- @param ... optional arguments
|
||||
function tablex.foreachi(t,fun,...)
|
||||
assert_arg_indexable(1,t)
|
||||
|
@ -432,13 +442,13 @@ function tablex.foreachi(t,fun,...)
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
--- Apply a function to a number of tables.
|
||||
-- A more general version of map
|
||||
-- The result is a table containing the result of applying that function to the
|
||||
-- ith value of each table. Length of output list is the minimum length of all the lists
|
||||
-- @param fun a function of n arguments
|
||||
-- @param ... n tables
|
||||
-- @within MappingAndFiltering
|
||||
-- @func fun a function of n arguments
|
||||
-- @tab ... n tables
|
||||
-- @usage mapn(function(x,y,z) return x+y+z end, {1,2,3},{10,20,30},{100,200,300}) is {111,222,333}
|
||||
-- @usage mapn(math.max, {1,20,300},{10,2,3},{100,200,100}) is {100,200,300}
|
||||
-- @param fun A function that takes as many arguments as there are tables
|
||||
|
@ -465,8 +475,9 @@ end
|
|||
-- The function can return a value and a key (note the order!). If both
|
||||
-- are not nil, then this pair is inserted into the result. If only value is not nil, then
|
||||
-- it is appended to the result.
|
||||
-- @param fun A function which will be passed each key and value as arguments, plus any extra arguments to pairmap.
|
||||
-- @param t A table
|
||||
-- @within MappingAndFiltering
|
||||
-- @func fun A function which will be passed each key and value as arguments, plus any extra arguments to pairmap.
|
||||
-- @tab t A table
|
||||
-- @param ... optional arguments
|
||||
-- @usage pairmap(function(k,v) return v end,{fred=10,bonzo=20}) is {10,20} _or_ {20,10}
|
||||
-- @usage pairmap(function(k,v) return {k,v},k end,{one=1,two=2}) is {one={'one',1},two={'two',2}}
|
||||
|
@ -488,7 +499,8 @@ end
|
|||
local function keys_op(i,v) return i end
|
||||
|
||||
--- return all the keys of a table in arbitrary order.
|
||||
-- @param t A table
|
||||
-- @within Extraction
|
||||
-- @tab t A table
|
||||
function tablex.keys(t)
|
||||
assert_arg_iterable(1,t)
|
||||
return makelist(tablex.pairmap(keys_op,t))
|
||||
|
@ -497,7 +509,8 @@ end
|
|||
local function values_op(i,v) return v end
|
||||
|
||||
--- return all the values of the table in arbitrary order
|
||||
-- @param t A table
|
||||
-- @within Extraction
|
||||
-- @tab t A table
|
||||
function tablex.values(t)
|
||||
assert_arg_iterable(1,t)
|
||||
return makelist(tablex.pairmap(values_op,t))
|
||||
|
@ -507,7 +520,7 @@ local function index_map_op (i,v) return i,v end
|
|||
|
||||
--- create an index map from a list-like table. The original values become keys,
|
||||
-- and the associated values are the indices into the original list.
|
||||
-- @param t a list-like table
|
||||
-- @array t a list-like table
|
||||
-- @return a map-like table
|
||||
function tablex.index_map (t)
|
||||
assert_arg_indexable(1,t)
|
||||
|
@ -518,20 +531,20 @@ local function set_op(i,v) return true,v end
|
|||
|
||||
--- create a set from a list-like table. A set is a table where the original values
|
||||
-- become keys, and the associated values are all true.
|
||||
-- @param t a list-like table
|
||||
-- @array t a list-like table
|
||||
-- @return a set (a map-like table)
|
||||
function tablex.makeset (t)
|
||||
assert_arg_indexable(1,t)
|
||||
return setmetatable(tablex.pairmap(set_op,t),Set)
|
||||
end
|
||||
|
||||
|
||||
--- combine two tables, either as union or intersection. Corresponds to
|
||||
-- set operations for sets () but more general. Not particularly
|
||||
-- useful for list-like tables.
|
||||
-- @param t1 a table
|
||||
-- @param t2 a table
|
||||
-- @param dup true for a union, false for an intersection.
|
||||
-- @within Merging
|
||||
-- @tab t1 a table
|
||||
-- @tab t2 a table
|
||||
-- @bool dup true for a union, false for an intersection.
|
||||
-- @usage merge({alice=23,fred=34},{bob=25,fred=34}) is {fred=34}
|
||||
-- @usage merge({alice=23,fred=34},{bob=25,fred=34},true) is {bob=25,fred=34,alice=23}
|
||||
-- @see tablex.index_map
|
||||
|
@ -553,28 +566,29 @@ end
|
|||
--- a new table which is the difference of two tables.
|
||||
-- With sets (where the values are all true) this is set difference and
|
||||
-- symmetric difference depending on the third parameter.
|
||||
-- @param s1 a map-like table or set
|
||||
-- @param s2 a map-like table or set
|
||||
-- @param symm symmetric difference (default false)
|
||||
-- @within Merging
|
||||
-- @tab s1 a map-like table or set
|
||||
-- @tab s2 a map-like table or set
|
||||
-- @bool symm symmetric difference (default false)
|
||||
-- @return a map-like table or set
|
||||
function tablex.difference (s1,s2,symm)
|
||||
assert_arg_iterable(1,s1)
|
||||
assert_arg_iterable(2,s2)
|
||||
local res = {}
|
||||
for k,v in pairs(s1) do
|
||||
if not s2[k] then res[k] = v end
|
||||
if s2[k] == nil then res[k] = v end
|
||||
end
|
||||
if symm then
|
||||
for k,v in pairs(s2) do
|
||||
if not s1[k] then res[k] = v end
|
||||
if s1[k] == nil then res[k] = v end
|
||||
end
|
||||
end
|
||||
return setmeta(res,s1,Map)
|
||||
end
|
||||
|
||||
--- A table where the key/values are the values and value counts of the table.
|
||||
-- @param t a list-like table
|
||||
-- @param cmp a function that defines equality (otherwise uses ==)
|
||||
-- @array t a list-like table
|
||||
-- @func cmp a function that defines equality (otherwise uses ==)
|
||||
-- @return a map-like table
|
||||
-- @see seq.count_map
|
||||
function tablex.count_map (t,cmp)
|
||||
|
@ -590,7 +604,13 @@ function tablex.count_map (t,cmp)
|
|||
res[v] = 1 -- there's at least one instance
|
||||
for j = i+1,n do
|
||||
local w = t[j]
|
||||
if cmp and cmp(v,w) or v == w then
|
||||
local ok
|
||||
if cmp then
|
||||
ok = cmp(v,w)
|
||||
else
|
||||
ok = v == w
|
||||
end
|
||||
if ok then
|
||||
res[v] = res[v] + 1
|
||||
mask[w] = true
|
||||
end
|
||||
|
@ -600,9 +620,10 @@ function tablex.count_map (t,cmp)
|
|||
return setmetatable(res,Map)
|
||||
end
|
||||
|
||||
--- filter a table's values using a predicate function
|
||||
-- @param t a list-like table
|
||||
-- @param pred a boolean function
|
||||
--- filter an array's values using a predicate function
|
||||
-- @within MappingAndFiltering
|
||||
-- @array t a list-like table
|
||||
-- @func pred a boolean function
|
||||
-- @param arg optional argument to be passed as second argument of the predicate
|
||||
function tablex.filter (t,pred,arg)
|
||||
assert_arg_indexable(1,t)
|
||||
|
@ -620,7 +641,9 @@ end
|
|||
|
||||
--- return a table where each element is a table of the ith values of an arbitrary
|
||||
-- number of tables. It is equivalent to a matrix transpose.
|
||||
-- @within Merging
|
||||
-- @usage zip({10,20,30},{100,200,300}) is {{10,100},{20,200},{30,300}}
|
||||
-- @array ... arrays to be zipped
|
||||
function tablex.zip(...)
|
||||
return tablex.mapn(function(...) return {...} end,...)
|
||||
end
|
||||
|
@ -652,24 +675,26 @@ function _copy (dest,src,idest,isrc,nsrc,clean_tail)
|
|||
return dest
|
||||
end
|
||||
|
||||
--- copy an array into another one, resizing the destination if necessary. <br>
|
||||
-- @param dest a list-like table
|
||||
-- @param src a list-like table
|
||||
-- @param idest where to start copying values from source (default 1)
|
||||
-- @param isrc where to start copying values into destination (default 1)
|
||||
-- @param nsrc number of elements to copy from source (default source size)
|
||||
--- copy an array into another one, clearing `dest` after `idest+nsrc`, if necessary.
|
||||
-- @within Copying
|
||||
-- @array dest a list-like table
|
||||
-- @array src a list-like table
|
||||
-- @int[opt=1] idest where to start copying values into destination
|
||||
-- @int[opt=1] isrc where to start copying values from source
|
||||
-- @int[opt=#src] nsrc number of elements to copy from source
|
||||
function tablex.icopy (dest,src,idest,isrc,nsrc)
|
||||
assert_arg_indexable(1,dest)
|
||||
assert_arg_indexable(2,src)
|
||||
return _copy(dest,src,idest,isrc,nsrc,true)
|
||||
end
|
||||
|
||||
--- copy an array into another one. <br>
|
||||
-- @param dest a list-like table
|
||||
-- @param src a list-like table
|
||||
-- @param idest where to start copying values from source (default 1)
|
||||
-- @param isrc where to start copying values into destination (default 1)
|
||||
-- @param nsrc number of elements to copy from source (default source size)
|
||||
--- copy an array into another one.
|
||||
-- @within Copying
|
||||
-- @array dest a list-like table
|
||||
-- @array src a list-like table
|
||||
-- @int[opt=1] idest where to start copying values into destination
|
||||
-- @int[opt=1] isrc where to start copying values from source
|
||||
-- @int[opt=#src] nsrc number of elements to copy from source
|
||||
function tablex.move (dest,src,idest,isrc,nsrc)
|
||||
assert_arg_indexable(1,dest)
|
||||
assert_arg_indexable(2,src)
|
||||
|
@ -690,9 +715,10 @@ end
|
|||
-- If first or last are negative then they are relative to the end of the list
|
||||
-- eg. sub(t,-2) gives last 2 entries in a list, and
|
||||
-- sub(t,-4,-2) gives from -4th to -2nd
|
||||
-- @param t a list-like table
|
||||
-- @param first An index
|
||||
-- @param last An index
|
||||
-- @within Extraction
|
||||
-- @array t a list-like table
|
||||
-- @int first An index
|
||||
-- @int last An index
|
||||
-- @return a new List
|
||||
function tablex.sub(t,first,last)
|
||||
assert_arg_indexable(1,t)
|
||||
|
@ -704,14 +730,14 @@ end
|
|||
|
||||
--- set an array range to a value. If it's a function we use the result
|
||||
-- of applying it to the indices.
|
||||
-- @param t a list-like table
|
||||
-- @array t a list-like table
|
||||
-- @param val a value
|
||||
-- @param i1 start range (default 1)
|
||||
-- @param i2 end range (default table size)
|
||||
-- @int[opt=1] i1 start range
|
||||
-- @int[opt=#t] i2 end range
|
||||
function tablex.set (t,val,i1,i2)
|
||||
assert_arg_indexable(1,t)
|
||||
i1,i2 = i1 or 1,i2 or #t
|
||||
if utils.is_callable(val) then
|
||||
if types.is_callable(val) then
|
||||
for i = i1,i2 do
|
||||
t[i] = val(i)
|
||||
end
|
||||
|
@ -723,8 +749,8 @@ function tablex.set (t,val,i1,i2)
|
|||
end
|
||||
|
||||
--- create a new array of specified size with initial value.
|
||||
-- @param n size
|
||||
-- @param val initial value (can be nil, but don't expect # to work!)
|
||||
-- @int n size
|
||||
-- @param val initial value (can be `nil`, but don't expect `#` to work!)
|
||||
-- @return the table
|
||||
function tablex.new (n,val)
|
||||
local res = {}
|
||||
|
@ -733,17 +759,20 @@ function tablex.new (n,val)
|
|||
end
|
||||
|
||||
--- clear out the contents of a table.
|
||||
-- @param t a table
|
||||
-- @array t a list
|
||||
-- @param istart optional start position
|
||||
function tablex.clear(t,istart)
|
||||
istart = istart or 1
|
||||
for i = istart,#t do remove(t) end
|
||||
end
|
||||
|
||||
--- insert values into a table. <br>
|
||||
-- insertvalues(t, [pos,] values) <br>
|
||||
-- similar to table.insert but inserts values from given table "values",
|
||||
-- not the object itself, into table "t" at position "pos".
|
||||
--- insert values into a table.
|
||||
-- similar to `table.insert` but inserts values from given table `values`,
|
||||
-- not the object itself, into table `t` at position `pos`.
|
||||
-- @within Copying
|
||||
-- @array t the list
|
||||
-- @int[opt] position (default is at end)
|
||||
-- @array values
|
||||
function tablex.insertvalues(t, ...)
|
||||
assert_arg(1,t,'table')
|
||||
local pos, values
|
||||
|
@ -765,9 +794,10 @@ function tablex.insertvalues(t, ...)
|
|||
end
|
||||
|
||||
--- remove a range of values from a table.
|
||||
-- @param t a list-like table
|
||||
-- @param i1 start index
|
||||
-- @param i2 end index
|
||||
-- End of range may be negative.
|
||||
-- @array t a list-like table
|
||||
-- @int i1 start index
|
||||
-- @int i2 end index
|
||||
-- @return the table
|
||||
function tablex.removevalues (t,i1,i2)
|
||||
assert_arg(1,t,'table')
|
||||
|
@ -800,9 +830,10 @@ _find = function (t,value,tables)
|
|||
end
|
||||
|
||||
--- find a value in a table by recursive search.
|
||||
-- @param t the table
|
||||
-- @within Finding
|
||||
-- @tab t the table
|
||||
-- @param value the value
|
||||
-- @param exclude any tables to avoid searching
|
||||
-- @array[opt] exclude any tables to avoid searching
|
||||
-- @usage search(_G,math.sin,{package.path}) == 'math.sin'
|
||||
-- @return a fieldspec, e.g. 'a.b' or 'math.sin'
|
||||
function tablex.search (t,value,exclude)
|
||||
|
@ -814,4 +845,54 @@ function tablex.search (t,value,exclude)
|
|||
return _find(t,value,tables)
|
||||
end
|
||||
|
||||
--- return an iterator to a table sorted by its keys
|
||||
-- @within Iterating
|
||||
-- @tab t the table
|
||||
-- @func f an optional comparison function (f(x,y) is true if x < y)
|
||||
-- @usage for k,v in tablex.sort(t) do print(k,v) end
|
||||
-- @return an iterator to traverse elements sorted by the keys
|
||||
function tablex.sort(t,f)
|
||||
local keys = {}
|
||||
for k in pairs(t) do keys[#keys + 1] = k end
|
||||
tsort(keys,f)
|
||||
local i = 0
|
||||
return function()
|
||||
i = i + 1
|
||||
return keys[i], t[keys[i]]
|
||||
end
|
||||
end
|
||||
|
||||
--- return an iterator to a table sorted by its values
|
||||
-- @within Iterating
|
||||
-- @tab t the table
|
||||
-- @func f an optional comparison function (f(x,y) is true if x < y)
|
||||
-- @usage for k,v in tablex.sortv(t) do print(k,v) end
|
||||
-- @return an iterator to traverse elements sorted by the values
|
||||
function tablex.sortv(t,f)
|
||||
local rev = {}
|
||||
for k,v in pairs(t) do rev[v] = k end
|
||||
local next = tablex.sort(rev,f)
|
||||
return function()
|
||||
local value,key = next()
|
||||
return key,value
|
||||
end
|
||||
end
|
||||
|
||||
--- modifies a table to be read only.
|
||||
-- This only offers weak protection. Tables can still be modified with
|
||||
-- `table.insert` and `rawset`.
|
||||
-- @tab t the table
|
||||
-- @return the table read only.
|
||||
function tablex.readonly(t)
|
||||
local mt = {
|
||||
__index=t,
|
||||
__newindex=function(t, k, v) error("Attempt to modify read-only table", 2) end,
|
||||
__pairs=function() return pairs(t) end,
|
||||
__ipairs=function() return ipairs(t) end,
|
||||
__len=function() return #t end,
|
||||
__metatable=false
|
||||
}
|
||||
return setmetatable({}, mt)
|
||||
end
|
||||
|
||||
return tablex
|
||||
|
|
|
@ -29,31 +29,32 @@
|
|||
-- @module pl.template
|
||||
|
||||
local utils = require 'pl.utils'
|
||||
local append,format = table.insert,string.format
|
||||
|
||||
|
||||
local function parseHashLines(chunk,brackets,esc)
|
||||
local append,format,strsub,strfind = table.insert,string.format,string.sub,string.find
|
||||
local exec_pat = "()$(%b"..brackets..")()"
|
||||
|
||||
local function parseDollarParen(pieces, chunk, s, e)
|
||||
local s = 1
|
||||
for term, executed, e in chunk:gmatch (exec_pat) do
|
||||
executed = '('..executed:sub(2,-2)..')'
|
||||
executed = '('..strsub(executed,2,-2)..')'
|
||||
append(pieces,
|
||||
format("%q..(%s or '')..",chunk:sub(s, term - 1), executed))
|
||||
format("%q..(%s or '')..",strsub(chunk,s, term - 1), executed))
|
||||
s = e
|
||||
end
|
||||
append(pieces, format("%q", chunk:sub(s)))
|
||||
append(pieces, format("%q", strsub(chunk,s)))
|
||||
end
|
||||
|
||||
local esc_pat = esc.."+([^\n]*\n?)"
|
||||
local esc_pat1, esc_pat2 = "^"..esc_pat, "\n"..esc_pat
|
||||
local pieces, s = {"return function(_put) ", n = 1}, 1
|
||||
while true do
|
||||
local ss, e, lua = chunk:find (esc_pat1, s)
|
||||
local ss, e, lua = strfind (chunk,esc_pat1, s)
|
||||
if not e then
|
||||
ss, e, lua = chunk:find(esc_pat2, s)
|
||||
ss, e, lua = strfind(chunk,esc_pat2, s)
|
||||
append(pieces, "_put(")
|
||||
parseDollarParen(pieces, chunk:sub(s, ss))
|
||||
parseDollarParen(pieces, strsub(chunk,s, ss))
|
||||
append(pieces, ")")
|
||||
if not e then break end
|
||||
end
|
||||
|
@ -67,13 +68,14 @@ end
|
|||
local template = {}
|
||||
|
||||
--- expand the template using the specified environment.
|
||||
-- @param str the template string
|
||||
-- @param env the environment (by default empty). <br>
|
||||
-- There are three special fields in the environment table <ul>
|
||||
-- <li><code>_parent</code> continue looking up in this table</li>
|
||||
-- <li><code>_brackets</code>; default is '()', can be any suitable bracket pair</li>
|
||||
-- <li><code>_escape</code>; default is '#' </li>
|
||||
-- </ul>
|
||||
-- There are three special fields in the environment table `env`
|
||||
--
|
||||
-- * `_parent` continue looking up in this table (e.g. `_parent=_G`)
|
||||
-- * `_brackets`; default is '()', can be any suitable bracket pair
|
||||
-- * `_escape`; default is '#'
|
||||
--
|
||||
-- @string str the template string
|
||||
-- @tab[opt] env the environment
|
||||
function template.substitute(str,env)
|
||||
env = env or {}
|
||||
if rawget(env,"_parent") then
|
||||
|
|
|
@ -12,7 +12,7 @@ local tablex = require 'pl.tablex'
|
|||
local utils = require 'pl.utils'
|
||||
local pretty = require 'pl.pretty'
|
||||
local path = require 'pl.path'
|
||||
local print,type = print,type
|
||||
local print,type,unpack = print,type,utils.pack
|
||||
local clock = os.clock
|
||||
local debug = require 'debug'
|
||||
local io,debug = io,debug
|
||||
|
@ -29,8 +29,8 @@ end
|
|||
|
||||
local test = {}
|
||||
|
||||
local function complain (x,y,msg)
|
||||
local i = debug.getinfo(3)
|
||||
local function complain (x,y,msg,where)
|
||||
local i = debug.getinfo(3 + (where or 0))
|
||||
local err = io.stderr
|
||||
err:write(path.basename(i.short_src)..':'..i.currentline..': assertion failed\n')
|
||||
err:write("got:\t",dump(x),'\n')
|
||||
|
@ -43,6 +43,8 @@ end
|
|||
-- @param x a value
|
||||
-- @param y value to compare first value against
|
||||
-- @param msg message
|
||||
-- @param where extra level offset for errors
|
||||
-- @function complain
|
||||
test.complain = complain
|
||||
|
||||
--- like assert, except takes two arguments that must be equal and can be tables.
|
||||
|
@ -50,32 +52,40 @@ test.complain = complain
|
|||
-- @param x any value
|
||||
-- @param y a value equal to x
|
||||
-- @param eps an optional tolerance for numerical comparisons
|
||||
function test.asserteq (x,y,eps)
|
||||
-- @param where extra level offset
|
||||
function test.asserteq (x,y,eps,where)
|
||||
local res = x == y
|
||||
if not res then
|
||||
res = tablex.deepcompare(x,y,true,eps)
|
||||
end
|
||||
if not res then
|
||||
complain(x,y)
|
||||
complain(x,y,nil,where)
|
||||
end
|
||||
end
|
||||
|
||||
--- assert that the first string matches the second.
|
||||
-- @param s1 a string
|
||||
-- @param s2 a string
|
||||
function test.assertmatch (s1,s2)
|
||||
-- @param where extra level offset
|
||||
function test.assertmatch (s1,s2,where)
|
||||
if not s1:match(s2) then
|
||||
complain (s1,s2,"these strings did not match")
|
||||
complain (s1,s2,"these strings did not match",where)
|
||||
end
|
||||
end
|
||||
|
||||
--- assert that the function raises a particular error.
|
||||
-- @param fn a table of the form {function,arg1,...}
|
||||
-- @param fn a function or a table of the form {function,arg1,...}
|
||||
-- @param e a string to match the error against
|
||||
function test.assertraise(fn,e)
|
||||
local ok, err = pcall(unpack(fn))
|
||||
-- @param where extra level offset
|
||||
function test.assertraise(fn,e,where)
|
||||
local ok, err
|
||||
if type(fn) == 'table' then
|
||||
ok, err = pcall(unpack(fn))
|
||||
else
|
||||
ok, err = pcall(fn)
|
||||
end
|
||||
if not err or err:match(e)==nil then
|
||||
complain (err,e,"these errors did not match")
|
||||
complain (err,e,"these errors did not match",where)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -86,9 +96,10 @@ end
|
|||
-- @param x2 any value
|
||||
-- @param y1 any value
|
||||
-- @param y2 any value
|
||||
function test.asserteq2 (x1,x2,y1,y2)
|
||||
if x1 ~= y1 then complain(x1,y1) end
|
||||
if x2 ~= y2 then complain(x2,y2) end
|
||||
-- @param where extra level offset
|
||||
function test.asserteq2 (x1,x2,y1,y2,where)
|
||||
if x1 ~= y1 then complain(x1,y1,nil,where) end
|
||||
if x2 ~= y2 then complain(x2,y2,nil,where) end
|
||||
end
|
||||
|
||||
-- tuple type --
|
||||
|
@ -99,7 +110,7 @@ function tuple_mt.__tostring(self)
|
|||
local ts = {}
|
||||
for i=1, self.n do
|
||||
local s = self[i]
|
||||
ts[i] = type(s) == 'string' and string.format('%q', s) or tostring(s)
|
||||
ts[i] = type(s) == 'string' and ('%q'):format(s) or tostring(s)
|
||||
end
|
||||
return 'tuple(' .. table.concat(ts, ', ') .. ')'
|
||||
end
|
||||
|
@ -117,14 +128,14 @@ end
|
|||
-- very useful for testing functions which return a number of values.
|
||||
-- @usage asserteq(tuple( ('ab'):find 'a'), tuple(1,1))
|
||||
function test.tuple(...)
|
||||
return setmetatable({n=select('#', ...), ...}, tuple_mt)
|
||||
return setmetatable(table.pack(...), tuple_mt)
|
||||
end
|
||||
|
||||
--- Time a function. Call the function a given number of times, and report the number of seconds taken,
|
||||
-- together with a message. Any extra arguments will be passed to the function.
|
||||
-- @param msg a descriptive message
|
||||
-- @param n number of times to call the function
|
||||
-- @param fun the function
|
||||
-- @string msg a descriptive message
|
||||
-- @int n number of times to call the function
|
||||
-- @func fun the function
|
||||
-- @param ... optional arguments to fun
|
||||
function test.timer(msg,n,fun,...)
|
||||
local start = clock()
|
||||
|
|
|
@ -15,13 +15,15 @@
|
|||
-- > = '$name = $value' % {name='dog',value='Pluto'}
|
||||
-- dog = Pluto
|
||||
--
|
||||
-- Dependencies: `pl.utils`
|
||||
-- Dependencies: `pl.utils`, `pl.types`
|
||||
-- @module pl.text
|
||||
|
||||
local gsub = string.gsub
|
||||
local concat,append = table.concat,table.insert
|
||||
local utils = require 'pl.utils'
|
||||
local bind1,usplit,assert_arg,is_callable = utils.bind1,utils.split,utils.assert_arg,utils.is_callable
|
||||
local bind1,usplit,assert_arg = utils.bind1,utils.split,utils.assert_arg
|
||||
local is_callable = require 'pl.types'.is_callable
|
||||
local unpack = utils.unpack
|
||||
|
||||
local function lstrip(str) return (str:gsub('^%s+','')) end
|
||||
local function strip(str) return (lstrip(str):gsub('%s+$','')) end
|
||||
|
@ -52,7 +54,7 @@ end
|
|||
-- @return indented string
|
||||
function text.indent (s,n,ch)
|
||||
assert_arg(1,s,'string')
|
||||
assert_arg(2,s,'number')
|
||||
assert_arg(2,n,'number')
|
||||
return _indent(s,string.rep(ch or ' ',n))
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
---- Dealing with Detailed Type Information
|
||||
|
||||
-- Dependencies `pl.utils`
|
||||
-- @module pl.types
|
||||
|
||||
local utils = require 'pl.utils'
|
||||
local types = {}
|
||||
|
||||
--- is the object either a function or a callable object?.
|
||||
-- @param obj Object to check.
|
||||
function types.is_callable (obj)
|
||||
return type(obj) == 'function' or getmetatable(obj) and getmetatable(obj).__call
|
||||
end
|
||||
|
||||
--- is the object of the specified type?.
|
||||
-- If the type is a string, then use type, otherwise compare with metatable
|
||||
-- @param obj An object to check
|
||||
-- @param tp String of what type it should be
|
||||
-- @function is_type
|
||||
types.is_type = utils.is_type
|
||||
|
||||
local fileMT = getmetatable(io.stdout)
|
||||
|
||||
--- a string representation of a type.
|
||||
-- For tables with metatables, we assume that the metatable has a `_name`
|
||||
-- field. Knows about Lua file objects.
|
||||
-- @param obj an object
|
||||
-- @return a string like 'number', 'table' or 'List'
|
||||
function types.type (obj)
|
||||
local t = type(obj)
|
||||
if t == 'table' or t == 'userdata' then
|
||||
local mt = getmetatable(obj)
|
||||
if mt == fileMT then
|
||||
return 'file'
|
||||
else
|
||||
return mt._name or "unknown "..t
|
||||
end
|
||||
else
|
||||
return t
|
||||
end
|
||||
end
|
||||
|
||||
--- is this number an integer?
|
||||
-- @param x a number
|
||||
-- @raise error if x is not a number
|
||||
function types.is_integer (x)
|
||||
return math.ceil(x)==x
|
||||
end
|
||||
|
||||
--- Check if the object is "empty".
|
||||
-- An object is considered empty if it is nil, a table with out any items (key,
|
||||
-- value pairs or indexes), or a string with no content ("").
|
||||
-- @param o The object to check if it is empty.
|
||||
-- @param ignore_spaces If the object is a string and this is true the string is
|
||||
-- considered empty is it only contains spaces.
|
||||
-- @return true if the object is empty, otherwise false.
|
||||
function types.is_empty(o, ignore_spaces)
|
||||
if o == nil or (type(o) == "table" and not next(o)) or (type(o) == "string" and (o == "" or (ignore_spaces and o:match("^%s+$")))) then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function check_meta (val)
|
||||
if type(val) == 'table' then return true end
|
||||
return getmetatable(val)
|
||||
end
|
||||
|
||||
--- is an object 'array-like'?
|
||||
-- @param val any value.
|
||||
function types.is_indexable (val)
|
||||
local mt = check_meta(val)
|
||||
if mt == true then return true end
|
||||
return not(mt and mt.__len and mt.__index)
|
||||
end
|
||||
|
||||
--- can an object be iterated over with `ipairs`?
|
||||
-- @param val any value.
|
||||
function types.is_iterable (val)
|
||||
local mt = check_meta(val)
|
||||
if mt == true then return true end
|
||||
return not(mt and mt.__pairs)
|
||||
end
|
||||
|
||||
--- can an object accept new key/pair values?
|
||||
-- @param val any value.
|
||||
function types.is_writeable (val)
|
||||
local mt = check_meta(val)
|
||||
if mt == true then return true end
|
||||
return not(mt and mt.__newindex)
|
||||
end
|
||||
|
||||
-- Strings that should evaluate to true.
|
||||
local trues = { yes=true, y=true, ["true"]=true, t=true, ["1"]=true }
|
||||
-- Conditions types should evaluate to true.
|
||||
local true_types = {
|
||||
boolean=function(o, true_strs, check_objs) return o end,
|
||||
string=function(o, true_strs, check_objs)
|
||||
if trues[o:lower()] then
|
||||
return true
|
||||
end
|
||||
-- Check alternative user provided strings.
|
||||
for _,v in ipairs(true_strs or {}) do
|
||||
if type(v) == "string" and o == v:lower() then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end,
|
||||
number=function(o, true_strs, check_objs) return o ~= 0 end,
|
||||
table=function(o, true_strs, check_objs) if check_objs and next(o) ~= nil then return true end return false end
|
||||
}
|
||||
--- Convert to a boolean value.
|
||||
-- True values are:
|
||||
--
|
||||
-- * boolean: true.
|
||||
-- * string: 'yes', 'y', 'true', 't', '1' or additional strings specified by `true_strs`.
|
||||
-- * number: Any non-zero value.
|
||||
-- * table: Is not empty and `check_objs` is true.
|
||||
-- * object: Is not `nil` and `check_objs` is true.
|
||||
--
|
||||
-- @param o The object to evaluate.
|
||||
-- @param[opt] true_strs optional Additional strings that when matched should evaluate to true. Comparison is case insensitive.
|
||||
-- This should be a List of strings. E.g. "ja" to support German.
|
||||
-- @param[opt] check_objs True if objects should be evaluated. Default is to evaluate objects as true if not nil
|
||||
-- or if it is a table and it is not empty.
|
||||
-- @return true if the input evaluates to true, otherwise false.
|
||||
function types.to_bool(o, true_strs, check_objs)
|
||||
local true_func
|
||||
if true_strs then
|
||||
utils.assert_arg(2, true_strs, "table")
|
||||
end
|
||||
true_func = true_types[type(o)]
|
||||
if true_func then
|
||||
return true_func(o, true_strs, check_objs)
|
||||
elseif check_objs and o ~= nil then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
return types
|
|
@ -0,0 +1,45 @@
|
|||
--- Python-style URL quoting library.
|
||||
--
|
||||
-- @module pl.url
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Quote the url.
|
||||
-- @string s the string
|
||||
-- @bool quote_plus Use quote_plus rules
|
||||
function M.quote(s, quote_plus)
|
||||
function url_quote_char(c)
|
||||
return string.format("%%%02X", string.byte(c))
|
||||
end
|
||||
|
||||
if not s or not type(s) == "string" then
|
||||
return s
|
||||
end
|
||||
|
||||
s = s:gsub("\n", "\r\n")
|
||||
s = s:gsub("([^A-Za-z0-9 %-_%./])", url_quote_char)
|
||||
if quote_plus then
|
||||
s = s:gsub(" ", "+")
|
||||
s = s:gsub("/", url_quote_char)
|
||||
else
|
||||
s = s:gsub(" ", "%%20")
|
||||
end
|
||||
|
||||
return s
|
||||
end
|
||||
|
||||
--- Unquote the url.
|
||||
-- @string s the string
|
||||
function M.unquote(s)
|
||||
if not s or not type(s) == "string" then
|
||||
return s
|
||||
end
|
||||
|
||||
s = s:gsub("+", " ")
|
||||
s = s:gsub("%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end)
|
||||
s = s:gsub("\r\n", "\n")
|
||||
|
||||
return s
|
||||
end
|
||||
|
||||
return M
|
|
@ -2,25 +2,24 @@
|
|||
-- See @{01-introduction.md.Generally_useful_functions|the Guide}.
|
||||
-- @module pl.utils
|
||||
local format,gsub,byte = string.format,string.gsub,string.byte
|
||||
local compat = require 'pl.compat'
|
||||
local clock = os.clock
|
||||
local stdout = io.stdout
|
||||
local append = table.insert
|
||||
local unpack = rawget(_G,'unpack') or rawget(table,'unpack')
|
||||
|
||||
local collisions = {}
|
||||
|
||||
local utils = {}
|
||||
|
||||
utils._VERSION = "1.0.3"
|
||||
|
||||
local lua51 = rawget(_G,'setfenv')
|
||||
|
||||
utils.lua51 = lua51
|
||||
if not lua51 then -- Lua 5.2 compatibility
|
||||
unpack = table.unpack
|
||||
loadstring = load
|
||||
end
|
||||
|
||||
utils.dir_separator = _G.package.config:sub(1,1)
|
||||
local utils = {
|
||||
_VERSION = "1.3.2",
|
||||
lua51 = compat.lua51,
|
||||
setfenv = compat.setfenv,
|
||||
getfenv = compat.getfenv,
|
||||
load = compat.load,
|
||||
execute = compat.execute,
|
||||
dir_separator = _G.package.config:sub(1,1),
|
||||
unpack = unpack
|
||||
}
|
||||
|
||||
--- end this program gracefully.
|
||||
-- @param code The exit code or a message to be printed
|
||||
|
@ -58,7 +57,8 @@ local function import_symbol(T,k,v,libname)
|
|||
local key = rawget(T,k)
|
||||
-- warn about collisions!
|
||||
if key and k ~= '_M' and k ~= '_NAME' and k ~= '_PACKAGE' and k ~= '_VERSION' then
|
||||
utils.printf("warning: '%s.%s' overrides existing symbol\n",libname,k)
|
||||
utils.printf("warning: '%s.%s' will not override existing symbol\n",libname,k)
|
||||
return
|
||||
end
|
||||
rawset(T,k,v)
|
||||
end
|
||||
|
@ -205,96 +205,47 @@ function utils.splitv (s,re)
|
|||
return unpack(utils.split(s,re))
|
||||
end
|
||||
|
||||
local lua51_load = load
|
||||
|
||||
if utils.lua51 then -- define Lua 5.2 style load()
|
||||
function utils.load(str,src,mode,env)
|
||||
local chunk,err
|
||||
if type(str) == 'string' then
|
||||
chunk,err = loadstring(str,src)
|
||||
else
|
||||
chunk,err = lua51_load(str,src)
|
||||
end
|
||||
if chunk and env then setfenv(chunk,env) end
|
||||
return chunk,err
|
||||
end
|
||||
else
|
||||
utils.load = load
|
||||
-- setfenv/getfenv replacements for Lua 5.2
|
||||
-- by Sergey Rozhenko
|
||||
-- http://lua-users.org/lists/lua-l/2010-06/msg00313.html
|
||||
-- Roberto Ierusalimschy notes that it is possible for getfenv to return nil
|
||||
-- in the case of a function with no globals:
|
||||
-- http://lua-users.org/lists/lua-l/2010-06/msg00315.html
|
||||
function setfenv(f, t)
|
||||
f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func)
|
||||
local name
|
||||
local up = 0
|
||||
repeat
|
||||
up = up + 1
|
||||
name = debug.getupvalue(f, up)
|
||||
until name == '_ENV' or name == nil
|
||||
if name then
|
||||
debug.upvaluejoin(f, up, function() return name end, 1) -- use unique upvalue
|
||||
debug.setupvalue(f, up, t)
|
||||
end
|
||||
if f ~= 0 then return f end
|
||||
end
|
||||
|
||||
function getfenv(f)
|
||||
local f = f or 0
|
||||
f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func)
|
||||
local name, val
|
||||
local up = 0
|
||||
repeat
|
||||
up = up + 1
|
||||
name, val = debug.getupvalue(f, up)
|
||||
until name == '_ENV' or name == nil
|
||||
return val
|
||||
--- convert an array of values to strings.
|
||||
-- @param t a list-like table
|
||||
-- @param temp buffer to use, otherwise allocate
|
||||
-- @param tostr custom tostring function, called with (value,index).
|
||||
-- Otherwise use `tostring`
|
||||
-- @return the converted buffer
|
||||
function utils.array_tostring (t,temp,tostr)
|
||||
temp, tostr = temp or {}, tostr or tostring
|
||||
for i = 1,#t do
|
||||
temp[i] = tostr(t[i],i)
|
||||
end
|
||||
return temp
|
||||
end
|
||||
|
||||
|
||||
--- execute a shell command.
|
||||
-- This is a compatibility function that returns the same for Lua 5.1 and Lua 5.2
|
||||
--- execute a shell command and return the output.
|
||||
-- This function redirects the output to tempfiles and returns the content of those files.
|
||||
-- @param cmd a shell command
|
||||
-- @param bin boolean, if true, read output as binary file
|
||||
-- @return true if successful
|
||||
-- @return actual return code
|
||||
function utils.execute (cmd)
|
||||
local res1,res2,res2 = os.execute(cmd)
|
||||
if lua51 then
|
||||
return res1==0,res1
|
||||
else
|
||||
return res1,res2
|
||||
-- @return stdout output (string)
|
||||
-- @return errout output (string)
|
||||
function utils.executeex(cmd, bin)
|
||||
local mode
|
||||
local outfile = os.tmpname()
|
||||
local errfile = os.tmpname()
|
||||
|
||||
if utils.dir_separator == '\\' then
|
||||
outfile = os.getenv('TEMP')..outfile
|
||||
errfile = os.getenv('TEMP')..errfile
|
||||
end
|
||||
cmd = cmd .. [[ >"]]..outfile..[[" 2>"]]..errfile..[["]]
|
||||
|
||||
local success, retcode = utils.execute(cmd)
|
||||
local outcontent = utils.readfile(outfile, bin)
|
||||
local errcontent = utils.readfile(errfile, bin)
|
||||
os.remove(outfile)
|
||||
os.remove(errfile)
|
||||
return success, retcode, (outcontent or ""), (errcontent or "")
|
||||
end
|
||||
|
||||
if lua51 then
|
||||
function table.pack (...)
|
||||
local n = select('#',...)
|
||||
return {n=n; ...}
|
||||
end
|
||||
local sep = package.config:sub(1,1)
|
||||
function package.searchpath (mod,path)
|
||||
mod = mod:gsub('%.',sep)
|
||||
for m in path:gmatch('[^;]+') do
|
||||
local nm = m:gsub('?',mod)
|
||||
local f = io.open(nm,'r')
|
||||
if f then f:close(); return nm end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not table.pack then table.pack = _G.pack end
|
||||
if not rawget(_G,"pack") then _G.pack = table.pack end
|
||||
|
||||
--- take an arbitrary set of arguments and make into a table.
|
||||
-- This returns the table and the size; works fine for nil arguments
|
||||
-- @param ... arguments
|
||||
-- @return table
|
||||
-- @return table size
|
||||
-- @usage local t,n = utils.args(...)
|
||||
|
||||
--- 'memoize' a function (cache returned value for next call).
|
||||
-- This is useful if you have a function which is relatively expensive,
|
||||
-- but you don't know in advance what values will be required, so
|
||||
|
@ -312,49 +263,6 @@ function utils.memoize(func)
|
|||
})
|
||||
end
|
||||
|
||||
--- is the object either a function or a callable object?.
|
||||
-- @param obj Object to check.
|
||||
function utils.is_callable (obj)
|
||||
return type(obj) == 'function' or getmetatable(obj) and getmetatable(obj).__call
|
||||
end
|
||||
|
||||
--- is the object of the specified type?.
|
||||
-- If the type is a string, then use type, otherwise compare with metatable
|
||||
-- @param obj An object to check
|
||||
-- @param tp String of what type it should be
|
||||
function utils.is_type (obj,tp)
|
||||
if type(tp) == 'string' then return type(obj) == tp end
|
||||
local mt = getmetatable(obj)
|
||||
return tp == mt
|
||||
end
|
||||
|
||||
local fileMT = getmetatable(io.stdout)
|
||||
|
||||
--- a string representation of a type.
|
||||
-- For tables with metatables, we assume that the metatable has a `_name`
|
||||
-- field. Knows about Lua file objects.
|
||||
-- @param obj an object
|
||||
-- @return a string like 'number', 'table' or 'List'
|
||||
function utils.type (obj)
|
||||
local t = type(obj)
|
||||
if t == 'table' or t == 'userdata' then
|
||||
local mt = getmetatable(obj)
|
||||
if mt == fileMT then
|
||||
return 'file'
|
||||
else
|
||||
return mt._name or "unknown "..t
|
||||
end
|
||||
else
|
||||
return t
|
||||
end
|
||||
end
|
||||
|
||||
--- is this number an integer?
|
||||
-- @param x a number
|
||||
-- @raise error if x is not a number
|
||||
function utils.is_integer (x)
|
||||
return math.ceil(x)==x
|
||||
end
|
||||
|
||||
utils.stdmt = {
|
||||
List = {_name='List'}, Map = {_name='Map'},
|
||||
|
@ -366,8 +274,8 @@ local _function_factories = {}
|
|||
--- associate a function factory with a type.
|
||||
-- A function factory takes an object of the given type and
|
||||
-- returns a function for evaluating it
|
||||
-- @param mt metatable
|
||||
-- @param fun a callable that returns a function
|
||||
-- @tab mt metatable
|
||||
-- @func fun a callable that returns a function
|
||||
function utils.add_function_factory (mt,fun)
|
||||
_function_factories[mt] = fun
|
||||
end
|
||||
|
@ -383,7 +291,7 @@ local function _string_lambda(f)
|
|||
if not args then return raise 'bad string lambda' end
|
||||
end
|
||||
local fstr = 'return function('..args..') return '..body..' end'
|
||||
local fn,err = loadstring(fstr)
|
||||
local fn,err = utils.load(fstr)
|
||||
if not fn then return raise(err) end
|
||||
fn = fn()
|
||||
return fn
|
||||
|
@ -412,7 +320,6 @@ local ops
|
|||
-- @param msg optional error message
|
||||
-- @return a callable
|
||||
-- @raise if idx is not a number or if f is not callable
|
||||
-- @see utils.is_callable
|
||||
function utils.function_arg (idx,f,msg)
|
||||
utils.assert_arg(1,idx,'number')
|
||||
local tp = type(f)
|
||||
|
@ -449,7 +356,7 @@ end
|
|||
-- @param p a value
|
||||
-- @return a function such that f(x) is fn(p,x)
|
||||
-- @raise same as @{function_arg}
|
||||
-- @see pl.func.curry
|
||||
-- @see func.bind1
|
||||
function utils.bind1 (fn,p)
|
||||
fn = utils.function_arg(1,fn)
|
||||
return function(...) return fn(p,...) end
|
||||
|
@ -503,7 +410,13 @@ local err_mode = 'default'
|
|||
-- @param mode - either 'default', 'quit' or 'error'
|
||||
-- @see utils.raise
|
||||
function utils.on_error (mode)
|
||||
err_mode = mode
|
||||
if ({['default'] = 1, ['quit'] = 2, ['error'] = 3})[mode] then
|
||||
err_mode = mode
|
||||
else
|
||||
-- fail loudly
|
||||
if err_mode == 'default' then err_mode = 'error' end
|
||||
utils.raise("Bad argument expected string; 'default', 'quit', or 'error'. Got '"..tostring(mode).."'")
|
||||
end
|
||||
end
|
||||
|
||||
--- used by Penlight functions to return errors. Its global behaviour is controlled
|
||||
|
@ -517,6 +430,16 @@ function utils.raise (err)
|
|||
end
|
||||
end
|
||||
|
||||
--- is the object of the specified type?.
|
||||
-- If the type is a string, then use type, otherwise compare with metatable
|
||||
-- @param obj An object to check
|
||||
-- @param tp String of what type it should be
|
||||
function utils.is_type (obj,tp)
|
||||
if type(tp) == 'string' then return type(obj) == tp end
|
||||
local mt = getmetatable(obj)
|
||||
return tp == mt
|
||||
end
|
||||
|
||||
raise = utils.raise
|
||||
|
||||
--- load a code string or bytecode chunk.
|
||||
|
@ -528,22 +451,25 @@ raise = utils.raise
|
|||
-- @return error message (chunk is nil)
|
||||
-- @function utils.load
|
||||
|
||||
---------------
|
||||
-- Get environment of a function.
|
||||
-- With Lua 5.2, may return nil for a function with no global references!
|
||||
-- Based on code by [Sergey Rozhenko](http://lua-users.org/lists/lua-l/2010-06/msg00313.html)
|
||||
-- @param f a function or a call stack reference
|
||||
-- @function utils.getfenv
|
||||
|
||||
--- Lua 5.2 Compatible Functions
|
||||
-- @section lua52
|
||||
---------------
|
||||
-- Set environment of a function
|
||||
-- @param f a function or a call stack reference
|
||||
-- @param env a table that becomes the new environment of `f`
|
||||
-- @function utils.setfenv
|
||||
|
||||
--- pack an argument list into a table.
|
||||
-- @param ... any arguments
|
||||
-- @return a table with field n set to the length
|
||||
-- @return the length
|
||||
-- @function table.pack
|
||||
|
||||
------
|
||||
-- return the full path where a Lua module name would be matched.
|
||||
-- @param mod module name, possibly dotted
|
||||
-- @param path a path in the same form as package.path or package.cpath
|
||||
-- @see path.package_path
|
||||
-- @function package.searchpath
|
||||
--- execute a shell command.
|
||||
-- This is a compatibility function that returns the same for Lua 5.1 and Lua 5.2
|
||||
-- @param cmd a shell command
|
||||
-- @return true if successful
|
||||
-- @return actual return code
|
||||
-- @function utils.execute
|
||||
|
||||
return utils
|
||||
|
||||
|
|
|
@ -29,7 +29,8 @@
|
|||
-- Soft Dependencies: `lxp.lom` (fallback is to use basic Lua parser)
|
||||
-- @module pl.xml
|
||||
|
||||
local split = require 'pl.utils'.split
|
||||
local utils = require 'pl.utils'
|
||||
local split = utils.split;
|
||||
local t_insert = table.insert;
|
||||
local t_concat = table.concat;
|
||||
local t_remove = table.remove;
|
||||
|
@ -43,7 +44,7 @@ local ipairs = ipairs;
|
|||
local type = type;
|
||||
local next = next;
|
||||
local print = print;
|
||||
local unpack = unpack;
|
||||
local unpack = utils.unpack;
|
||||
local s_gsub = string.gsub;
|
||||
local s_char = string.char;
|
||||
local s_find = string.find;
|
||||
|
@ -159,17 +160,19 @@ function Doc:get_attribs()
|
|||
return self.attr
|
||||
end
|
||||
|
||||
local function is_text(s) return type(s) == 'string' end
|
||||
|
||||
--- function to create an element with a given tag name and a set of children.
|
||||
-- @param tag a tag name
|
||||
-- @param items either text or a table where the hash part is the attributes and the list part is the children.
|
||||
function _M.elem(tag,items)
|
||||
local s = _M.new(tag)
|
||||
if type(items) == 'string' then items = {items} end
|
||||
if is_text(items) then items = {items} end
|
||||
if _M.is_tag(items) then
|
||||
t_insert(s,items)
|
||||
elseif type(items) == 'table' then
|
||||
for k,v in pairs(items) do
|
||||
if type(k) == 'string' then
|
||||
if is_text(k) then
|
||||
s.attr[k] = v
|
||||
t_insert(s.attr,k)
|
||||
else
|
||||
|
@ -187,7 +190,7 @@ end
|
|||
function _M.tags(list)
|
||||
local ctors = {}
|
||||
local elem = _M.elem
|
||||
if type(list) == 'string' then list = split(list,'%s*,%s*') end
|
||||
if is_text(list) then list = split(list,'%s*,%s*') end
|
||||
for _,tag in ipairs(list) do
|
||||
local ctor = function(items) return _M.elem(tag,items) end
|
||||
t_insert(ctors,ctor)
|
||||
|
@ -197,6 +200,22 @@ end
|
|||
|
||||
local templ_cache = {}
|
||||
|
||||
local function template_cache (templ)
|
||||
if is_text(templ) then
|
||||
if templ_cache[templ] then
|
||||
templ = templ_cache[templ]
|
||||
else
|
||||
local str,err = templ
|
||||
templ,err = _M.parse(str,false,true)
|
||||
if not templ then return nil,err end
|
||||
templ_cache[str] = templ
|
||||
end
|
||||
elseif not _M.is_tag(templ) then
|
||||
return nil, "template is not a document"
|
||||
end
|
||||
return templ
|
||||
end
|
||||
|
||||
local function is_data(data)
|
||||
return #data == 0 or type(data[1]) ~= 'table'
|
||||
end
|
||||
|
@ -214,20 +233,13 @@ end
|
|||
-- @param data a table of name-value pairs or a list of such tables
|
||||
-- @return an XML document
|
||||
function Doc.subst(templ, data)
|
||||
local err
|
||||
if type(data) ~= 'table' or not next(data) then return nil, "data must be a non-empty table" end
|
||||
if is_data(data) then
|
||||
prepare_data(data)
|
||||
end
|
||||
if type(templ) == 'string' then
|
||||
if templ_cache[templ] then
|
||||
templ = templ_cache[templ]
|
||||
else
|
||||
local str,err = templ
|
||||
templ,err = _M.parse(str)
|
||||
if not templ then return nil,err end
|
||||
templ_cache[str] = templ
|
||||
end
|
||||
end
|
||||
templ,err = template_cache(templ)
|
||||
if err then return nil, err end
|
||||
local function _subst(item)
|
||||
return _M.clone(templ,function(s)
|
||||
return s:gsub('%$(%w+)',item)
|
||||
|
@ -292,7 +304,7 @@ end
|
|||
function Doc:matching_tags(tag, xmlns)
|
||||
xmlns = xmlns or self.attr.xmlns;
|
||||
local tags = self;
|
||||
local start_i, max_i = 1, #tags;
|
||||
local start_i, max_i, v = 1, #tags;
|
||||
return function ()
|
||||
for i=start_i,max_i do
|
||||
v = tags[i];
|
||||
|
@ -302,7 +314,7 @@ function Doc:matching_tags(tag, xmlns)
|
|||
return v;
|
||||
end
|
||||
end
|
||||
end, tags, i;
|
||||
end, tags, start_i;
|
||||
end
|
||||
|
||||
--- iterate over all child elements of a document node.
|
||||
|
@ -355,15 +367,23 @@ local function _dostring(t, buf, self, xml_escape, parentns, idn, indent, attr_i
|
|||
if indent then lf = '\n'..idn end
|
||||
if attr_indent then alf = '\n'..idn..attr_indent end
|
||||
t_insert(buf, lf.."<"..tag);
|
||||
for k, v in pairs(t.attr) do
|
||||
if type(k) ~= 'number' then -- LOM attr table has list-like part
|
||||
if s_find(k, "\1", 1, true) then
|
||||
local ns, attrk = s_match(k, "^([^\1]*)\1?(.*)$");
|
||||
nsid = nsid + 1;
|
||||
t_insert(buf, " xmlns:ns"..nsid.."='"..xml_escape(ns).."' ".."ns"..nsid..":"..attrk.."='"..xml_escape(v).."'");
|
||||
elseif not(k == "xmlns" and v == parentns) then
|
||||
t_insert(buf, alf..k.."='"..xml_escape(v).."'");
|
||||
end
|
||||
local function write_attr(k,v)
|
||||
if s_find(k, "\1", 1, true) then
|
||||
local ns, attrk = s_match(k, "^([^\1]*)\1?(.*)$");
|
||||
nsid = nsid + 1;
|
||||
t_insert(buf, " xmlns:ns"..nsid.."='"..xml_escape(ns).."' ".."ns"..nsid..":"..attrk.."='"..xml_escape(v).."'");
|
||||
elseif not(k == "xmlns" and v == parentns) then
|
||||
t_insert(buf, alf..k.."='"..xml_escape(v).."'");
|
||||
end
|
||||
end
|
||||
-- it's useful for testing to have predictable attribute ordering, if available
|
||||
if #t.attr > 0 then
|
||||
for _,k in ipairs(t.attr) do
|
||||
write_attr(k,t.attr[k])
|
||||
end
|
||||
else
|
||||
for k, v in pairs(t.attr) do
|
||||
write_attr(k,v)
|
||||
end
|
||||
end
|
||||
local len,has_children = #t;
|
||||
|
@ -391,9 +411,17 @@ end
|
|||
--- @param idn an initial indent (indents are all strings)
|
||||
--- @param indent an indent for each level
|
||||
--- @param attr_indent if given, indent each attribute pair and put on a separate line
|
||||
--- @param xml force prefacing with default or custom <?xml...>
|
||||
--- @return a string representation
|
||||
function _M.tostring(t,idn,indent, attr_indent)
|
||||
function _M.tostring(t,idn,indent, attr_indent, xml)
|
||||
local buf = {};
|
||||
if xml then
|
||||
if type(xml) == "string" then
|
||||
buf[1] = xml
|
||||
else
|
||||
buf[1] = "<?xml version='1.0'?>"
|
||||
end
|
||||
end
|
||||
_dostring(t, buf, _dostring, xml_escape, nil,idn,indent, attr_indent);
|
||||
return t_concat(buf);
|
||||
end
|
||||
|
@ -404,7 +432,7 @@ Doc.__tostring = _M.tostring
|
|||
function Doc:get_text()
|
||||
local res = {}
|
||||
for i,el in ipairs(self) do
|
||||
if type(el) == 'string' then t_insert(res,el) end
|
||||
if is_text(el) then t_insert(res,el) end
|
||||
end
|
||||
return t_concat(res);
|
||||
end
|
||||
|
@ -414,25 +442,37 @@ end
|
|||
-- @param strsubst an optional function for handling string copying which could do substitution, etc.
|
||||
function _M.clone(doc, strsubst)
|
||||
local lookup_table = {};
|
||||
local function _copy(object)
|
||||
local function _copy(object,kind,parent)
|
||||
if type(object) ~= "table" then
|
||||
if strsubst and type(object) == 'string' then return strsubst(object)
|
||||
else return object;
|
||||
if strsubst and is_text(object) then return strsubst(object,kind,parent)
|
||||
else return object
|
||||
end
|
||||
elseif lookup_table[object] then
|
||||
return lookup_table[object];
|
||||
return lookup_table[object]
|
||||
end
|
||||
local new_table = {};
|
||||
lookup_table[object] = new_table;
|
||||
for index, value in pairs(object) do
|
||||
new_table[_copy(index)] = _copy(value); -- is cloning keys much use, hm?
|
||||
lookup_table[object] = new_table
|
||||
local tag = object.tag
|
||||
new_table.tag = _copy(tag,'*TAG',parent)
|
||||
if object.attr then
|
||||
local res = {}
|
||||
for attr,value in pairs(object.attr) do
|
||||
res[attr] = _copy(value,attr,object)
|
||||
end
|
||||
new_table.attr = res
|
||||
end
|
||||
return setmetatable(new_table, getmetatable(object));
|
||||
for index = 1,#object do
|
||||
local v = _copy(object[index],'*TEXT',object)
|
||||
t_insert(new_table,v)
|
||||
end
|
||||
return setmetatable(new_table, getmetatable(object))
|
||||
end
|
||||
|
||||
return _copy(doc)
|
||||
end
|
||||
|
||||
Doc.filter = _M.clone -- also available as method
|
||||
|
||||
--- compare two documents.
|
||||
-- @param t1 any value
|
||||
-- @param t2 any value
|
||||
|
@ -464,7 +504,7 @@ end
|
|||
--- is this value a document element?
|
||||
-- @param d any value
|
||||
function _M.is_tag(d)
|
||||
return type(d) == 'table' and type(d.tag) == 'string'
|
||||
return type(d) == 'table' and is_text(d.tag)
|
||||
end
|
||||
|
||||
--- call the desired function recursively over the document.
|
||||
|
@ -481,69 +521,121 @@ function _M.walk (doc, depth_first, operation)
|
|||
if depth_first then operation(doc.tag,doc) end
|
||||
end
|
||||
|
||||
local html_empty_elements = { --lists all HTML empty (void) elements
|
||||
br = true,
|
||||
img = true,
|
||||
meta = true,
|
||||
frame = true,
|
||||
area = true,
|
||||
hr = true,
|
||||
base = true,
|
||||
col = true,
|
||||
link = true,
|
||||
input = true,
|
||||
option = true,
|
||||
param = true,
|
||||
isindex = true,
|
||||
embed = true,
|
||||
}
|
||||
|
||||
local escapes = { quot = "\"", apos = "'", lt = "<", gt = ">", amp = "&" }
|
||||
local function unescape(str) return (str:gsub( "&(%a+);", escapes)); end
|
||||
|
||||
local function parseargs(s)
|
||||
local arg = {}
|
||||
s:gsub("([%w:]+)%s*=%s*([\"'])(.-)%2", function (w, _, a)
|
||||
arg[w] = unescape(a)
|
||||
end)
|
||||
return arg
|
||||
--- Parse a well-formed HTML file as a string.
|
||||
-- Tags are case-insenstive, DOCTYPE is ignored, and empty elements can be .. empty.
|
||||
-- @param s the HTML
|
||||
function _M.parsehtml (s)
|
||||
return _M.basic_parse(s,false,true)
|
||||
end
|
||||
|
||||
--- Parse a simple XML document using a pure Lua parser based on Robero Ierusalimschy's original version.
|
||||
-- @param s the XML document to be parsed.
|
||||
-- @param all_text if true, preserves all whitespace. Otherwise only text containing non-whitespace is included.
|
||||
function _M.basic_parse(s,all_text)
|
||||
local t_insert,t_remove = table.insert,table.remove
|
||||
local s_find,s_sub = string.find,string.sub
|
||||
local stack = {}
|
||||
local top = {}
|
||||
t_insert(stack, top)
|
||||
local ni,c,label,xarg, empty
|
||||
local i, j = 1, 1
|
||||
-- we're not interested in <?xml version="1.0"?>
|
||||
local _,istart = s_find(s,'^%s*<%?[^%?]+%?>%s*')
|
||||
if istart then i = istart+1 end
|
||||
while true do
|
||||
ni,j,c,label,xarg, empty = s_find(s, "<(%/?)([%w:%-_]+)(.-)(%/?)>", i)
|
||||
if not ni then break end
|
||||
local text = s_sub(s, i, ni-1)
|
||||
if all_text or not s_find(text, "^%s*$") then
|
||||
t_insert(top, unescape(text))
|
||||
end
|
||||
if empty == "/" then -- empty element tag
|
||||
t_insert(top, setmetatable({tag=label, attr=parseargs(xarg), empty=1},Doc))
|
||||
elseif c == "" then -- start tag
|
||||
top = setmetatable({tag=label, attr=parseargs(xarg)},Doc)
|
||||
t_insert(stack, top) -- new level
|
||||
else -- end tag
|
||||
local toclose = t_remove(stack) -- remove top
|
||||
top = stack[#stack]
|
||||
if #stack < 1 then
|
||||
error("nothing to close with "..label)
|
||||
-- @param html if true, uses relaxed HTML rules for parsing
|
||||
function _M.basic_parse(s,all_text,html)
|
||||
local t_insert,t_remove = table.insert,table.remove
|
||||
local s_find,s_sub = string.find,string.sub
|
||||
local stack = {}
|
||||
local top = {}
|
||||
|
||||
local function parseargs(s)
|
||||
local arg = {}
|
||||
s:gsub("([%w:%-_]+)%s*=%s*([\"'])(.-)%2", function (w, _, a)
|
||||
if html then w = w:lower() end
|
||||
arg[w] = unescape(a)
|
||||
end)
|
||||
if html then
|
||||
s:gsub("([%w:%-_]+)%s*=%s*([^\"']+)%s*", function (w, a)
|
||||
w = w:lower()
|
||||
arg[w] = unescape(a)
|
||||
end)
|
||||
end
|
||||
if toclose.tag ~= label then
|
||||
error("trying to close "..toclose.tag.." with "..label)
|
||||
end
|
||||
t_insert(top, toclose)
|
||||
return arg
|
||||
end
|
||||
|
||||
t_insert(stack, top)
|
||||
local ni,c,label,xarg, empty, _, istart
|
||||
local i, j = 1, 1
|
||||
-- we're not interested in <?xml version="1.0"?>
|
||||
_,istart = s_find(s,'^%s*<%?[^%?]+%?>%s*')
|
||||
if not istart then -- or <!DOCTYPE ...>
|
||||
_,istart = s_find(s,'^%s*<!DOCTYPE.->%s*')
|
||||
end
|
||||
if istart then i = istart+1 end
|
||||
while true do
|
||||
ni,j,c,label,xarg, empty = s_find(s, "<([%/!]?)([%w:%-_]+)(.-)(%/?)>", i)
|
||||
if not ni then break end
|
||||
if c == "!" then -- comment
|
||||
-- case where there's no space inside comment
|
||||
if not (label:match '%-%-$' and xarg == '') then
|
||||
if xarg:match '%-%-$' then -- we've grabbed it all
|
||||
j = j - 2
|
||||
end
|
||||
-- match end of comment
|
||||
_,j = s_find(s, "-->", j, true)
|
||||
end
|
||||
else
|
||||
local text = s_sub(s, i, ni-1)
|
||||
if html then
|
||||
label = label:lower()
|
||||
if html_empty_elements[label] then empty = "/" end
|
||||
if label == 'script' then
|
||||
end
|
||||
end
|
||||
if all_text or not s_find(text, "^%s*$") then
|
||||
t_insert(top, unescape(text))
|
||||
end
|
||||
if empty == "/" then -- empty element tag
|
||||
t_insert(top, setmetatable({tag=label, attr=parseargs(xarg), empty=1},Doc))
|
||||
elseif c == "" then -- start tag
|
||||
top = setmetatable({tag=label, attr=parseargs(xarg)},Doc)
|
||||
t_insert(stack, top) -- new level
|
||||
else -- end tag
|
||||
local toclose = t_remove(stack) -- remove top
|
||||
top = stack[#stack]
|
||||
if #stack < 1 then
|
||||
error("nothing to close with "..label..':'..text)
|
||||
end
|
||||
if toclose.tag ~= label then
|
||||
error("trying to close "..toclose.tag.." with "..label.." "..text)
|
||||
end
|
||||
t_insert(top, toclose)
|
||||
end
|
||||
end
|
||||
i = j+1
|
||||
end
|
||||
local text = s_sub(s, i)
|
||||
if all_text or not s_find(text, "^%s*$") then
|
||||
t_insert(stack[#stack], unescape(text))
|
||||
end
|
||||
if #stack > 1 then
|
||||
error("unclosed "..stack[#stack].tag)
|
||||
end
|
||||
local res = stack[1]
|
||||
return type(res[1])=='string' and res[2] or res[1]
|
||||
end
|
||||
local text = s_sub(s, i)
|
||||
if all_text or not s_find(text, "^%s*$") then
|
||||
t_insert(stack[#stack], unescape(text))
|
||||
end
|
||||
if #stack > 1 then
|
||||
error("unclosed "..stack[#stack].tag)
|
||||
end
|
||||
local res = stack[1]
|
||||
return is_text(res[1]) and res[2] or res[1]
|
||||
end
|
||||
|
||||
local function empty(attr) return not attr or not next(attr) end
|
||||
local function is_text(s) return type(s) == 'string' end
|
||||
local function is_element(d) return type(d) == 'table' and d.tag ~= nil end
|
||||
|
||||
-- returns the key,value pair from a table if it has exactly one entry
|
||||
|
@ -588,11 +680,11 @@ end
|
|||
local match
|
||||
function match(d,pat,res,keep_going)
|
||||
local ret = true
|
||||
if d == nil then return false end
|
||||
if d == nil then d = '' end --return false end
|
||||
-- attribute string matching is straight equality, except if the pattern is a $ capture,
|
||||
-- which always succeeds.
|
||||
if type(d) == 'string' then
|
||||
if type(pat) ~= 'string' then return false end
|
||||
if is_text(d) then
|
||||
if not is_text(pat) then return false end
|
||||
if _M.debug then print(d,pat) end
|
||||
if pat:find '^%$' then
|
||||
return capture_attrib(res,pat,d)
|
||||
|
@ -667,9 +759,9 @@ function match(d,pat,res,keep_going)
|
|||
end
|
||||
|
||||
function Doc:match(pat)
|
||||
if is_text(pat) then
|
||||
pat = _M.parse(pat,false,true)
|
||||
end
|
||||
local err
|
||||
pat,err = template_cache(pat)
|
||||
if not pat then return nil, err end
|
||||
_M.walk(pat,false,function(_,d)
|
||||
if is_text(d[1]) and is_element(d[2]) and is_text(d[3]) and
|
||||
d[1]:find '%s*{{' and d[3]:find '}}%s*' then
|
||||
|
|
Loading…
Reference in New Issue