Updated stdlib to release 28.

Updated Penlight to 1.3.2.
Updated SubLua to 1.8.10.
master
rjpcomputing 2015-03-18 15:36:09 -04:00
parent c7cf98f6a7
commit f076d36e3c
45 changed files with 4507 additions and 3441 deletions

View File

@ -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

290
files/docs/SubLua/ldoc.css Normal file
View File

@ -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; }

610
files/docs/SubLua/ldoc.lua Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

137
files/lua/pl/compat.lua Normal file
View File

@ -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

View File

@ -253,7 +253,7 @@ local function new(env)
-- performance penalty.
if not env then
env = getfenv(2)
env = utils.getfenv(2)
end
local mt = {}

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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

View 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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -6,7 +6,7 @@
-- Does some calculations
-- -o,--offset (default 0.0) Offset to add to scaled number
-- -s,--scale (number) Scaling factor
-- &lt;number&gt; (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

View File

@ -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')

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

143
files/lua/pl/types.lua Normal file
View File

@ -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

45
files/lua/pl/url.lua Normal file
View File

@ -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

View File

@ -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

View File

@ -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