summaryrefslogtreecommitdiff
path: root/macros/luatex/generic/lparse/lparse.lua
diff options
context:
space:
mode:
Diffstat (limited to 'macros/luatex/generic/lparse/lparse.lua')
-rw-r--r--macros/luatex/generic/lparse/lparse.lua302
1 files changed, 302 insertions, 0 deletions
diff --git a/macros/luatex/generic/lparse/lparse.lua b/macros/luatex/generic/lparse/lparse.lua
new file mode 100644
index 0000000000..755cf2883f
--- /dev/null
+++ b/macros/luatex/generic/lparse/lparse.lua
@@ -0,0 +1,302 @@
+-- lparse.lua
+-- Copyright 2023 Josef Friedrich
+--
+-- This work may be distributed and/or modified under the
+-- conditions of the LaTeX Project Public License, either version 1.3c
+-- of this license or (at your option) any later version.
+-- The latest version of this license is in
+-- http://www.latex-project.org/lppl.txt
+-- and version 1.3c or later is part of all distributions of LaTeX
+-- version 2008/05/04 or later.
+--
+-- This work has the LPPL maintenance status `maintained'.
+--
+-- The Current Maintainer of this work is Josef Friedrich.
+--
+-- This work consists of the files lparse.lua, lparse.tex,
+-- and lparse.sty.
+---
+if lpeg == nil then
+ lpeg = require('lpeg')
+end
+
+---
+---@param spec string
+---@return Argument[]
+local function parse_spec(spec)
+ local V = lpeg.V
+ local P = lpeg.P
+ local Set = lpeg.S
+ local Range = lpeg.R
+ local CaptureFolding = lpeg.Cf
+ local CaptureTable = lpeg.Ct
+ local Cc = lpeg.Cc
+ local CaptureSimple = lpeg.C
+
+ local function add_result(result, value)
+ if not result then
+ result = {}
+ end
+ table.insert(result, value)
+ return result
+ end
+
+ local function collect_delims(a, b)
+ return { init_delim = a, end_delim = b }
+ end
+
+ local function collect_token(a)
+ return { token = a }
+ end
+
+ local function set_default(a)
+ return { default = a }
+ end
+
+ local function combine(...)
+ local args = { ... }
+
+ local output = {}
+
+ for _, arg in ipairs(args) do
+ if type(arg) ~= 'table' then
+ arg = {}
+ end
+
+ for key, value in pairs(arg) do
+ output[key] = value
+ end
+
+ end
+
+ return output
+ end
+
+ local function ArgumentType(letter)
+ local function get_type(l)
+ return { argument_type = l }
+ end
+ return CaptureSimple(P(letter)) / get_type
+ end
+
+ local T = ArgumentType
+
+ local pattern = P({
+ 'init',
+ init = V('whitespace') ^ 0 *
+ CaptureFolding(CaptureTable('') * V('list'), add_result),
+
+ list = (V('arg') * V('whitespace') ^ 1) ^ 0 * V('arg') ^ -1,
+
+ arg = V('m') + V('r') + V('R') + V('v') + V('o') + V('d') + V('O') +
+ V('D') + V('s') + V('t'),
+
+ m = T('m') / combine,
+
+ r = T('r') * V('delimiters') / combine,
+
+ R = T('R') * V('delimiters') * V('default') / combine,
+
+ v = T('v') * Cc({ verbatim = true }) / combine,
+
+ o = T('o') * Cc({ optional = true }) / combine,
+
+ d = T('d') * V('delimiters') * Cc({ optional = true }) / combine,
+
+ O = T('O') * V('default') * Cc({ optional = true }) / combine,
+
+ D = T('D') * V('delimiters') * V('default') *
+ Cc({ optional = true }) / combine,
+
+ s = T('s') * Cc({ star = true }) / combine,
+
+ t = T('t') * V('token') / combine,
+
+ token = V('delimiter') / collect_token,
+
+ delimiter = CaptureSimple(Range('!~')),
+
+ delimiters = V('delimiter') * V('delimiter') / collect_delims,
+
+ whitespace = Set(' \t\n\r'),
+
+ default = P('{') * CaptureSimple((1 - P('}')) ^ 0) * P('}') /
+ set_default,
+ })
+
+ return pattern:match(spec)
+
+end
+
+---
+---Scan for an optional argument.
+---
+---@param init_delim? string # The character that marks the beginning of an optional argument (by default `[`).
+---@param end_delim? string # The character that marks the end of an optional argument (by default `]`).
+---
+---@return string|nil # The string that was enclosed by the delimiters. The delimiters themselves are not returned.
+local function scan_delimited(init_delim, end_delim)
+ if init_delim == nil then
+ init_delim = '['
+ end
+ if end_delim == nil then
+ end_delim = ']'
+ end
+
+ ---
+ ---@param t Token
+ ---
+ ---@return string
+ local function convert_token_to_string(t)
+ if t.index ~= nil then
+ return utf8.char(t.index)
+ else
+ return '\\' .. t.csname
+ end
+ end
+
+ local delimiter_stack = 0
+
+ local function get_next_char()
+ local t = token.get_next()
+ local char = convert_token_to_string(t)
+ if char == init_delim then
+ delimiter_stack = delimiter_stack + 1
+ end
+
+ if char == end_delim then
+ delimiter_stack = delimiter_stack - 1
+ end
+ return char, t
+ end
+
+ local char, t = get_next_char()
+
+ if t.cmdname == 'spacer' then
+ char, t = get_next_char()
+ end
+
+ if char == init_delim then
+ local output = {}
+
+ char, t = get_next_char()
+
+ -- “while” better than “repeat ... until”: The end_delimiter is
+ -- included in the result output.
+ while not (char == end_delim and delimiter_stack == 0) do
+ table.insert(output, char)
+ char, t = get_next_char()
+ end
+ return table.concat(output, '')
+ else
+ token.put_next(t)
+ end
+end
+
+---@class Argument
+---@field argument_type? string
+---@field optional? boolean
+---@field init_delim? string
+---@field end_delim? string
+---@field dest? string
+---@field star? boolean
+---@field default? string
+---@field verbatim? boolean
+---@field token? string
+
+---@class Parser
+---@field args Argument[]
+---@field result any[]
+local Parser = {}
+---@private
+Parser.__index = Parser
+
+function Parser:new(spec)
+ local parser = {}
+ setmetatable(parser, Parser)
+ parser.spec = spec
+ parser.args = parse_spec(spec)
+ parser.result = parser:parse(parser.args)
+ return parser
+end
+
+---@return any[]
+function Parser:parse()
+ local result = {}
+ local index = 1
+ for _, arg in pairs(self.args) do
+ if arg.star then
+ -- s
+ result[index] = token.scan_keyword('*')
+ elseif arg.token then
+ -- t
+ result[index] = token.scan_keyword(arg.token)
+ elseif arg.optional then
+ -- o d O D
+ local oarg = scan_delimited(arg.init_delim, arg.end_delim)
+ if arg.default and oarg == nil then
+ oarg = arg.default
+ end
+ result[index] = oarg
+ elseif arg.init_delim and arg.end_delim then
+ -- r R
+ local oarg = scan_delimited(arg.init_delim, arg.end_delim)
+ if arg.default and oarg == nil then
+ oarg = arg.default
+ end
+ if oarg == nil then
+ tex.error('Missing required argument')
+ end
+ result[index] = oarg
+ else
+ -- m v
+ local marg = token.scan_argument(arg.verbatim ~= true)
+ if marg == nil then
+ tex.error('Missing required argument')
+ end
+ result[index] = marg
+ end
+ index = index + 1
+ end
+ return result
+end
+
+---@private
+function Parser:set_result(...)
+ self.result = { ... }
+end
+
+function Parser:assert(...)
+ local arguments = { ... }
+ for index, arg in ipairs(arguments) do
+ assert(self.result[index] == arg, string.format(
+ 'Argument at index %d doesn’t match: “%s” != “%s”',
+ index, self.result[index], arg))
+ end
+end
+
+---
+---@return string|boolean|nil ...
+function Parser:export()
+ -- #self.arg: to get all elements of the result table, also elements
+ -- with nil values.
+ return table.unpack(self.result, 1, #self.args)
+end
+
+function Parser:debug()
+ for index = 1, #self.args do
+ print(index, self.result[index])
+ end
+end
+
+---@return Parser
+local function create_parser(spec)
+ return Parser:new(spec)
+end
+
+local function scan(spec)
+ local parser = create_parser(spec)
+ return parser:export()
+end
+
+return { Parser = create_parser, scan = scan, parse_spec = parse_spec }