summaryrefslogtreecommitdiff
path: root/macros/luatex/generic/penlight/pl/stringx.lua
diff options
context:
space:
mode:
Diffstat (limited to 'macros/luatex/generic/penlight/pl/stringx.lua')
-rw-r--r--macros/luatex/generic/penlight/pl/stringx.lua594
1 files changed, 0 insertions, 594 deletions
diff --git a/macros/luatex/generic/penlight/pl/stringx.lua b/macros/luatex/generic/penlight/pl/stringx.lua
deleted file mode 100644
index 627611316b..0000000000
--- a/macros/luatex/generic/penlight/pl/stringx.lua
+++ /dev/null
@@ -1,594 +0,0 @@
---- Python-style extended string library.
---
--- see 3.6.1 of the Python reference.
--- If you want to make these available as string methods, then say
--- `stringx.import()` to bring them into the standard `string` table.
---
--- See @{03-strings.md|the Guide}
---
--- Dependencies: `pl.utils`
--- @module pl.stringx
-local utils = require 'pl.utils'
-local string = string
-local find = string.find
-local type,setmetatable,ipairs = type,setmetatable,ipairs
-local error = error
-local gsub = string.gsub
-local rep = string.rep
-local sub = string.sub
-local reverse = string.reverse
-local concat = table.concat
-local append = table.insert
-local escape = utils.escape
-local ceil, max = math.ceil, math.max
-local assert_arg,usplit = utils.assert_arg,utils.split
-local lstrip
-
-local function assert_string (n,s)
- assert_arg(n,s,'string')
-end
-
-local function non_empty(s)
- return #s > 0
-end
-
-local function assert_nonempty_string(n,s)
- assert_arg(n,s,'string',non_empty,'must be a non-empty string')
-end
-
-local function makelist(l)
- return setmetatable(l, require('pl.List'))
-end
-
-local stringx = {}
-
-------------------
--- String Predicates
--- @section predicates
-
---- does s only contain alphabetic characters?
--- @string s a string
-function stringx.isalpha(s)
- assert_string(1,s)
- return find(s,'^%a+$') == 1
-end
-
---- does s only contain digits?
--- @string s a string
-function stringx.isdigit(s)
- assert_string(1,s)
- return find(s,'^%d+$') == 1
-end
-
---- does s only contain alphanumeric characters?
--- @string s a string
-function stringx.isalnum(s)
- assert_string(1,s)
- return find(s,'^%w+$') == 1
-end
-
---- does s only contain spaces?
--- @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?
--- @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?
--- @string s a string
-function stringx.isupper(s)
- assert_string(1,s)
- return find(s,'^[%u%s]+$') == 1
-end
-
-local function raw_startswith(s, prefix)
- return find(s,prefix,1,true) == 1
-end
-
-local function raw_endswith(s, suffix)
- return #s >= #suffix and find(s, suffix, #s-#suffix+1, true) and true or false
-end
-
-local function test_affixes(s, affixes, fn)
- if type(affixes) == 'string' then
- return fn(s,affixes)
- elseif type(affixes) == 'table' then
- for _,affix in ipairs(affixes) do
- if fn(s,affix) then return true end
- end
- return false
- else
- error(("argument #2 expected a 'string' or a 'table', got a '%s'"):format(type(affixes)))
- end
-end
-
---- does s start with prefix or one of prefixes?
--- @string s a string
--- @param prefix a string or an array of strings
-function stringx.startswith(s,prefix)
- assert_string(1,s)
- return test_affixes(s,prefix,raw_startswith)
-end
-
---- does s end with suffix or one of suffixes?
--- @string s a string
--- @param suffix a string or an array of strings
-function stringx.endswith(s,suffix)
- assert_string(1,s)
- return test_affixes(s,suffix,raw_endswith)
-end
-
---- Strings and Lists
--- @section lists
-
---- concatenate the strings using this string as a delimiter.
--- Note that the arguments are reversed from `string.concat`.
--- @string s the string
--- @param seq a table of strings or numbers
--- @usage stringx.join(' ', {1,2,3}) == '1 2 3'
-function stringx.join(s,seq)
- assert_string(1,s)
- return concat(seq,s)
-end
-
---- Split a string into a list of lines.
--- `"\r"`, `"\n"`, and `"\r\n"` are considered line ends.
--- They are not included in the lines unless `keepends` is passed.
--- Terminal line end does not produce an extra line.
--- Splitting an empty string results in an empty list.
--- @string s the string.
--- @bool[opt] keep_ends include line ends.
--- @return List of lines
-function stringx.splitlines(s, keep_ends)
- assert_string(1, s)
- local res = {}
- local pos = 1
- while true do
- local line_end_pos = find(s, '[\r\n]', pos)
- if not line_end_pos then
- break
- end
-
- local line_end = sub(s, line_end_pos, line_end_pos)
- if line_end == '\r' and sub(s, line_end_pos + 1, line_end_pos + 1) == '\n' then
- line_end = '\r\n'
- end
-
- local line = sub(s, pos, line_end_pos - 1)
- if keep_ends then
- line = line .. line_end
- end
- append(res, line)
-
- pos = line_end_pos + #line_end
- end
-
- if pos <= #s then
- append(res, sub(s, pos))
- end
- return makelist(res)
-end
-
---- split a string into a list of strings using a delimiter.
--- @function split
--- @string s the string
--- @string[opt] re a delimiter (defaults to whitespace)
--- @int[opt] n maximum number of results
--- @return List
--- @usage #(stringx.split('one two')) == 2
--- @usage stringx.split('one,two,three', ',') == List{'one','two','three'}
--- @usage stringx.split('one,two,three', ',', 2) == List{'one','two,three'}
-function stringx.split(s,re,n)
- assert_string(1,s)
- local plain = true
- if not re then -- default spaces
- s = lstrip(s)
- plain = false
- end
- local res = usplit(s,re,plain,n)
- if re and re ~= '' and
- find(s,re,-#re,true) and
- (n or math.huge) > #res then
- res[#res+1] = ""
- end
- return makelist(res)
-end
-
---- replace all tabs in s with tabsize spaces. If not specified, tabsize defaults to 8.
--- Tab stops will be honored.
--- @string s the string
--- @int tabsize[opt=8] number of spaces to expand each tab
--- @return expanded string
--- @usage stringx.expandtabs('\tone,two,three', 4) == ' one,two,three'
--- @usage stringx.expandtabs(' \tone,two,three', 4) == ' one,two,three'
-function stringx.expandtabs(s,tabsize)
- assert_string(1,s)
- tabsize = tabsize or 8
- return (s:gsub("([^\t\r\n]*)\t", function(before_tab)
- return before_tab .. (" "):rep(tabsize - #before_tab % tabsize)
- end))
-end
-
---- Finding and Replacing
--- @section find
-
-local function _find_all(s,sub,first,last,allow_overlap)
- first = first or 1
- last = last or #s
- if sub == '' then return last+1,last-first+1 end
- local i1,i2 = find(s,sub,first,true)
- local res
- local k = 0
- while i1 do
- if last and i2 > last then break end
- res = i1
- k = k + 1
- if allow_overlap then
- i1,i2 = find(s,sub,i1+1,true)
- else
- i1,i2 = find(s,sub,i2+1,true)
- end
- end
- return res,k
-end
-
---- find index of first instance of sub in s from the left.
--- @string s the string
--- @string sub substring
--- @int[opt] first first index
--- @int[opt] last last index
--- @return start index, or nil if not found
-function stringx.lfind(s,sub,first,last)
- assert_string(1,s)
- assert_string(2,sub)
- local i1, i2 = find(s,sub,first,true)
-
- if i1 and (not last or i2 <= last) then
- return i1
- else
- return nil
- end
-end
-
---- find index of first instance of sub in s from the right.
--- @string s the string
--- @string sub substring
--- @int[opt] first first index
--- @int[opt] last last index
--- @return start index, or nil if not found
-function stringx.rfind(s,sub,first,last)
- assert_string(1,s)
- assert_string(2,sub)
- return (_find_all(s,sub,first,last,true))
-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
-function stringx.replace(s,old,new,n)
- assert_string(1,s)
- assert_string(2,old)
- assert_string(3,new)
- return (gsub(s,escape(old),new:gsub('%%','%%%%'),n))
-end
-
---- count all instances of substring in string.
--- @string s the string
--- @string sub substring
--- @bool[opt] allow_overlap allow matches to overlap
--- @usage
--- assert(stringx.count('banana', 'ana') == 1)
--- assert(stringx.count('banana', 'ana', true) == 2)
-function stringx.count(s,sub,allow_overlap)
- assert_string(1,s)
- local _,k = _find_all(s,sub,1,false,allow_overlap)
- return k
-end
-
---- Stripping and Justifying
--- @section strip
-
-local function _just(s,w,ch,left,right)
- local n = #s
- if w > n then
- if not ch then ch = ' ' end
- local f1,f2
- if left and right then
- local rn = ceil((w-n)/2)
- local ln = w - n - rn
- f1 = rep(ch,ln)
- f2 = rep(ch,rn)
- elseif right then
- f1 = rep(ch,w-n)
- f2 = ''
- else
- f2 = rep(ch,w-n)
- f1 = ''
- end
- return f1..s..f2
- else
- return s
- end
-end
-
---- left-justify s with width w.
--- @string s the string
--- @int w width of justification
--- @string[opt=' '] ch padding character
--- @usage stringx.ljust('hello', 10, '*') == '*****hello'
-function stringx.ljust(s,w,ch)
- assert_string(1,s)
- assert_arg(2,w,'number')
- return _just(s,w,ch,true,false)
-end
-
---- right-justify s with width w.
--- @string s the string
--- @int w width of justification
--- @string[opt=' '] ch padding character
--- @usage stringx.rjust('hello', 10, '*') == 'hello*****'
-function stringx.rjust(s,w,ch)
- assert_string(1,s)
- assert_arg(2,w,'number')
- return _just(s,w,ch,false,true)
-end
-
---- center-justify s with width w.
--- @string s the string
--- @int w width of justification
--- @string[opt=' '] ch padding character
--- @usage stringx.center('hello', 10, '*') == '**hello***'
-function stringx.center(s,w,ch)
- assert_string(1,s)
- assert_arg(2,w,'number')
- return _just(s,w,ch,true,true)
-end
-
-local function _strip(s,left,right,chrs)
- if not chrs then
- chrs = '%s'
- else
- chrs = '['..escape(chrs)..']'
- end
- local f = 1
- local t
- if left then
- local i1,i2 = find(s,'^'..chrs..'*')
- if i2 >= i1 then
- f = i2+1
- end
- end
- if right then
- if #s < 200 then
- local i1,i2 = find(s,chrs..'*$',f)
- if i2 >= i1 then
- t = i1-1
- end
- else
- local rs = reverse(s)
- local i1,i2 = find(rs, '^'..chrs..'*')
- if i2 >= i1 then
- t = -i2-1
- end
- end
- end
- return sub(s,f,t)
-end
-
---- trim any whitespace on the left of s.
--- @string s the string
--- @string[opt='%s'] chrs default any whitespace character,
--- but can be a string of characters to be trimmed
-function stringx.lstrip(s,chrs)
- assert_string(1,s)
- return _strip(s,true,false,chrs)
-end
-lstrip = stringx.lstrip
-
---- trim any whitespace on the right of s.
--- @string s the string
--- @string[opt='%s'] 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.
--- @string s the string
--- @string[opt='%s'] chrs default any whitespace character,
--- but can be a string of characters to be trimmed
-function stringx.strip(s,chrs)
- assert_string(1,s)
- return _strip(s,true,true,chrs)
-end
-
---- Partioning Strings
--- @section partioning
-
---- split a string using a pattern. Note that at least one value will be returned!
--- @string s the string
--- @string[opt='%s'] re a Lua string pattern (defaults to whitespace)
--- @return the parts of the string
--- @usage a,b = line:splitv('=')
--- @see utils.splitv
-function stringx.splitv(s,re)
- assert_string(1,s)
- return utils.splitv(s,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)
- local i1,i2 = fn(p,delim)
- if not i1 or i1 == -1 then
- return p,'',''
- else
- if not i2 then i2 = i1 end
- return sub(p,1,i1-1),sub(p,i1,i2),sub(p,i2+1)
- end
-end
-
---- partition the string using first occurance of a delimiter
--- @string s the string
--- @string ch delimiter
--- @return part before ch
--- @return ch
--- @return part after ch
--- @usage {stringx.partition('a,b,c', ','))} == {'a', ',', 'b,c'}
--- @usage {stringx.partition('abc', 'x'))} == {'abc', '', ''}
-function stringx.partition(s,ch)
- assert_string(1,s)
- assert_nonempty_string(2,ch)
- return _partition(s,ch,stringx.lfind)
-end
-
---- partition the string p using last occurance of a delimiter
--- @string s the string
--- @string ch delimiter
--- @return part before ch
--- @return ch
--- @return part after ch
--- @usage {stringx.rpartition('a,b,c', ','))} == {'a,b', ',', 'c'}
--- @usage {stringx.rpartition('abc', 'x'))} == {'', '', 'abc'}
-function stringx.rpartition(s,ch)
- assert_string(1,s)
- assert_nonempty_string(2,ch)
- local a,b,c = _partition(s,ch,stringx.rfind)
- if a == s then -- no match found
- return c,b,a
- end
- return a,b,c
-end
-
---- return the 'character' at the index.
--- @string s the string
--- @int idx an index (can be negative)
--- @return a substring of length 1 if successful, empty string otherwise.
-function stringx.at(s,idx)
- assert_string(1,s)
- assert_arg(2,idx,'number')
- return sub(s,idx,idx)
-end
-
---- Miscelaneous
--- @section misc
-
---- return an iterator over all lines in a string
--- @string s the string
--- @return an iterator
--- @usage
--- local line_no = 1
--- for line in stringx.lines(some_text) do
--- print(line_no, line)
--- line_no = line_no + 1
--- end
-function stringx.lines(s)
- assert_string(1,s)
- if not s:find '\n$' then s = s..'\n' end
- return s:gmatch('([^\n]*)\n')
-end
-
---- inital word letters uppercase ('title case').
--- Here 'words' mean chunks of non-space characters.
--- @string s the string
--- @return a string with each word's first letter uppercase
--- @usage stringx.title("hello world") == "Hello World")
-function stringx.title(s)
- assert_string(1,s)
- return (s:gsub('(%S)(%S*)',function(f,r)
- return f:upper()..r:lower()
- end))
-end
-
-stringx.capitalize = stringx.title
-
-local ellipsis = '...'
-local n_ellipsis = #ellipsis
-
---- Return a shortened version of a string.
--- Fits string within w characters. Removed characters are marked with ellipsis.
--- @string s the string
--- @int w the maxinum size allowed
--- @bool tail true if we want to show the end of the string (head otherwise)
--- @usage ('1234567890'):shorten(8) == '12345...'
--- @usage ('1234567890'):shorten(8, true) == '...67890'
--- @usage ('1234567890'):shorten(20) == '1234567890'
-function stringx.shorten(s,w,tail)
- assert_string(1,s)
- if #s > w then
- if w < n_ellipsis then return ellipsis:sub(1,w) end
- if tail then
- local i = #s - w + 1 + n_ellipsis
- return ellipsis .. s:sub(i)
- else
- return s:sub(1,w-n_ellipsis) .. ellipsis
- end
- end
- return s
-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 equals, new_equals, _
- local finish = 1
- repeat
- _, finish, _, new_equals = s:find(lstring_pat, finish)
- if new_equals then
- equals = max(equals or 0, #new_equals)
- end
- until not new_equals
-
- 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)
- assert_string(1,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.
- -- Append a closing bracket to catch unfinished long-quote sequences at the end of the string.
- local equal_signs = has_lquote(s .. "]")
-
- -- Note that strings containing "\r" can't be quoted using long brackets
- -- as Lua lexer converts all newlines to "\n" within long strings.
- if (s:find("\n") or equal_signs) and not s:find("\r") then
- -- 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.
- equal_signs = ("="):rep((equal_signs or -1) + 1)
- -- Long strings strip out leading newline. We want to retain that, when quoting.
- if s:find("^\n") then s = "\n" .. s end
- local lbracket, rbracket =
- "[" .. equal_signs .. "[",
- "]" .. equal_signs .. "]"
- s = lbracket .. s .. rbracket
- else
- -- Escape funny stuff. Lua 5.1 does not handle "\r" correctly.
- s = ("%q"):format(s):gsub("\r", "\\r")
- end
- return s
-end
-
-function stringx.import()
- utils.import(stringx,string)
-end
-
-return stringx