diff --git a/changes.txt b/changes.txt index 852e7c8..dd79590 100644 --- a/changes.txt +++ b/changes.txt @@ -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 diff --git a/files/clibs/SubLua.dll b/files/clibs/SubLua.dll index a5f88eb..e0b8d1a 100644 Binary files a/files/clibs/SubLua.dll and b/files/clibs/SubLua.dll differ diff --git a/files/docs/SubLua/index.html b/files/docs/SubLua/index.html index dbe14f4..2977d90 100644 --- a/files/docs/SubLua/index.html +++ b/files/docs/SubLua/index.html @@ -1,13 +1,13 @@ + - Reference - - + SubLua Reference + - +
@@ -16,69 +16,1239 @@
+
+ + + - -
- - -

Modules

- - +
    +
  • SubLua
  • +
+ + +
+ +

Module SubLua

+ +

Lua binding to SubCpp - a C++ Subversion library.

+

+ +

+

Usage:

+
    +
    local SubLua = require( "SubLua" )
    +local svn = SubLua.new( { username = "user", password = "my_password" } )
    +
+

Info:

+
    +
  • Release: 1.00 <04/21/2012>
  • +
  • License: MIT/X11
  • +
  • Author: Ryan P.
  • +
+ + +

Functions

+
- - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SubLuaLua binding to SubCpp - a C++ Subversion library.new (options)Create a new SVN client to work with.
SubLua:Cat (path, options)Output the content of specified files or URLs.
SubLua:Upgrade (path)Upgrade the working copy format of the specified path.
SubLua:GetWorkingCopyFormatVersion (path)Get the format version of the working copy at the specified path.
SubLua:Checkout (url, path, options)Checks out a working copy from a url to a path.
SubLua:Update (path, options)Updates the file or directory.
SubLua:Export (srcPath, destPath, options)Create an unversioned copy of a tree.
SubLua:Get (path, destPath, options)Retrieves the contents for a specific revision of a path and saves it to the destination file dstPath.
SubLua:Add (path, options)Schedule a working copy path for addition to the repository.
SubLua:Import (path, url, message, options)Commit an unversioned file or tree into the repository.
SubLua:MkDir (path, message)Create a new directory under version control.
SubLua:Delete (path, message, options)Remove files and directories from version control.
SubLua:Revert (path, options)Revert files and directories from version control.
SubLua:Lock (path, message, options)Locks files and directories.
SubLua:Unlock (path, options)Unlocks files and directories.
SubLua:Commit (path, message, options)Commit files or directories into repository
SubLua:DiffRevisions (path, startRevision, endRevision, summarizeCallback, options)Display the differences between two revisions of a path.
SubLua:DiffPaths (path1, path2, summarizeCallback, options)Produce a diff summary which lists the changed items between path1@revision and path2@revision2 without creating text deltas.
SubLua:Copy (srcPath, srcRevision, destPath)Copies one path to another
SubLua:Info (path, infoReceiverCallback, options)Return information about pathOrUrl
SubLua:PropGet (propertyName, path, options)Gets the value of a property on files, dirs, or revisions.
SubLua:PropSet (propName, propValue, path, options)set property in @a path no matter whether local or repository
SubLua:Cleanup (path)Recursively cleans up a local directory, finishing any + incomplete operations, removing lockfiles, etc.
ReleaseWorkingCopyLock ()Release the lock on the working copy + Does nothing and is unnecessary in svn versions 1.6 and below
+

Tables

+ + + + + + + + + + + + -
OptionsThe Options class.
SvnInfoThe SvnInfo class.
EnumsThe Subverison enums.
+
+
+ + +

Functions

+ +
+
+ + new (options) +
+
+ Create a new SVN client to work with. +
    Available listener callbacks are:
  • Notify( path, svn.action, kind, mimeType, contentState, propState, revision )
  • +
  • GetLogin( realm, username, password, maySave )
  • +
  • GetLogMessage() and it should return the log message.
  • +
  • Incomplete may not function Cancel()
+ +

Parameters:

+
    +
  • options + {table} [OPT] The table can contain key of 'username' for the username, 'password' for the password, 'no_auth_cache' to control if the is saved, and 'listener' which is a table of the listeners you want to overwride.
  • +
+ +

Returns:

+
    + + {table} The SubLua object. +
+
+
+ + SubLua:Cat (path, options) +
+
+ Output the content of specified files or URLs. + +

Parameters:

+
    +
  • path + {string} The path or URL to output.
  • +
  • options + {table} [OPT] Options table. Possible Options: peg, revision
  • +
+ +

Returns:

+
    + + {string} String of the contents of @path. +
+ + + + +
+
+ + SubLua:Upgrade (path) +
+
+ Upgrade the working copy format of the specified path. + +

Parameters:

+
    +
  • path + {string} The path to a working copy to upgrade.
  • +
+ + + + + +
+
+ + SubLua:GetWorkingCopyFormatVersion (path) +
+
+ Get the format version of the working copy at the specified path. + +

Parameters:

+
    +
  • path + {string} The absolute path to a working copy to get the format for.
  • +
+ +

Returns:

+
    + + The format version of the working copy specified (eg 29 for Subversion 1.7.13 and 31 for Subversion 1.8.0) +
+ + + + +
+
+ + SubLua:Checkout (url, path, options) +
+
+ Checks out a working copy from a url to a path. + +

Parameters:

+
    +
  • url + {string} The URL to check out.
  • +
  • path + {string} The local path to check out to.
  • +
  • options + {table} [OPT] Options table. Possible Options: peg, revision, depth, ignore_externals, force
  • +
+ +

Returns:

+
    + + Revision of the checkout. +
+ + +

see also:

+ + + +
+
+ + SubLua:Update (path, options) +
+
+ Updates the file or directory. + +

Parameters:

+
    +
  • path + {string} The path or URL to output.
  • +
  • options + {table} [OPT] Options table. Possible Options: revision, depth, set_depth, ignore_externals, force
  • +
+ +

Returns:

+
    + + Revision of the checkout. +
+ + +

see also:

+ + + +
+
+ + SubLua:Export (srcPath, destPath, options) +
+
+ Create an unversioned copy of a tree. + +

Parameters:

+
    +
  • srcPath + {string} The source path or URL to export from.
  • +
  • destPath + {string} The destination path to export to.
  • +
  • options + {table} [OPT] Options table. Possible Options: peg, revision, force, ignore_externals, depth, native_eol
  • +
+ +

Returns:

+
    + + Revision of the Export. +
+ + +

see also:

+ + + +
+
+ + SubLua:Get (path, destPath, options) +
+
+ Retrieves the contents for a specific revision of a path and saves it to the destination file dstPath.

+ +

If destPath is empty (""), then this path will be constructed from the temporary directory on this system + and the filename in path. destPath will still have the file extension from path and uniqueness of the + temporary filename will be ensured. + +

Parameters:

+
    +
  • path + {string} path or url
  • +
  • destPath + {string} destination path
  • +
  • options + {table} [OPT] Options table. Possible Options: peg, revision
  • +
+ +

Returns:

+
    + + {string} the destPath or the temp file path if destPath was an empty string +
+ + +

see also:

+ + + +
+
+ + SubLua:Add (path, options) +
+
+ Schedule a working copy path for addition to the repository. Adds a file to the repository. + +

Parameters:

+
    +
  • path + {string} The path to add to the working copy. Path's parent must be under revision control already.
  • +
  • options + {table} [OPT] Options table. Possible Options: depth, force, no_ignore, add_parents
  • +
+ + + +

see also:

+ + + +
+
+ + SubLua:Import (path, url, message, options) +
+
+ Commit an unversioned file or tree into the repository. Parent directories are created as necessary in the repository. + +

Parameters:

+
    +
  • path + {string} The path to add to the working copy. Path's parent must be under revision control already.
  • +
  • url + {string} The URL to check out.
  • +
  • message + {string} [OPT] The commit message.
  • +
  • options + {table} [OPT] Options table. Possible Options: depth, no_ignore, auto_props
  • +
+ + + +

see also:

+ + + +
+
+ + SubLua:MkDir (path, message) +
+
+ Create a new directory under version control. + +

Parameters:

+
    +
  • path + {string}/{table} The path(s) or URL(s) to output. If it is a local path then the directory is created and + scheduled for addition upon the next commit. If it is a URL then the commit is immediate.
  • +
  • message + {string} [OPT] Log message as a string.
  • +
+ + + + + +
+
+ + SubLua:Delete (path, message, options) +
+
+ Remove files and directories from version control. + +

Parameters:

+
    +
  • path + {string}/{table} The path(s) or URL(s) to remove. If it is a local path(s) then the paths are scheduled for + deletion upon the next commit. If it is a URL(s) then they are deleted immediatly.
  • +
  • message + {string} [OPT] Log message.
  • +
  • options + {table} [OPT] Options table. Possible Options: force, keep_local
  • +
+ + + +

see also:

+ + + +
+
+ + SubLua:Revert (path, options) +
+
+ Revert files and directories from version control. + +

Parameters:

+
    +
  • path + {string}/{table} The path(s) or URL(s) to revert.
  • +
  • options + {table} [OPT] Options table. Possible Options: depth
  • +
+ + + +

see also:

+ + + +
+
+ + SubLua:Lock (path, message, options) +
+
+ Locks files and directories. + +

Parameters:

+
    +
  • path + {string}/{table} The path(s) or URL(s) to lock.
  • +
  • message + {string} [OPT] The message for the lock.
  • +
  • options + {table} [OPT] Options table. Possible Options: force
  • +
+ + + +

see also:

+ + + +
+
+ + SubLua:Unlock (path, options) +
+
+ Unlocks files and directories. + +

Parameters:

+
    +
  • path + {string}/{table} The path(s) or URL(s) to unlock.
  • +
  • options + {table} [OPT] Options table. Possible Options: force
  • +
+ + + +

see also:

+ + + +
+
+ + SubLua:Commit (path, message, options) +
+
+ Commit files or directories into repository + +

Parameters:

+
    +
  • path + {string}/{table} The path(s) to commit.
  • +
  • message + {string} The commit message.
  • +
  • options + {table} [OPT] Options table. Possible Options: depth, keep_locks, keep_changelists
  • +
+ + + +

see also:

+ + + +
+
+ + SubLua:DiffRevisions (path, startRevision, endRevision, summarizeCallback, options) +
+
+ Display the differences between two revisions of a path. + The function may report false positives if notice_ancestry is true, since a file might have been modified between two revisions, + but still have the same contents. + If summarizeCallback and options.summarize = true then the results passed to the summarizeCallback. + If summarizeCallback is not provided, but options.summarize = true then an array of summaryInfo is returned. + +

Parameters:

+
    +
  • path + {string} The path to diff. Can be either a working-copy path or a URL.
  • +
  • startRevision + {string}/{number} The start revisions to check.
  • +
  • endRevision + {string}/{number} The end revision to check.
  • +
  • summarizeCallback + +

    {function} [OPT] Called for every difference found when . + For each invocation it passes summaryInfo table that has keys path, summarize_kind, prop_changed, and node_kind. ??Return true from the summarizeCallback() to have the function continue, if you return false it will stop the operation and throw an error.?? + summarize_kind available values are found in the Enum section under the summarize.kind. node_kind available values are found in the Enums section under node.kind + The prototype for the callback is

    +
    function SubLua:MyDiffSummarizeReceiver( summaryInfo )
    +    print( summaryInfo.path, summaryInfo.summarize_kind, summaryInfo.prop_changed, summaryInfo.node_kind )
    +    return true
    +end
    +
    +
  • +
  • options + {table} [OPT] Options table. Possible Options: depth, summarize, peg, notice_ancestry, no_diff_deleted, ignore_content_type
  • +
+ +

Returns:

+
    + + Normally the differences are returned as a {string} of file differences. +
+ + +

see also:

+ + + +
+
+ + SubLua:DiffPaths (path1, path2, summarizeCallback, options) +
+
+ Produce a diff summary which lists the changed items between path1@revision and path2@revision2 without creating text deltas. + The function may report false positives if notice_ancestry is true, since a file might have been modified between two revisions, + but still have the same contents. + If summarizeCallback and options.summarize = true then the results passed to the summarizeCallback. + If summarizeCallback is not provided, but options.summarize = true then an array of summaryInfo is returned. + +

Parameters:

+
    +
  • path1 + {string} The first path to diff. Can be either a working-copy path or a URL.
  • +
  • path2 + {string} The second path to diff. Can be either a working-copy path or a URL.
  • +
  • summarizeCallback + +

    {function} Called for every difference found. + For each invocation it passes summaryInfo table that has keys path, summarize_kind, prop_changed, and node_kind. ??Return true from the summarizeCallback() to have the function continue, if you return false it will stop the operation and throw an error.?? + summarize_kind available values are found in the Enum section under the summarize.kind. node_kind available values are found in the Enums section under node.kind + The prototype for the callback is

    +
    function SubLua:MyDiffSummarizeReceiver( summaryInfo )
    +    print( summaryInfo.path, summaryInfo.summarize_kind, summaryInfo.prop_changed, summaryInfo.node_kind )
    +    return true
    +end
    +
    +
  • +
  • options + {table} [OPT] Options table. Possible Options: depth, revision, revision2, summarize, peg, notice_ancestry.
  • +
+ +

Returns:

+
    + + Normally the differences are returned as a {string} of file differences. +
+ + +

see also:

+ + + +
+
+ + SubLua:Copy (srcPath, srcRevision, destPath) +
+
+ Copies one path to another + +

Parameters:

+
    +
  • srcPath + {string} The source path to copy from.
  • +
  • srcRevision + {string} The source revision to copy from.
  • +
  • destPath + {string} The destination path to copy to.
  • +
+ + + + + +
+
+ + SubLua:Info (path, infoReceiverCallback, options) +
+
+ Return information about pathOrUrl + +

Parameters:

+
    +
  • path + {string}/{table} The path or URL to get the specified information from.
  • +
  • infoReceiverCallback + +

    {function} [OPT] Called everytime a file is found during receiving information. For each invocation it passes path with the information present in info. Return true from the InfoReceiver() to have the function contiue, if you return false it will stop the operation and throw an error. + The prototype for the callback is

    +
    function SubLua:MyInfoReceiver( path, info )
    +    print( path, info )
    +    return true
    +end
    +
    +
  • +
  • options + {table} [OPT] Options table. Possible Options: peg, revision, depth
  • +
+ + + +

see also:

+ + + +
+
+ + SubLua:PropGet (propertyName, path, options) +
+
+ Gets the value of a property on files, dirs, or revisions. + property table. In the property table there are keys of property names to their value, both as strings. + +

Parameters:

+
    +
  • propertyName + {string} The property to get from path.
  • +
  • path + {string}/{table} The path or URL to get the specified property from.
  • +
  • options + {table} [OPT] Options table. Possible Options: revision, depth
  • +
+ +

Returns:

+
    + + {table} Table of properties. The list will contain tables where the keys are the path, and the values are a +
+ + +

see also:

+ + + +
+
+ + SubLua:PropSet (propName, propValue, path, options) +
+
+ set property in @a path no matter whether local or repository + +

Parameters:

+
    +
  • propName + {string} The property name to set.
  • +
  • propValue + {string} The value of the property to set
  • +
  • path + {string} Path where property to be set
  • +
  • options + {table} Options table. Possible Options: depth
  • +
+ + + +

see also:

+ + + +
+
+ + SubLua:Cleanup (path) +
+
+ Recursively cleans up a local directory, finishing any + incomplete operations, removing lockfiles, etc. + +

Parameters:

+
    +
  • path + {string} Path to a local directory.
  • +
+ + + + + +
+
+ + ReleaseWorkingCopyLock () +
+
+ Release the lock on the working copy + Does nothing and is unnecessary in svn versions 1.6 and below + + + + + + +
+
+

Tables

+ +
+
+ + Options +
+
+ The Options class. + +

Fields:

+
    +
  • revision + the revision you want to update to; defaults to "HEAD".
  • +
  • revision2 + the revision for path2 when finding the differences in paths; defaults to "HEAD".
  • +
  • peg + the peg revision you want to update using; defaults to "HEAD".
  • +
  • depth + limit operation by depth ARG ('empty', 'files', 'immediates', or 'infinity'); defaults to unknown.
  • +
  • force + force operation to run; defaults to false.
  • +
  • ignore_externals + set to ignore the externals; defaults to false.
  • +
  • ignore_keywords + set to ignore the keyword substitution; defaults to false.
  • +
  • no_ignore + set to disregard default and svn:ignore property ignores; defaults to false.
  • +
  • native_eol + use a different EOL marker than the standard system marker for files with the svn:eol-style property set to 'native'. ARG may be one of 'LF', 'CR', 'CRLF'; defaults to NULL (no change).
  • +
  • set_depth + set new working copy depth to depth. If depth is not specified then this does nothing; defaults to false.
  • +
  • parents + If true, create any non-existent parent directories also; defaults to false.
  • +
  • add_parents + If true, recurse up path's directory and look for a versioned directory. If found, add all intermediate paths between it and path; defaults to false.
  • +
  • keep_changelists + After the commit completes successfully, remove changelist associations from the targets, unless keep_changelists is true; defaults to false.
  • +
  • keep_locks + Unlock paths in the repository, unless keep_locks is true; defaults to false.
  • +
  • auto_props + Enable automatic properties; defaults to true.
  • +
  • ignore_unknown_node_types + Ignore files of which the node type is unknown, such as device files and pipes.; defaults to false.
  • +
  • keep_local + if true then the paths will not be removed from the working copy, only scheduled for removal from the repository. Once the scheduled deletion is committed, they will appear as unversioned paths in the working copy; defaults to false.
  • +
  • no_diff_deleting + Do not print differences for deleted files; defaults to false.
  • +
  • notice_ancestry + Notice ancestry when calculating differences; defaults to false.
  • +
  • ignore_content_type + if true, Diff output will be generated for binary files, in which case diffs will be shown regardless of the content types; defaults to false.
  • +
  • summarize + if true, during a diff operation only produce a summary which lists the changed items without creating text deltas; defaults to false.
  • +
+ + + + + +
+
+ + SvnInfo +
+
+ The SvnInfo class. + +

Fields:

+
    +
  • isValid + Is it valid?
  • +
  • url + The url
  • +
  • revision + The revision
  • +
  • kind + The kind
  • +
  • rootUrl + The root URL
  • +
  • uuid + The uuid
  • +
  • lastChangedRevision + Last changed revision
  • +
  • lastChangedDate + Last changed date
  • +
  • lastChangedAuthor + Last changed author
  • +
  • lock + Lock
  • +
  • hasWCInfo + Has working copy info?
  • +
  • schedule + The schedule
  • +
  • copyFromUrl + Copy from URL
  • +
  • copyFromRev + Copy from revision
  • +
  • textTime + Time in text
  • +
  • propTime + Properties time
  • +
  • checksum + The checksum
  • +
  • conflictOld + Conflict old
  • +
  • conflictNew + Conflict new
  • +
  • conflictWrk + Conflict working
  • +
  • propRejectFile + Prop reject file
  • +
  • changelist + The changelist
  • +
  • depth + Depth
  • +
  • workingSize + Working size
  • +
  • size + Size
  • +
  • size64 + Size64
  • +
  • workingSize64 + Working Size64
  • +
  • treeConflict + the TreeConflict
  • +
+ + + + + +
+
+ + Enums +
+
+ The Subverison enums. These are part of the SubLua object returned after calling new(). + +

Fields:

+
    +
  • wc.notify.action.add + Adding a path to revision control.
  • +
  • wc.notify.action.copy + Copying a versioned path.
  • +
  • wc.notify.action.delete + Deleting a versioned path.
  • +
  • wc.notify.action.restore + Restoring a missing path from the pristine text-base.
  • +
  • wc.notify.action.revert + Reverting a modified path.
  • +
  • wc.notify.action.failed_revert + A revert operation has failed.
  • +
  • wc.notify.action.resolved + Resolving a conflict.
  • +
  • wc.notify.action.skip + Skipping a path.
  • +
  • wc.notify.action.update_delete + Got a delete in an update.
  • +
  • wc.notify.action.update_add + Got an add in an update.
  • +
  • wc.notify.action.update_update + Got any other action in an update.
  • +
  • wc.notify.action.update_completed + The last notification in an update (including updates of externals).
  • +
  • wc.notify.action.update_external + Updating an external module.
  • +
  • wc.notify.action.status_completed + The last notification in a status (including status on externals).
  • +
  • wc.notify.action.status_external + Running status on an external module.
  • +
  • wc.notify.action.commit_modified + Committing a modification.
  • +
  • wc.notify.action.commit_added + Committing an addition.
  • +
  • wc.notify.action.commit_deleted + Committing a deletion.
  • +
  • wc.notify.action.commit_replaced + Committing a replacement.
  • +
  • wc.notify.action.commit_postfix_txdelta + Transmitting post-fix text-delta data for a file.
  • +
  • wc.notify.action.blame_revision + Processed a single revision's blame.
  • +
  • wc.notify.action.locked + Locking a path. New in 1.2.
  • +
  • wc.notify.action.unlocked + Unlocking a path. New in 1.2.
  • +
  • wc.notify.action.failed_lock + Failed to lock a path. New in 1.2.
  • +
  • wc.notify.action.failed_unlock + Failed to unlock a path. New in 1.2.
  • +
  • wc.notify.action.exists + Tried adding a path that already exists. New in 1.5.
  • +
  • wc.notify.action.changelist_set + Changelist name set. New in 1.5.
  • +
  • wc.notify.action.changelist_clear + Changelist name cleared. New in 1.5.
  • +
  • wc.notify.action.changelist_moved + Warn user that a path has moved from one changelist to another. New in 1.5. Deprecated: As of 1.7, separate clear and set notifications are sent.
  • +
  • wc.notify.action.merge_begin + A merge operation (to path) has begun.
  • +
  • wc.notify.action.foreign_merge_begin + A merge operation (to path) from a foreign repository has begun.
  • +
  • wc.notify.action.update_replace + Replace notification. New in 1.5.
  • +
  • wc.notify.action.property_added + Property added. New in 1.6.
  • +
  • wc.notify.action.property_modified + Property updated. New in 1.6.
  • +
  • wc.notify.action.property_deleted + Property deleted. New in 1.6.
  • +
  • wc.notify.action.property_deleted_nonexistentNonexistent + property deleted. New in 1.6.
  • +
  • wc.notify.action.revprop_set + Revprop set. New in 1.6.
  • +
  • wc.notify.action.revprop_deleted + Revprop deleted. New in 1.6.
  • +
  • wc.notify.action.merge_completed + The last notification in a merge. New in 1.6.
  • +
  • wc.notify.action.tree_conflict + The path is a tree-conflict victim of the intended action (not a persistent tree-conflict from an earlier operation, but this operation caused the tree-conflict). New in 1.6.
  • +
  • wc.notify.action.failed_external + The path is a subdirectory referenced in an externals definition which is unable to be operated on. New in 1.6.
  • +
  • wc.notify.action.update_started + Starting an update operation. New in 1.7.
  • +
  • wc.notify.action.update_skip_obstruction + An update tried to add a file or directory at a path where a separate working copy was found. New in 1.7.
  • +
  • wc.notify.action.update_skip_working_only + An explicit update tried to update a file or directory that doesn't live in the repository and can't be brought in. New in 1.7.
  • +
  • wc.notify.action.update_skip_access_denied + An update tried to update a file or directory to which access could not be obtained. New in 1.7.
  • +
  • wc.notify.action.update_external_removed + An update operation removed an external working copy. New in 1.7.
  • +
  • wc.notify.action.update_shadowed_add + A node below an existing node was added during update. New in 1.7.
  • +
  • wc.notify.action.update_shadowed_update + A node below an exising node was updated during update. New in 1.7.
  • +
  • wc.notify.action.update_shadowed_delete + A node below an existing node was deleted during update. New in 1.7.
  • +
  • wc.notify.action.merge_record_info + The mergeinfo on path was updated. New in 1.7.
  • +
  • wc.notify.action.upgraded_path + An working copy directory was upgraded to the latest format. New in 1.7.
  • +
  • wc.notify.action.merge_record_info_begin + Mergeinfo describing a merge was recorded. New in 1.7.
  • +
  • wc.notify.action.merge_elide_info + Mergeinfo was removed due to elision. New in 1.7.
  • +
  • wc.notify.action.patch + A file in the working copy was patched. New in 1.7.
  • +
  • wc.notify.action.patch_applied_hunk + A hunk from a patch was applied. New in 1.7.
  • +
  • wc.notify.action.patch_rejected_hunk + A hunk from a patch was rejected. New in 1.7.
  • +
  • wc.notify.action.patch_hunk_already_applied + A hunk from a patch was found to already be applied. New in 1.7.
  • +
  • wc.notify.action.commit_copied + Committing a non-overwriting copy (path is the target of the copy, not the source). New in 1.7.
  • +
  • wc.notify.action.commit_copied_replaced + Committing an overwriting (replace) copy (path is the target of the copy, not the source). New in 1.7.
  • +
  • wc.notify.action.url_redirect + The server has instructed the client to follow a URL redirection. New in 1.7.
  • +
  • wc.notify.action.path_nonexistent + The operation was attempted on a path which doesn't exist. New in 1.7.
  • +
  • wc.notify.action.exclude + Removing a path by excluding it. New in 1.7.
  • +
  • wc.notify.action.failed_conflict + Operation failed because the node remains in conflict. New in 1.7.
  • +
  • wc.notify.action.failed_missing + Operation failed because an added node is missing. New in 1.7.
  • +
  • wc.notify.action.failed_out_of_date + Operation failed because a node is out of date. New in 1.7.
  • +
  • wc.notify.action.failed_no_parent + Operation failed because an added parent is not selected. New in 1.7.
  • +
  • wc.notify.action.failed_locked + Operation failed because a node is locked by another user and/or working copy. New in 1.7.
  • +
  • wc.notify.action.failed_forbidden_by_server + Operation failed because the operation was forbidden by the server. New in 1.7.
  • +
  • wc.notify.action.skip_conflicted + The operation skipped the path because it was conflicted. New in 1.7.
  • +
  • wc.notify.state.unknown + Notifier doesn't know or isn't saying.
  • +
  • wc.notify.state.unchanged + The state did not change.
  • +
  • wc.notify.state.missing + The item wasn't present.
  • +
  • wc.notify.state.obstructed + An unversioned item obstructed work.
  • +
  • wc.notify.state.changed + Pristine state was modified.
  • +
  • wc.notify.state.merged + Modified state had mods merged in.
  • +
  • wc.notify.state.conflicted + Modified state got conflicting mods.
  • +
  • wc.notify.state.source_missing + The source to copy the file from is missing. New in 1.7
  • +
  • wc.schedule.normal + Nothing special here.
  • +
  • wc.schedule.add + Slated for addition.
  • +
  • wc.schedule.delete + Slated for deletion.
  • +
  • wc.schedule.replace + Slated for replacement (delete + add)
  • +
  • wc.conflict.action.edit + attempting to change text or props.
  • +
  • wc.conflict.action.add + attempting to add object.
  • +
  • wc.conflict.action.delete + attempting to delete object.
  • +
  • wc.conflict.action.replace + attempting to replace object. New in 1.7
  • +
  • wc.conflict.reason.edited + Local edits are already present.
  • +
  • wc.conflict.reason.obstructed + Another object is in the way.
  • +
  • wc.conflict.reason.deleted + Object is already schedule-delete.
  • +
  • wc.conflict.reason.missing + Object is unknown or missing.
  • +
  • wc.conflict.reason.unversioned + Object is unversioned.
  • +
  • wc.conflict.reason.added + Object is already added or schedule-add.
  • +
  • wc.conflict.reason.replaced + Object is already replaced. New in 1.7
  • +
  • wc.conflict.kind.text + textual conflict (on a file)
  • +
  • wc.conflict.kind.property + property conflict (on a file or dir)
  • +
  • wc.conflict.kind.tree + tree conflict (on a dir)
  • +
  • wc.operation.none + No user operation exposed a conflict.
  • +
  • wc.operation.update + User operation 'update' exposed a conflict.
  • +
  • wc.operation.switch + User operation 'switch' exposed a conflict.
  • +
  • wc.operation.merge + User operation 'merge' exposed a conflict.
  • +
  • node.kind.none + Absent node in the Subversion filesystem.
  • +
  • node.kind.file + Regular file node in the Subversion filesystem.
  • +
  • node.kind.dir + Directory node in the Subversion filesystem.
  • +
  • node.kind.unknown + "something's here, but we don't know what" node in the Subversion filesystem.
  • +
  • node.action.change + Changed "action" attached to nodes in the dumpfile.
  • +
  • node.action.add + Added "action" attached to nodes in the dumpfile.
  • +
  • node.action.delete + Deleted "action" attached to nodes in the dumpfile.
  • +
  • node.action.replace + Replaced "action" attached to nodes in the dumpfile.
  • +
  • depth.unknown + Depth undetermined or ignored. In some contexts, this means the client should choose an appropriate default depth. The server will generally treat it as depth.infinity.
  • +
  • depth.exclude + Exclude (i.e., don't descend into) directory D.
  • +
  • depth.empty + Just the named directory D, no entries. Updates will not pull in any files or subdirectories not already present.
  • +
  • depth.files + D + its file children, but not subdirs. Updates will pull in any files not already present, but not subdirectories.
  • +
  • depth.immediates + D + immediate children (D and its entries). Updates will pull in any files or subdirectories not already present; those subdirectories' this_dir entries will have depth-empty.
  • +
  • depth.infinity + D + all descendants (full recursion from D). Updates will pull in any files or subdirectories not already present; those subdirectories' this_dir entries will have depth-infinity. Equivalent to the pre-1.5 default update behavior.
  • +
  • recurse.kind.nonrecursive + Indicates recursion is NOT needed.
  • +
  • recurse.kind.recursive + Indicates recursion is needed.
  • +
  • summarize.kind.normal + An item with no text modifications. @see SubLua:DiffRevisions @see SubLua:DiffPaths
  • +
  • summarize.kind.added + An added item. @see SubLua:DiffRevisions @see SubLua:DiffPaths
  • +
  • summarize.kind.modified + An item with text modifications. @see SubLua:DiffRevisions @see SubLua:DiffPaths
  • +
  • summarize.kind.deleted + A deleted item. @see SubLua:DiffRevisions @see SubLua:DiffPaths
  • +
+ + + + + +
+
+
-
-
-

Valid XHTML 1.0!

+generated by LDoc 1.3
- -
+ diff --git a/files/docs/SubLua/ldoc.css b/files/docs/SubLua/ldoc.css new file mode 100644 index 0000000..4e3ea4e --- /dev/null +++ b/files/docs/SubLua/ldoc.css @@ -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; } diff --git a/files/docs/SubLua/ldoc.lua b/files/docs/SubLua/ldoc.lua new file mode 100644 index 0000000..8fef574 --- /dev/null +++ b/files/docs/SubLua/ldoc.lua @@ -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 + (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 + + diff --git a/files/docs/SubLua/luadoc.css b/files/docs/SubLua/luadoc.css deleted file mode 100644 index bc0f98a..0000000 --- a/files/docs/SubLua/luadoc.css +++ /dev/null @@ -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 - diff --git a/files/docs/SubLua/modules/SubLua.html b/files/docs/SubLua/modules/SubLua.html deleted file mode 100644 index ae2ca1b..0000000 --- a/files/docs/SubLua/modules/SubLua.html +++ /dev/null @@ -1,1527 +0,0 @@ - - - - Reference - - - - - -
- -
- -
-
-
- -
- - - -
- -

Module SubLua

- -

Lua binding to SubCpp - a C++ Subversion library.

- -

Author: - - - - -
Ryan P.
-

- - - -

Release: 1.00 <04/21/2010>

- - - -

Functions

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SubLua.ReleaseWorkingCopyLock ()Release the lock on the working copy Does nothing and is unnecessary in svn versions 1.6 and below
SubLua:Add (path, options)Schedule a working copy path for addition to the repository.
SubLua:Cat (path, options)Output the content of specified files or URLs.
SubLua:Checkout (url, path, options)Checks out a working copy from a url to a path.
SubLua:Cleanup (path)Recursively cleans up a local directory, finishing any incomplete operations, removing lockfiles, etc.
SubLua:Commit (path, message, options)Commit files or directories into repository
SubLua:Copy (srcPath, srcRevision, destPath)Copies one path to another
SubLua:Delete (path, options)Remove files and directories from version control.
SubLua:Diff (tmpPath, path, revision1, revision2, options)Display the differences between two revisions or paths.
SubLua:Export (srcPath, destPath, options)Create an unversioned copy of a tree.
SubLua:Get (path, destPath, options)Retrieves the contents for a specific revision of a path and saves it to the destination file dstPath.
SubLua:Import (path, url, message, options)Commit an unversioned file or tree into the repository.
SubLua:Info (path, infoReceiverCallback, options)Return information about pathOrUrl
SubLua:Lock (path, options)Locks files and directories.
SubLua:MkDir (path, message)Create a new directory under version control.
SubLua:PropGet (propertyName, path, options)Gets the value of a property on files, dirs, or revisions.
SubLua:PropSet (propName, propValue, path, options)
SubLua:Revert (path, options)Revert files and directories from version control.
SubLua:Unlock (path, options)Unlocks files and directories.
SubLua:Update (path, options)Updates the file or directory.
- - - - -

Tables

- - - - - - - - - - - - - - - - - -
EnumsThe Subverison enums.
OptionsThe Options class.
SvnInfoThe SvnInfo class.
- - - -
-
- - - -

Functions

-
- - - -
SubLua.ReleaseWorkingCopyLock ()
-
-Release the lock on the working copy Does nothing and is unnecessary in svn versions 1.6 and below - - - - - - - - - -
- - - - -
SubLua:Add (path, options)
-
-Schedule a working copy path for addition to the repository. Adds a file to the repository. - - -

Parameters

-
    - -
  • - path: {string} The path to add to the working copy. Path's parent must be under revision control already. -
  • - -
  • - options: {table} [OPT] Options table. Possible Options: depth, force, no_ignore, add_parents -
  • - -
- - - - - - - - -

See also:

- - -
- - - - -
SubLua:Cat (path, options)
-
-Output the content of specified files or URLs. - - -

Parameters

-
    - -
  • - path: {string} The path or URL to output. -
  • - -
  • - options: {table} [OPT] Options table. Possible Options: peg, revision -
  • - -
- - - - - - -

Return value:

-{string} String of the contents of `@path`. - - - -
- - - - -
SubLua:Checkout (url, path, options)
-
-Checks out a working copy from a url to a path. - - -

Parameters

-
    - -
  • - url: {string} The URL to check out. -
  • - -
  • - path: {string} The local path to check out to. -
  • - -
  • - options: {table} [OPT] Options table. Possible Options: peg, revision, depth, ignore_externals, force -
  • - -
- - - - - - -

Return value:

-Revision of the checkout. - - - -

See also:

- - -
- - - - -
SubLua:Cleanup (path)
-
-Recursively cleans up a local directory, finishing any incomplete operations, removing lockfiles, etc. - - -

Parameters

-
    - -
  • - path: {string} Path to a local directory. -
  • - -
- - - - - - - - -
- - - - -
SubLua:Commit (path, message, options)
-
-Commit files or directories into repository - - -

Parameters

-
    - -
  • - path: {string}/{table} The path(s) to commit. -
  • - -
  • - message: {string} The commit message. -
  • - -
  • - options: {table} [OPT] Options table. Possible Options: depth, keep_locks, keep_changelists -
  • - -
- - - - - - - - -

See also:

- - -
- - - - -
SubLua:Copy (srcPath, srcRevision, destPath)
-
-Copies one path to another - - -

Parameters

-
    - -
  • - srcPath: {string} The source path to copy from. -
  • - -
  • - srcRevision: {string} The source revision to copy from. -
  • - -
  • - destPath: {string} The destination path to copy to. -
  • - -
- - - - - - - - -
- - - - -
SubLua:Delete (path, options)
-
-Remove files and directories from version control. - - -

Parameters

-
    - -
  • - path: {string}/{table} The path(s) or URL(s) to remove. If it is a local path(s) then the paths are scheduled for deletion upon the next commit. If it is a URL(s) then they are deleted immediatly. -
  • - -
  • - options: {table} [OPT] Options table. Possible Options: force, keep_local -
  • - -
- - - - - - - - -

See also:

- - -
- - - - -
SubLua:Diff (tmpPath, path, revision1, revision2, options)
-
-Display the differences between two revisions or paths. - - -

Parameters

-
    - -
  • - tmpPath: {string} The first path to diff. -
  • - -
  • - path: {string} The second path to diff. -
  • - -
  • - revision1: {string}/{number} [OPT] The revision of the first file to diff. -
  • - -
  • - revision2: {string}/{number} [OPT] The revision of the second file to diff. -
  • - -
  • - options: {table} [OPT] Options table. Possible Options: depth, notice_ancestry, no_diff_deleted -
  • - -
- - - - - - - - -

See also:

- - -
- - - - -
SubLua:Export (srcPath, destPath, options)
-
-Create an unversioned copy of a tree. - - -

Parameters

-
    - -
  • - srcPath: {string} The source path or URL to export from. -
  • - -
  • - destPath: {string} The destination path to export to. -
  • - -
  • - options: {table} [OPT] Options table. Possible Options: peg, revision, force, ignore_externals, depth, native_eol -
  • - -
- - - - - - -

Return value:

-Revision of the Export. - - - -

See also:

- - -
- - - - -
SubLua:Get (path, destPath, options)
-
-Retrieves the contents for a specific revision of a path and saves it to the destination file dstPath. If is empty (""), then this path will be constructed from the temporary directory on this system and the filename in . will still have the file extension from and uniqueness of the temporary filename will be ensured. - - -

Parameters

-
    - -
  • - path: {string} path or url -
  • - -
  • - destPath: {string} destination path -
  • - -
  • - options: {table} [OPT] Options table. Possible Options: peg, revision -
  • - -
- - - - - - -

Return value:

-{string} the destPath or the temp file path if was an empty string - - - -

See also:

- - -
- - - - -
SubLua:Import (path, url, message, options)
-
-Commit an unversioned file or tree into the repository. Parent directories are created as necessary in the repository. - - -

Parameters

-
    - -
  • - path: {string} The path to add to the working copy. Path's parent must be under revision control already. -
  • - -
  • - url: {string} The URL to check out. -
  • - -
  • - message: {string} [OPT] The commit message. -
  • - -
  • - options: {table} [OPT] Options table. Possible Options: depth, no_ignore, auto_props -
  • - -
- - - - - - - - -

See also:

- - -
- - - - -
SubLua:Info (path, infoReceiverCallback, options)
-
-Return information about pathOrUrl - - -

Parameters

-
    - -
  • - path: {string}/{table} The path or URL to get the specified information from. -
  • - -
  • - infoReceiverCallback: {function} [OPT] Called everytime a file is found during receiving information. For each invocation it passes path with the information present in info. Return true from the InfoReceiver() to have the function contiue, if you return false it will stop the operation and throw an error.
    The prototype for the callback is

    function SubLua:MyInfoReceiver( path, info )
            print( path, info )
            return true
    end -
  • - -
  • - options: {table} [OPT] Options table. Possible Options: peg, revision, depth -
  • - -
- - - - - - - - -

See also:

- - -
- - - - -
SubLua:Lock (path, options)
-
-Locks files and directories. - - -

Parameters

-
    - -
  • - path: {string}/{table} The path(s) or URL(s) to revert. -
  • - -
  • - options: {table} [OPT] Options table. Possible Options: force -
  • - -
- - - - - - - - -

See also:

- - -
- - - - -
SubLua:MkDir (path, message)
-
-Create a new directory under version control. - - -

Parameters

-
    - -
  • - path: {string}/{table} The path(s) or URL(s) to output. If it is a local path then the directory is created and scheduled for addition upon the next commit. If it is a URL then the commit is immediate. -
  • - -
  • - message: {string} [OPT] Log message as a string. -
  • - -
- - - - - - - - -
- - - - -
SubLua:PropGet (propertyName, path, options)
-
-Gets the value of a property on files, dirs, or revisions. - - -

Parameters

-
    - -
  • - propertyName: {string} The property to get from path. -
  • - -
  • - path: {string}/{table} The path or URL to get the specified property from. -
  • - -
  • - options: {table} [OPT] Options table. Possible Options: revision, depth -
  • - -
- - - - - - -

Return value:

-{table} Table of properties. The list will contain tables where the keys are the path, and the values are a property table. In the property table there are keys of property names to their value, both as strings. - - - -

See also:

- - -
- - - - -
SubLua:PropSet (propName, propValue, path, options)
-
- - - -

Parameters

-
    - -
  • - propName: {string} The property name to set. -
  • - -
  • - propValue: {string} The value of the property to set -
  • - -
  • - path: {string} Path where property to be set -
  • - -
  • - options: {table} Options table. Possible Options: depth -
  • - -
- - - - - - - - -

See also:

- - -
- - - - -
SubLua:Revert (path, options)
-
-Revert files and directories from version control. - - -

Parameters

-
    - -
  • - path: {string}/{table} The path(s) or URL(s) to revert. -
  • - -
  • - options: {table} [OPT] Options table. Possible Options: depth -
  • - -
- - - - - - - - -

See also:

- - -
- - - - -
SubLua:Unlock (path, options)
-
-Unlocks files and directories. - - -

Parameters

-
    - -
  • - path: {string}/{table} The path(s) or URL(s) to revert. -
  • - -
  • - options: {table} [OPT] Options table. Possible Options: force -
  • - -
- - - - - - - - -

See also:

- - -
- - - - -
SubLua:Update (path, options)
-
-Updates the file or directory. - - -

Parameters

-
    - -
  • - path: {string} The path or URL to output. -
  • - -
  • - options: {table} [OPT] Options table. Possible Options: revision, depth, set_depth, ignore_externals, force -
  • - -
- - - - - - -

Return value:

-Revision of the checkout. - - - -

See also:

- - -
- - -
- - - - -

Tables

-
- -
Enums
-
The Subverison enums. These are part of the SubLua object returned after calling new(). - - -Fields -
    - -
  • - wc.notify.action.add: Adding a path to revision control. -
  • - -
  • - wc.notify.action.copy: Copying a versioned path. -
  • - -
  • - wc.notify.action.delete: Deleting a versioned path. -
  • - -
  • - wc.notify.action.restore: Restoring a missing path from the pristine text-base. -
  • - -
  • - wc.notify.action.revert: Reverting a modified path. -
  • - -
  • - wc.notify.action.failed_revert: A revert operation has failed. -
  • - -
  • - wc.notify.action.resolved: Resolving a conflict. -
  • - -
  • - wc.notify.action.skip: Skipping a path. -
  • - -
  • - wc.notify.action.update_delete: Got a delete in an update. -
  • - -
  • - wc.notify.action.update_add: Got an add in an update. -
  • - -
  • - wc.notify.action.update_update: Got any other action in an update. -
  • - -
  • - wc.notify.action.update_completed: The last notification in an update (including updates of externals). -
  • - -
  • - wc.notify.action.update_external: Updating an external module. -
  • - -
  • - wc.notify.action.status_completed: The last notification in a status (including status on externals). -
  • - -
  • - wc.notify.action.status_external: Running status on an external module. -
  • - -
  • - wc.notify.action.commit_modified: Committing a modification. -
  • - -
  • - wc.notify.action.commit_added: Committing an addition. -
  • - -
  • - wc.notify.action.commit_deleted: Committing a deletion. -
  • - -
  • - wc.notify.action.commit_replaced: Committing a replacement. -
  • - -
  • - wc.notify.action.commit_postfix_txdelta: Transmitting post-fix text-delta data for a file. -
  • - -
  • - wc.notify.action.blame_revision: Processed a single revision's blame. -
  • - -
  • - wc.notify.action.locked: Locking a path. New in 1.2. -
  • - -
  • - wc.notify.action.unlocked: Unlocking a path. New in 1.2. -
  • - -
  • - wc.notify.action.failed_lock: Failed to lock a path. New in 1.2. -
  • - -
  • - wc.notify.action.failed_unlock: Failed to unlock a path. New in 1.2. -
  • - -
  • - wc.notify.action.exists: Tried adding a path that already exists. New in 1.5. -
  • - -
  • - wc.notify.action.changelist_set: Changelist name set. New in 1.5. -
  • - -
  • - wc.notify.action.changelist_clear: Changelist name cleared. New in 1.5. -
  • - -
  • - wc.notify.action.changelist_moved: Warn user that a path has moved from one changelist to another. New in 1.5. Deprecated: As of 1.7, separate clear and set notifications are sent. -
  • - -
  • - wc.notify.action.merge_begin: A merge operation (to path) has begun. See svn_-- @field wc.notify.action.merge_range. New in 1.5. -
  • - -
  • - wc.notify.action.foreign_merge_begin: A merge operation (to path) from a foreign repository has begun. See -- @field wc.notify.action.merge_range. New in 1.5. -
  • - -
  • - wc.notify.action.update_replace: Replace notification. New in 1.5. -
  • - -
  • - wc.notify.action.property_added: Property added. New in 1.6. -
  • - -
  • - wc.notify.action.property_modified: Property updated. New in 1.6. -
  • - -
  • - wc.notify.action.property_deleted: Property deleted. New in 1.6. -
  • - -
  • - wc.notify.action.property_deleted_nonexistentNonexistent: property deleted. New in 1.6. -
  • - -
  • - wc.notify.action.revprop_set: Revprop set. New in 1.6. -
  • - -
  • - wc.notify.action.revprop_deleted: Revprop deleted. New in 1.6. -
  • - -
  • - wc.notify.action.merge_completed: The last notification in a merge. New in 1.6. -
  • - -
  • - wc.notify.action.tree_conflict: The path is a tree-conflict victim of the intended action (*not* a persistent tree-conflict from an earlier operation, but *this* operation caused the tree-conflict). New in 1.6. -
  • - -
  • - wc.notify.action.failed_external: The path is a subdirectory referenced in an externals definition which is unable to be operated on. New in 1.6. -
  • - -
  • - wc.notify.action.update_started: Starting an update operation. New in 1.7. -
  • - -
  • - wc.notify.action.update_skip_obstruction: An update tried to add a file or directory at a path where a separate working copy was found. New in 1.7. -
  • - -
  • - wc.notify.action.update_skip_working_only: An explicit update tried to update a file or directory that doesn't live in the repository and can't be brought in. New in 1.7. -
  • - -
  • - wc.notify.action.update_skip_access_denied: An update tried to update a file or directory to which access could not be obtained. New in 1.7. -
  • - -
  • - wc.notify.action.update_external_removed: An update operation removed an external working copy. New in 1.7. -
  • - -
  • - wc.notify.action.update_shadowed_add: A node below an existing node was added during update. New in 1.7. -
  • - -
  • - wc.notify.action.update_shadowed_update: A node below an exising node was updated during update. New in 1.7. -
  • - -
  • - wc.notify.action.update_shadowed_delete: A node below an existing node was deleted during update. New in 1.7. -
  • - -
  • - wc.notify.action.merge_record_info: The mergeinfo on path was updated. New in 1.7. -
  • - -
  • - wc.notify.action.upgraded_path: An working copy directory was upgraded to the latest format. New in 1.7. -
  • - -
  • - wc.notify.action.merge_record_info_begin: Mergeinfo describing a merge was recorded. New in 1.7. -
  • - -
  • - wc.notify.action.merge_elide_info: Mergeinfo was removed due to elision. New in 1.7. -
  • - -
  • - wc.notify.action.patch: A file in the working copy was patched. New in 1.7. -
  • - -
  • - wc.notify.action.patch_applied_hunk: A hunk from a patch was applied. New in 1.7. -
  • - -
  • - wc.notify.action.patch_rejected_hunk: A hunk from a patch was rejected. New in 1.7. -
  • - -
  • - wc.notify.action.patch_hunk_already_applied: A hunk from a patch was found to already be applied. New in 1.7. -
  • - -
  • - wc.notify.action.commit_copied: Committing a non-overwriting copy (path is the target of the copy, not the source). New in 1.7. -
  • - -
  • - wc.notify.action.commit_copied_replaced: Committing an overwriting (replace) copy (path is the target of the copy, not the source). New in 1.7. -
  • - -
  • - wc.notify.action.url_redirect: The server has instructed the client to follow a URL redirection. New in 1.7. -
  • - -
  • - wc.notify.action.path_nonexistent: The operation was attempted on a path which doesn't exist. New in 1.7. -
  • - -
  • - wc.notify.action.exclude: Removing a path by excluding it. New in 1.7. -
  • - -
  • - wc.notify.action.failed_conflict: Operation failed because the node remains in conflict. New in 1.7. -
  • - -
  • - wc.notify.action.failed_missing: Operation failed because an added node is missing. New in 1.7. -
  • - -
  • - wc.notify.action.failed_out_of_date: Operation failed because a node is out of date. New in 1.7. -
  • - -
  • - wc.notify.action.failed_no_parent: Operation failed because an added parent is not selected. New in 1.7. -
  • - -
  • - wc.notify.action.failed_locked: Operation failed because a node is locked by another user and/or working copy. New in 1.7. -
  • - -
  • - wc.notify.action.failed_forbidden_by_server: Operation failed because the operation was forbidden by the server. New in 1.7. -
  • - -
  • - wc.notify.action.skip_conflicted: The operation skipped the path because it was conflicted. New in 1.7. -
  • - -
- - -
- - -
Options
-
The Options class. - - -Fields -
    - -
  • - revision: the revision you want to update to; defaults to "HEAD". -
  • - -
  • - peg: the peg revision you want to update using; defaults to "HEAD". -
  • - -
  • - depth: limit operation by depth ARG ('empty', 'files', 'immediates', or 'infinity'); defaults to unknown. -
  • - -
  • - force: force operation to run; defaults to false. -
  • - -
  • - ignore_externals: set to ignore the externals; defaults to false. -
  • - -
  • - ignore_keywords: set to ignore the keyword substitution; defaults to false. -
  • - -
  • - no_ignore: set to disregard default and svn:ignore property ignores; defaults to false. -
  • - -
  • - native_eol: use a different EOL marker than the standard system marker for files with the svn:eol-style property set to 'native'. ARG may be one of 'LF', 'CR', 'CRLF'; defaults to NULL (no change). -
  • - -
  • - set_depth: set new working copy depth to depth. If depth is not specified then this does nothing; defaults to false. -
  • - -
  • - parents: If true, create any non-existent parent directories also; defaults to false. -
  • - -
  • - add_parents: If true, recurse up path's directory and look for a versioned directory. If found, add all intermediate paths between it and path; defaults to false. -
  • - -
  • - keep_changelists: After the commit completes successfully, remove changelist associations from the targets, unless keep_changelists is true; defaults to false. -
  • - -
  • - keep_locks: Unlock paths in the repository, unless keep_locks is true; defaults to false. -
  • - -
  • - auto_props: Enable automatic properties; defaults to true. -
  • - -
  • - ignore_unknown_node_types: Ignore files of which the node type is unknown, such as device files and pipes.; defaults to false. -
  • - -
  • - keep_local: if true then the paths will not be removed from the working copy, only scheduled for removal from the repository. Once the scheduled deletion is committed, they will appear as unversioned paths in the working copy; defaults to false. -
  • - -
  • - no_diff_deleting: Do not print differences for deleted files; defaults to false. -
  • - -
  • - notice_ancestry: Notice ancestry when calculating differences; defaults to false. -
  • - -
- - -
- - -
SvnInfo
-
The SvnInfo class. - - -Fields -
    - -
  • - isValid: Is it valid? -
  • - -
  • - url: The url -
  • - -
  • - revision: The revision -
  • - -
  • - kind: The kind -
  • - -
  • - rootUrl: The root URL -
  • - -
  • - uuid: The uuid -
  • - -
  • - lastChangedRevision: Last changed revision -
  • - -
  • - lastChangedDate: Last changed date -
  • - -
  • - lastChangedAuthor: Last changed author -
  • - -
  • - lock: Lock -
  • - -
  • - hasWCInfo: Has working copy info? -
  • - -
  • - schedule: The schedule -
  • - -
  • - copyFromUrl: Copy from URL -
  • - -
  • - copyFromRev: Copy from revision -
  • - -
  • - textTime: Time in text -
  • - -
  • - propTime: Properties time -
  • - -
  • - checksum: The checksum -
  • - -
  • - conflictOld: Conflict old -
  • - -
  • - conflictNew: Conflict new -
  • - -
  • - conflictWrk: Conflict working -
  • - -
  • - propRejectFile: Prop reject file -
  • - -
  • - changelist: The changelist -
  • - -
  • - depth: Depth -
  • - -
  • - workingSize: Working size -
  • - -
  • - size: Size -
  • - -
  • - size64: Size64 -
  • - -
  • - workingSize64: Working Size64 -
  • - -
  • - treeConflict: the TreeConflict -
  • - -
- - -
- - -
- - - -
- -
- -
-

Valid XHTML 1.0!

-
- -
- - diff --git a/files/lua/pl/Date.lua b/files/lua/pl/Date.lua index 9d9fd78..25c5df5 100644 --- a/files/lua/pl/Date.lua +++ b/files/lua/pl/Date.lua @@ -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:
--- 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:
    ---
  • 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)
  • ---
+-- @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: ---
    ---
  1. ISO 8601, --- like 2010-05-10 12:35:23Z or 2008-10-03T14:30+02
  2. ---
  3. times like 15:30 or 8.05pm (assumed to be today's date)
  4. ---
  5. dates like 28/10/02 (European order!) or 5 Feb 2012
  6. ---
  7. month name like march or Mar (case-insensitive, first 3 letters); --- here the day will be 1 and the year this current year
  8. ---
+-- +-- * [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 diff --git a/files/lua/pl/List.lua b/files/lua/pl/List.lua index 70d61c4..ac9af14 100644 --- a/files/lua/pl/List.lua +++ b/files/lua/pl/List.lua @@ -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.
+--- 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.
--- 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 ''. --- @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) diff --git a/files/lua/pl/Map.lua b/files/lua/pl/Map.lua index 28433d1..1cb7ff4 100644 --- a/files/lua/pl/Map.lua +++ b/files/lua/pl/Map.lua @@ -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 diff --git a/files/lua/pl/MultiMap.lua b/files/lua/pl/MultiMap.lua index 9c0899a..d7a4ada 100644 --- a/files/lua/pl/MultiMap.lua +++ b/files/lua/pl/MultiMap.lua @@ -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' diff --git a/files/lua/pl/OrderedMap.lua b/files/lua/pl/OrderedMap.lua index 638dd35..cb9af58 100644 --- a/files/lua/pl/OrderedMap.lua +++ b/files/lua/pl/OrderedMap.lua @@ -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.
--- if it s a table of the form {{key1=val1},{key2=val2},...} these will be appended.
+-- 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.
+--- 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 diff --git a/files/lua/pl/Set.lua b/files/lua/pl/Set.lua index a09415d..8626f44 100644 --- a/files/lua/pl/Set.lua +++ b/files/lua/pl/Set.lua @@ -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 diff --git a/files/lua/pl/app.lua b/files/lua/pl/app.lua index 4c5f1fd..ca501ee 100644 --- a/files/lua/pl/app.lua +++ b/files/lua/pl/app.lua @@ -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. {out=true} +-- 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 diff --git a/files/lua/pl/array2d.lua b/files/lua/pl/array2d.lua index 7fde031..e3cf7e4 100644 --- a/files/lua/pl/array2d.lua +++ b/files/lua/pl/array2d.lua @@ -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 diff --git a/files/lua/pl/class.lua b/files/lua/pl/class.lua index e482738..d260c31 100644 --- a/files/lua/pl/class.lua +++ b/files/lua/pl/class.lua @@ -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 () 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 diff --git a/files/lua/pl/compat.lua b/files/lua/pl/compat.lua new file mode 100644 index 0000000..8ab7ec8 --- /dev/null +++ b/files/lua/pl/compat.lua @@ -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 diff --git a/files/lua/pl/comprehension.lua b/files/lua/pl/comprehension.lua index 0e9b371..9d8aa59 100644 --- a/files/lua/pl/comprehension.lua +++ b/files/lua/pl/comprehension.lua @@ -253,7 +253,7 @@ local function new(env) -- performance penalty. if not env then - env = getfenv(2) + env = utils.getfenv(2) end local mt = {} diff --git a/files/lua/pl/config.lua b/files/lua/pl/config.lua index f802162..f551da5 100644 --- a/files/lua/pl/config.lua +++ b/files/lua/pl/config.lua @@ -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 + diff --git a/files/lua/pl/data.lua b/files/lua/pl/data.lua index 45212cc..6c82116 100644 --- a/files/lua/pl/data.lua +++ b/files/lua/pl/data.lua @@ -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') diff --git a/files/lua/pl/dir.lua b/files/lua/pl/dir.lua index c3788b3..6c62442 100755 --- a/files/lua/pl/dir.lua +++ b/files/lua/pl/dir.lua @@ -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 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. --- @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 path and filtered by pattern. --- @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 diff --git a/files/lua/pl/file.lua b/files/lua/pl/file.lua index 1596656..572d7fc 100644 --- a/files/lua/pl/file.lua +++ b/files/lua/pl/file.lua @@ -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 diff --git a/files/lua/pl/func.lua b/files/lua/pl/func.lua index 8a43112..57bf685 100644 --- a/files/lua/pl/func.lua +++ b/files/lua/pl/func.lua @@ -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 diff --git a/files/lua/pl/import_into.lua b/files/lua/pl/import_into.lua new file mode 100644 index 0000000..7ef54f8 --- /dev/null +++ b/files/lua/pl/import_into.lua @@ -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 diff --git a/files/lua/pl/init.lua b/files/lua/pl/init.lua index d562a00..c27a890 100644 --- a/files/lua/pl/init.lua +++ b/files/lua/pl/init.lua @@ -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 diff --git a/files/lua/pl/input.lua b/files/lua/pl/input.lua index 09fcb46..3bfce9c 100644 --- a/files/lua/pl/input.lua +++ b/files/lua/pl/input.lua @@ -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) diff --git a/files/lua/pl/lapp.lua b/files/lua/pl/lapp.lua index 351d85e..d910882 100644 --- a/files/lua/pl/lapp.lua +++ b/files/lua/pl/lapp.lua @@ -6,7 +6,7 @@ -- Does some calculations -- -o,--offset (default 0.0) Offset to add to scaled number -- -s,--scale (number) Scaling factor --- <number> (number ) Number to be scaled +-- ; (number ) Number to be scaled -- ]] -- -- print(args.offset + args.scale * args.number) @@ -15,7 +15,7 @@ -- lines begining wih '' 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 io.open +-- @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. ' (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 '-', '-,--' or '--' - 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 ? -- so 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 ([] [default ])? + local optional + -- do we have ([optional] [] [default ])? 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 diff --git a/files/lua/pl/lexer.lua b/files/lua/pl/lexer.lua index b80ab3c..c61f9a3 100644 --- a/files/lua/pl/lexer.lua +++ b/files/lua/pl/lexer.lua @@ -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.
+--- 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 res["and"] etc would be true. +-- 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') diff --git a/files/lua/pl/operator.lua b/files/lua/pl/operator.lua index ba77d80..f857107 100644 --- a/files/lua/pl/operator.lua +++ b/files/lua/pl/operator.lua @@ -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, diff --git a/files/lua/pl/path.lua b/files/lua/pl/path.lua index ecf2b6d..772056f 100644 --- a/files/lua/pl/path.lua +++ b/files/lua/pl/path.lua @@ -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) diff --git a/files/lua/pl/platf/luajava.lua b/files/lua/pl/platf/luajava.lua deleted file mode 100644 index 4fb82e6..0000000 --- a/files/lua/pl/platf/luajava.lua +++ /dev/null @@ -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 diff --git a/files/lua/pl/pretty.lua b/files/lua/pl/pretty.lua index 8cc37d2..5319fed 100644 --- a/files/lua/pl/pretty.lua +++ b/files/lua/pl/pretty.lua @@ -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(',') @@ -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 diff --git a/files/lua/pl/seq.lua b/files/lua/pl/seq.lua index 49c0090..6e53815 100644 --- a/files/lua/pl/seq.lua +++ b/files/lua/pl/seq.lua @@ -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 diff --git a/files/lua/pl/sip.lua b/files/lua/pl/sip.lua index e240f63..758c063 100644 --- a/files/lua/pl/sip.lua +++ b/files/lua/pl/sip.lua @@ -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') diff --git a/files/lua/pl/strict.lua b/files/lua/pl/strict.lua index 2a5b21a..df96227 100644 --- a/files/lua/pl/strict.lua +++ b/files/lua/pl/strict.lua @@ -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 diff --git a/files/lua/pl/stringio.lua b/files/lua/pl/stringio.lua index 59ae1cc..40cb108 100644 --- a/files/lua/pl/stringio.lua +++ b/files/lua/pl/stringio.lua @@ -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 value() 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 diff --git a/files/lua/pl/stringx.lua b/files/lua/pl/stringx.lua index 0e66c2a..9bae615 100644 --- a/files/lua/pl/stringx.lua +++ b/files/lua/pl/stringx.lua @@ -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 diff --git a/files/lua/pl/tablex.lua b/files/lua/pl/tablex.lua index c83df67..e8fbe01 100644 --- a/files/lua/pl/tablex.lua +++ b/files/lua/pl/tablex.lua @@ -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 finally any extra arguments passed to this function. --- Note that the Lua 5.0 function table.foreach passed the key 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 finally 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.
--- @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.
--- @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.
--- insertvalues(t, [pos,] values)
--- 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 diff --git a/files/lua/pl/template.lua b/files/lua/pl/template.lua index 5c992ae..05480e8 100644 --- a/files/lua/pl/template.lua +++ b/files/lua/pl/template.lua @@ -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).
--- There are three special fields in the environment table
    ---
  • _parent continue looking up in this table
  • ---
  • _brackets; default is '()', can be any suitable bracket pair
  • ---
  • _escape; default is '#'
  • ---
+-- 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 diff --git a/files/lua/pl/test.lua b/files/lua/pl/test.lua index b281cd2..bf4df9d 100644 --- a/files/lua/pl/test.lua +++ b/files/lua/pl/test.lua @@ -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() diff --git a/files/lua/pl/text.lua b/files/lua/pl/text.lua index 13a74be..96417e9 100644 --- a/files/lua/pl/text.lua +++ b/files/lua/pl/text.lua @@ -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 diff --git a/files/lua/pl/types.lua b/files/lua/pl/types.lua new file mode 100644 index 0000000..e40cd65 --- /dev/null +++ b/files/lua/pl/types.lua @@ -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 diff --git a/files/lua/pl/url.lua b/files/lua/pl/url.lua new file mode 100644 index 0000000..8b159d2 --- /dev/null +++ b/files/lua/pl/url.lua @@ -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 diff --git a/files/lua/pl/utils.lua b/files/lua/pl/utils.lua index cdc84d5..f933afb 100644 --- a/files/lua/pl/utils.lua +++ b/files/lua/pl/utils.lua @@ -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 diff --git a/files/lua/pl/xml.lua b/files/lua/pl/xml.lua index 437eddb..2edd98f 100644 --- a/files/lua/pl/xml.lua +++ b/files/lua/pl/xml.lua @@ -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 --- @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] = "" + 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 - 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 + _,istart = s_find(s,'^%s*<%?[^%?]+%?>%s*') + if not istart then -- or + _,istart = s_find(s,'^%s*%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