From c9fcf97bc045a9d020980c33b6c7c1ee5aedef99 Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Mon, 20 Sep 2021 03:02:23 +0000 Subject: CTAN sync 202109200302 --- macros/luatex/generic/penlight/pl/sip.lua | 337 ++++++++++++++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 macros/luatex/generic/penlight/pl/sip.lua (limited to 'macros/luatex/generic/penlight/pl/sip.lua') diff --git a/macros/luatex/generic/penlight/pl/sip.lua b/macros/luatex/generic/penlight/pl/sip.lua new file mode 100644 index 0000000000..5d1f2aa0cc --- /dev/null +++ b/macros/luatex/generic/penlight/pl/sip.lua @@ -0,0 +1,337 @@ +--- Simple Input Patterns (SIP). +-- SIP patterns start with '$', then a +-- one-letter type, and then an optional variable in curly braces. +-- +-- sip.match('$v=$q','name="dolly"',res) +-- ==> res=={'name','dolly'} +-- sip.match('($q{first},$q{second})','("john","smith")',res) +-- ==> res=={second='smith',first='john'} +-- +-- Type names: +-- +-- v identifier +-- i integer +-- f floating-point +-- q quoted string +-- ([{< match up to closing bracket +-- +-- See @{08-additional.md.Simple_Input_Patterns|the Guide} +-- +-- @module pl.sip + +local loadstring = rawget(_G,'loadstring') or load +local unpack = rawget(_G,'unpack') or rawget(table,'unpack') + +local append,concat = table.insert,table.concat +local ipairs,type = ipairs,type +local io,_G = io,_G +local print,rawget = print,rawget + +local patterns = { + FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*', + INTEGER = '[+%-%d]%d*', + IDEN = '[%a_][%w_]*', + OPTION = '[%a_][%w_%-]*', +} + +local function assert_arg(idx,val,tp) + if type(val) ~= tp then + error("argument "..idx.." must be "..tp, 2) + end +end + +local sip = {} + +local brackets = {['<'] = '>', ['('] = ')', ['{'] = '}', ['['] = ']' } +local stdclasses = {a=1,c=0,d=1,l=1,p=0,u=1,w=1,x=1,s=0} + +local function group(s) + return '('..s..')' +end + +-- escape all magic characters except $, which has special meaning +-- Also, un-escape any characters after $, so $( and $[ passes through as is. +local function escape (spec) + return (spec:gsub('[%-%.%+%[%]%(%)%^%%%?%*]','%%%0'):gsub('%$%%(%S)','$%1')) +end + +-- Most spaces within patterns can match zero or more spaces. +-- Spaces between alphanumeric characters or underscores or between +-- patterns that can match these characters, however, must match at least +-- one space. Otherwise '$v $v' would match 'abcd' as {'abc', 'd'}. +-- This function replaces continuous spaces within a pattern with either +-- '%s*' or '%s+' according to this rule. The pattern has already +-- been stripped of pattern names by now. +local function compress_spaces(patt) + return (patt:gsub("()%s+()", function(i1, i2) + local before = patt:sub(i1 - 2, i1 - 1) + if before:match('%$[vifadxlu]') or before:match('^[^%$]?[%w_]$') then + local after = patt:sub(i2, i2 + 1) + if after:match('%$[vifadxlu]') or after:match('^[%w_]') then + return '%s+' + end + end + return '%s*' + end)) +end + +local pattern_map = { + v = group(patterns.IDEN), + i = group(patterns.INTEGER), + f = group(patterns.FLOAT), + o = group(patterns.OPTION), + r = '(%S.*)', + p = '([%a]?[:]?[\\/%.%w_]+)' +} + +function sip.custom_pattern(flag,patt) + pattern_map[flag] = patt +end + +--- convert a SIP pattern into the equivalent Lua string pattern. +-- @param spec a SIP pattern +-- @param options a table; only the at_start field is +-- currently meaningful and ensures that the pattern is anchored +-- at the start of the string. +-- @return a Lua string pattern. +function sip.create_pattern (spec,options) + assert_arg(1,spec,'string') + local fieldnames,fieldtypes = {},{} + + if type(spec) == 'string' then + spec = escape(spec) + else + local res = {} + for i,s in ipairs(spec) do + res[i] = escape(s) + end + spec = concat(res,'.-') + end + + local kount = 1 + + local function addfield (name,type) + name = name or kount + append(fieldnames,name) + fieldtypes[name] = type + kount = kount + 1 + end + + local named_vars = spec:find('{%a+}') + + if options and options.at_start then + spec = '^'..spec + end + if spec:sub(-1,-1) == '$' then + spec = spec:sub(1,-2)..'$r' + if named_vars then spec = spec..'{rest}' end + end + + local names + + if named_vars then + names = {} + spec = spec:gsub('{(%a+)}',function(name) + append(names,name) + return '' + end) + end + spec = compress_spaces(spec) + + local k = 1 + local err + local r = (spec:gsub('%$%S',function(s) + local type,name + type = s:sub(2,2) + if names then name = names[k]; k=k+1 end + -- this kludge is necessary because %q generates two matches, and + -- we want to ignore the first. Not a problem for named captures. + if not names and type == 'q' then + addfield(nil,'Q') + else + addfield(name,type) + end + local res + if pattern_map[type] then + res = pattern_map[type] + elseif type == 'q' then + -- some Lua pattern matching voodoo; we want to match '...' as + -- well as "...", and can use the fact that %n will match a + -- previous capture. Adding the extra field above comes from needing + -- to accommodate the extra spurious match (which is either ' or ") + addfield(name,type) + res = '(["\'])(.-)%'..(kount-2) + else + local endbracket = brackets[type] + if endbracket then + res = '(%b'..type..endbracket..')' + elseif stdclasses[type] or stdclasses[type:lower()] then + res = '(%'..type..'+)' + else + err = "unknown format type or character class" + end + end + return res + end)) + + if err then + return nil,err + else + return r,fieldnames,fieldtypes + end +end + + +local function tnumber (s) + return s == 'd' or s == 'i' or s == 'f' +end + +function sip.create_spec_fun(spec,options) + local fieldtypes,fieldnames + local ls = {} + spec,fieldnames,fieldtypes = sip.create_pattern(spec,options) + if not spec then return spec,fieldnames end + local named_vars = type(fieldnames[1]) == 'string' + for i = 1,#fieldnames do + append(ls,'mm'..i) + end + ls[1] = ls[1] or "mm1" -- behave correctly if there are no patterns + local fun = ('return (function(s,res)\n\tlocal %s = s:match(%q)\n'):format(concat(ls,','),spec) + fun = fun..'\tif not mm1 then return false end\n' + local k=1 + for i,f in ipairs(fieldnames) do + if f ~= '_' then + local var = 'mm'..i + if tnumber(fieldtypes[f]) then + var = 'tonumber('..var..')' + elseif brackets[fieldtypes[f]] then + var = var..':sub(2,-2)' + end + if named_vars then + fun = ('%s\tres.%s = %s\n'):format(fun,f,var) + else + if fieldtypes[f] ~= 'Q' then -- we skip the string-delim capture + fun = ('%s\tres[%d] = %s\n'):format(fun,k,var) + k = k + 1 + end + end + end + end + return fun..'\treturn true\nend)\n', named_vars +end + +--- convert a SIP pattern into a matching function. +-- The returned function takes two arguments, the line and an empty table. +-- If the line matched the pattern, then this function returns true +-- and the table is filled with field-value pairs. +-- @param spec a SIP pattern +-- @param options optional table; {at_start=true} ensures that the pattern +-- is anchored at the start of the string. +-- @return a function if successful, or nil,error +function sip.compile(spec,options) + assert_arg(1,spec,'string') + local fun,names = sip.create_spec_fun(spec,options) + if not fun then return nil,names end + if rawget(_G,'_DEBUG') then print(fun) end + local chunk,err = loadstring(fun,'tmp') + if err then return nil,err end + return chunk(),names +end + +local cache = {} + +--- match a SIP pattern against a string. +-- @param spec a SIP pattern +-- @param line a string +-- @param res a table to receive values +-- @param options (optional) option table +-- @return true or false +function sip.match (spec,line,res,options) + assert_arg(1,spec,'string') + assert_arg(2,line,'string') + assert_arg(3,res,'table') + if not cache[spec] then + cache[spec] = sip.compile(spec,options) + end + return cache[spec](line,res) +end + +--- match a SIP pattern against the start of a string. +-- @param spec a SIP pattern +-- @param line a string +-- @param res a table to receive values +-- @return true or false +function sip.match_at_start (spec,line,res) + return sip.match(spec,line,res,{at_start=true}) +end + +--- given a pattern and a file object, return an iterator over the results +-- @param spec a SIP pattern +-- @param f a file-like object. +function sip.fields (spec,f) + assert_arg(1,spec,'string') + if not f then return nil,"no file object" end + local fun,err = sip.compile(spec) + if not fun then return nil,err end + local res = {} + return function() + while true do + local line = f:read() + if not line then return end + if fun(line,res) then + local values = res + res = {} + return unpack(values) + end + end + end +end + +local read_patterns = {} + +--- register a match which will be used in the read function. +-- @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') + local pat,named = sip.compile(spec) + append(read_patterns,{pat=pat,named=named,callback=fun}) +end + +--- enter a loop which applies all registered matches to the input file. +-- @param f a file-like object +-- @array matches optional list of `{spec,fun}` pairs, as for `pattern` above. +function sip.read (f,matches) + local owned,err + if not f then return nil,"no file object" end + if type(f) == 'string' then + f,err = io.open(f) + if not f then return nil,err end + owned = true + end + if matches then + for _,p in ipairs(matches) do + sip.pattern(p[1],p[2]) + end + end + local res = {} + for line in f:lines() do + for _,item in ipairs(read_patterns) do + if item.pat(line,res) then + if item.callback then + if item.named then + item.callback(res) + else + item.callback(unpack(res)) + end + end + res = {} + break + end + end + end + if owned then f:close() end +end + +return sip -- cgit v1.2.3