chroma_solstice/libs/narrator/narrator/parser.lua
Your Name 03532d29e4 Vendor libs/hump, sfxrlua, json4lua, narrator as real files
These were committed as empty submodule gitlinks with no .gitmodules,
so the folders were never populated and the game could not load
(require of libs/hump/timer, libs/sfxrlua/sfxr, narrator.narrator).
Checked out each at its pinned commit and vendored the files, matching
the existing bundled-libs convention. Removed the junk libs/libs/ tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 11:37:09 -04:00

789 lines
No EOL
24 KiB
Lua

local lume = require('narrator.libs.lume')
local enums = require('narrator.enums')
--
-- LPeg
-- To allow to build in Defold
local lpeg_name = 'lpeg'
if not pcall(require, lpeg_name) then
return false
end
local lpeg = require(lpeg_name)
local S, C, P, V = lpeg.S, lpeg.C, lpeg.P, lpeg.V
local Cb, Ct, Cc, Cg = lpeg.Cb, lpeg.Ct, lpeg.Cc, lpeg.Cg
local Cmt = lpeg.Cmt
lpeg.locale(lpeg)
--
-- Parser
local parser = { }
local constructor = { }
---Parse ink content string
---@param content string
---@return Narrator.Book
function parser.parse(content)
--
-- Basic patterns
local function get_length(array) return
#array
end
local eof = -1
local sp = S(' \t') ^ 0
local ws = S(' \t\r\n') ^ 0
local nl = S('\r\n') ^ 1
local none = Cc(nil)
local divert_sign = P'->'
local gather_mark = sp * C('-' - divert_sign)
local gather_level = Cg(Ct(gather_mark ^ 1) / get_length + none, 'level')
local sticky_marks = Cg(Ct((sp * C('+')) ^ 1) / get_length, 'level') * Cg(Cc(true), 'sticky')
local choice_marks = Cg(Ct((sp * C('*')) ^ 1) / get_length, 'level') * Cg(Cc(false), 'sticky')
local choice_level = sticky_marks + choice_marks
local id = (lpeg.alpha + '_') * (lpeg.alnum + '_') ^ 0
local label = Cg('(' * sp * C(id) * sp * ')', 'label')
local address = id * ('.' * id) ^ -2
---Something for tunnels
local function check_tunnel(s, i, a)
local r = lpeg.match (sp * divert_sign, s, i)
return i, r ~= nil
end
-- TODO: Clean divert expression to divert and tunnel
local divert = divert_sign * sp * Cg(address, 'path') -- base search for divert symbol and path to follow
local check_tunnel = Cg(Cmt(Cb('path'), check_tunnel), 'tunnel') -- a weird way to to check tunnel
local opt_tunnel_sign = (sp * divert_sign * sp * (#nl + #S'#') ) ^ -1 -- tunnel sign in end of string, keep newline not consumed
divert = Cg(Ct(divert * sp * check_tunnel * opt_tunnel_sign), 'divert')
local divert_to_nothing = divert_sign * none
local exit_tunnel = Cg(divert_sign * divert_sign, 'exit')
local tag = '#' * sp * V'text'
local tags = Cg(Ct(tag * (sp * tag) ^ 0), 'tags')
local todo = sp * 'TODO:' * (1 - nl) ^ 0
local comment_line = sp * '//' * sp * (1 - nl) ^ 0
local comment_multi = sp * '/*' * ((P(1) - '*/') ^ 0) * '*/'
local comment = comment_line + comment_multi
local multiline_end = ws * '}'
--
-- Dynamic patterns and evaluation helpers
local function item_type(type)
return Cg(Cc(type), 'type')
end
local function balanced_multiline_item(is_restricted)
local is_restricted = is_restricted ~= nil and is_restricted or false
local paragraph = is_restricted and V'restricted_paragraph' or V'paragraph'
return sp * paragraph ^ -1 * sp * V'multiline_item' * sp * paragraph ^ -1 * ws
end
local function sentence_before(excluded, tailed)
local tailed = tailed or false
local character = P(1 - S(' \t')) - excluded
local pattern = (sp * character ^ 1) ^ 1
local with_tail = C(pattern * sp)
local without_tail = C(pattern) * sp
local without_tail_always = C(pattern) * sp * #(tags + nl)
return without_tail_always + (tailed and with_tail or without_tail)
end
local function unwrap_assignment(assignment)
local unwrapped = assignment
unwrapped = unwrapped:gsub('([%w_]*)%s*([%+%-])[%+%-]', '%1 = %1 %2 1')
unwrapped = unwrapped:gsub('([%w_]*)%s*([%+%-])=%s*(.*)', '%1 = %1 %2 %3')
local name, value = unwrapped:match('([%w_]*)%s*=%s*(.*)')
return name or '', value or assignment
end
local function check_special_escape(s, i, a)
if string.sub(s, i - 2, i - 2) == '\\' then
return
end
return i
end
--
-- Grammar rules
local ink_grammar = P({ 'root',
-- Root
root = ws * V'items' + eof,
items = Ct(V'item' ^ 0),
item = balanced_multiline_item() + V'singleline_item',
singleline_item = sp * (V'global' + V'statement' + V'paragraph' + V'gatherPoint') * ws,
multiline_item = ('{' * sp * (V'sequence' + V'switch') * sp * multiline_end) - V'inline_condition',
-- Gather points
gatherPoint = Ct(gather_level * sp * nl * item_type('gather')),
-- Global declarations
global =
Ct(V'inclusion' * item_type('inclusion')) +
Ct(V'list' * item_type('list')) +
Ct(V'constant' * item_type('constant')) +
Ct(V'variable' * item_type('variable'))
,
inclusion = 'INCLUDE ' * sp * Cg(sentence_before(nl + comment), 'filename'),
list = 'LIST ' * sp * V'assignment_pair',
constant = 'CONST ' * sp * V'assignment_pair',
variable = 'VAR ' * sp * V'assignment_pair',
-- Statements
statement =
Ct(V'return_from_func' * item_type('return')) +
Ct(V'assignment' * item_type('assignment')) +
Ct(V'func' * item_type('func')) +
Ct(V'knot' * item_type('knot')) +
Ct(V'stitch' * item_type('stitch')) +
Ct(V'choice' * item_type('choice')) +
comment + todo
,
section_name = C(id) * sp * P'=' ^ 0,
knot = P'==' * (P'=' ^ 0) * sp * Cg(V'section_name', 'knot'),
stitch = '=' * sp * Cg(V'section_name', 'stitch'),
func_param = sp * C(id) * sp * S','^0,
func_params = P'(' * Cg(Ct(V'func_param'^0), 'params') * P')',
function_name = P'function' * sp * Cg(id, 'name') * sp * V'func_params' * sp * P'=' ^ 0,
func = P'==' * (P'=' ^ 0) * sp * Cg(Ct(V'function_name'), 'func'),
return_from_func = sp * '~' * sp * P('return') * sp * Cg((P(1) - nl)^0, 'value') * nl ^ 0,
assignment = gather_level * sp * '~' * sp * V'assignment_temp' * sp * V'assignment_pair',
assignment_temp = Cg('temp' * Cc(true) + Cc(false), 'temp'),
assignment_pair = Cg(sentence_before(nl + comment) / unwrap_assignment, 'name') * Cg(Cb('name') / 2, 'value'),
choice_condition = Cg(V'expression' + none, 'condition'),
choice_fallback = choice_level * sp * V'label_optional' * sp * V'choice_condition' * sp * (divert + divert_to_nothing) * sp * V'tags_optional',
choice_normal = choice_level * sp * V'label_optional' * sp * V'choice_condition' * sp * Cg(V'text', 'text') * divert ^ -1 * sp * V'tags_optional',
choice = V'choice_fallback' + V'choice_normal',
-- Paragraph
paragraph = Ct(gather_level * sp * (V'paragraph_label' + V'paragraph_text' + V'paragraph_tags') * item_type('paragraph')),
paragraph_label = label * sp * Cg(V'text_optional', 'parts') * sp * V'tags_optional',
paragraph_text = V'label_optional' * sp * Cg(V'text_complex', 'parts') * sp * V'tags_optional',
paragraph_tags = V'label_optional' * sp * Cg(V'text_optional', 'parts') * sp * tags,
label_optional = label + none,
text_optional = V'text_complex' + none,
tags_optional = tags + none,
text_complex = Ct((Ct(
Cg(V'inline_condition', 'condition') +
Cg(V'inline_sequence', 'sequence') +
Cg(V'expression', 'expression') +
Cg(V'text' + ' ', 'text') * (exit_tunnel ^ -1) * (divert ^ -1) + exit_tunnel + divert
) - V'multiline_item') ^ 1),
special_check_escape = Cmt(S("{|}"), check_special_escape),
text = sentence_before(nl + exit_tunnel + divert + comment + tag + V'special_check_escape', true) - V'statement',
-- Inline expressions, conditions, sequences
expression = '{' * sp * sentence_before('}' + nl) * sp * '}',
inline_condition = '{' * sp * Ct(V'inline_if_else' + V'inline_if') * sp * '}',
inline_if = Cg(sentence_before(S':}' + nl), 'condition') * sp * ':' * sp * Cg(V'text_complex', 'success'),
inline_if_else = (V'inline_if') * sp * '|' * sp * Cg(V'text_complex', 'failure'),
inline_alt_empty = Ct(Ct(Cg(sp * Cc'', 'text') * sp * divert ^ -1)),
inline_alt = V'text_complex' + V'inline_alt_empty',
inline_alts = Ct(((sp * V'inline_alt' * sp * '|') ^ 1) * sp * V'inline_alt'),
inline_sequence = '{' * sp * (
'!' * sp * Ct(Cg(V'inline_alts', 'alts') * Cg(Cc('once'), 'sequence')) +
'&' * sp * Ct(Cg(V'inline_alts', 'alts') * Cg(Cc('cycle'), 'sequence')) +
'~' * sp * Ct(Cg(V'inline_alts', 'alts') * Cg(Cc('stopping'), 'sequence') * Cg(Cc(true), 'shuffle')) +
Ct(Cg(V'inline_alts', 'alts') * Cg(Cc('stopping'), 'sequence'))
) * sp * '}',
-- Multiline conditions and switches
switch = Ct((V'switch_comparative' + V'switch_conditional') * item_type('switch')),
switch_comparative = Cg(V'switch_condition', 'expression') * ws * Cg(Ct((sp * V'switch_case') ^ 1), 'cases'),
switch_conditional = Cg(Ct(V'switch_cases_headed' + V'switch_cases_only'), 'cases'),
switch_cases_headed = V'switch_if' * ((sp * V'switch_case') ^ 0),
switch_cases_only = ws * ((sp * V'switch_case') ^ 1),
switch_if = Ct(Cg(V'switch_condition', 'condition') * ws * Cg(Ct(V'switch_items'), 'node')),
switch_case = ('-' - divert_sign) * sp * V'switch_if',
switch_condition = sentence_before(':' + nl) * sp * ':' * sp * comment ^ -1,
switch_items = (V'restricted_item' - V'switch_case') ^ 1,
-- Multiline sequences
sequence = Ct((V'sequence_params' * sp * nl * sp * V'sequence_alts') * item_type('sequence')),
sequence_params = (
V'sequence_shuffle_optional' * sp * V'sequence_type' +
V'sequence_shuffle' * sp * V'sequence_type' +
V'sequence_shuffle' * sp * V'sequence_type_optional'
) * sp * ':' * sp * comment ^ -1,
sequence_shuffle_optional = V'sequence_shuffle' + Cg(Cc(false), 'shuffle'),
sequence_shuffle = Cg(P'shuffle' / function() return true end, 'shuffle'),
sequence_type_optional = V'sequence_type' + Cg(Cc'cycle', 'sequence'),
sequence_type = Cg(P'cycle' + 'stopping' + 'once', 'sequence'),
sequence_alts = Cg(Ct((sp * V'sequence_alt') ^ 1), 'alts'),
sequence_alt = ('-' - divert_sign) * ws * Ct(V'sequence_items'),
sequence_items = (V'restricted_item' - V'sequence_alt') ^ 1,
-- Restricted items inside multiline items
restricted_item = balanced_multiline_item(true) + V'restricted_singleline_item',
restricted_singleline_item = sp * (V'global' + V'restricted_statement' + V'restricted_paragraph' - multiline_end) * ws,
restricted_statement = Ct(
V'choice' * item_type('choice') +
V'assignment' * item_type('assignment')
) + comment + todo,
restricted_paragraph = Ct((
Cg(V'text_complex', 'parts') * sp * V'tags_optional' +
Cg(V'text_optional', 'parts') * sp * tags
) * item_type('paragraph'))
})
--
-- Result
local parsed_items = ink_grammar:match(content)
local book = constructor.construct_book(parsed_items)
return book
end
--
-- A book construction
function constructor.unescape(text)
local result = text
result = result:gsub('\\|', '|')
result = result:gsub('\\{', '{')
result = result:gsub('\\}', '}')
return result
end
function constructor.construct_book(items)
local construction = {
current_knot = '_',
current_stitch = '_',
variables_to_compute = { }
}
construction.book = {
inclusions = { },
lists = { },
constants = { },
variables = { },
params = { },
tree = { _ = { _ = { } } }
}
construction.book.version = {
engine = enums.engine_version,
tree = 1
}
construction.nodes_chain = {
construction.book.tree[construction.current_knot][construction.current_stitch]
}
constructor.add_node(construction, items)
constructor.clear(construction.book.tree)
constructor.compute_variables(construction)
return construction.book
end
function constructor:add_node(items, is_restricted)
local is_restricted = is_restricted ~= nil and is_restricted or false
for _, item in ipairs(items) do
if is_restricted then
-- Are not allowed inside multiline blocks by Ink rules:
-- a) nesting levels
-- b) choices without diverts
item.level = nil
if item.type == 'choice' and item.divert == nil then
item.type = nil
end
end
if item.type == 'inclusion' then
-- filename
constructor.add_inclusion(self, item.filename)
elseif item.type == 'list' then
-- name, value
constructor.add_list(self, item.name, item.value)
elseif item.type == 'constant' then
-- name, value
constructor.add_constant(self, item.name, item.value)
elseif item.type == 'variable' then
-- name, value
constructor.add_variable(self, item.name, item.value)
elseif item.type == 'func' then
-- function
constructor.add_function(self, item.func.name, item.func.params)
elseif item.type == 'knot' then
-- knot
constructor.add_knot(self, item.knot)
elseif item.type == 'stitch' then
-- stitch
constructor.add_stitch(self, item.stitch)
elseif item.type == 'switch' then
-- expression, cases
constructor.add_switch(self, item.expression, item.cases)
elseif item.type == 'sequence' then
-- sequence, shuffle, alts
constructor.add_sequence(self, item.sequence, item.shuffle, item.alts)
elseif item.type == 'assignment' then
-- level, name, value, temp
constructor.add_assignment(self, item.level, item.name, item.value, item.temp)
elseif item.type == 'return' then
constructor.add_return(self, item.value)
elseif item.type == 'paragraph' then
-- level, label, parts, tags
constructor.add_paragraph(self, item.level, item.label, item.parts, item.tags)
elseif item.type == 'gather' then
constructor.add_paragraph(self, item.level, "", nil, item.tags)
elseif item.type == 'choice' then
-- level, sticky, label, condition, text, divert, tags
constructor.add_choice(self, item.level, item.sticky, item.label, item.condition, item.text, item.divert, item.tags)
end
end
end
function constructor:add_inclusion(filename)
table.insert(self.book.inclusions, filename)
end
function constructor:add_list(name, value)
local items = lume.array(value:gmatch('[%w_%.]+'))
self.book.lists[name] = items
local switched = lume.array(value:gmatch('%b()'))
switched = lume.map(switched, function(item) return item:sub(2, #item - 1) end)
self.book.variables[name] = { [name] = { } }
lume.each(switched, function(item) self.book.variables[name][name][item] = true end)
end
function constructor:add_constant(constant, value)
local value = lume.deserialize(value)
self.book.constants[constant] = value
end
function constructor:add_variable(variable, value)
self.variables_to_compute[variable] = value
end
function constructor:add_function(fname, params)
local node = { }
self.book.tree[fname] = { ['_'] = node }
self.book.params[fname] = params
self.nodes_chain = { node }
end
function constructor:add_knot(knot)
self.current_knot = knot
self.current_stitch = '_'
local node = { }
self.book.tree[self.current_knot] = { [self.current_stitch] = node }
self.nodes_chain = { node }
end
function constructor:add_stitch(stitch)
-- If a root stitch is empty we need to add a divert to the first stitch in the ink file.
if self.current_stitch == '_' then
local root_stitch_node = self.book.tree[self.current_knot]._
if #root_stitch_node == 0 then
local divertItem = { divert = { path = stitch } }
table.insert(root_stitch_node, divertItem)
end
end
self.current_stitch = stitch
local node = { }
self.book.tree[self.current_knot][self.current_stitch] = node
self.nodes_chain = { node }
end
function constructor:add_switch(expression, cases)
if expression then
-- Convert switch cases to comparing conditions with expression
for _, case in ipairs(cases) do
if case.condition ~= 'else' then
case.condition = expression .. '==' .. case.condition
end
end
end
local item = {
condition = { },
success = { }
}
for _, case in ipairs(cases) do
if case.condition == 'else' then
local failure_node = { }
table.insert(self.nodes_chain, failure_node)
constructor.add_node(self, case.node, true)
table.remove(self.nodes_chain)
item.failure = failure_node
else
local success_node = { }
table.insert(self.nodes_chain, success_node)
constructor.add_node(self, case.node, true)
table.remove(self.nodes_chain)
table.insert(item.success, success_node)
table.insert(item.condition, case.condition)
end
end
constructor.add_item(self, nil, item)
end
function constructor:add_sequence(sequence, shuffle, alts)
local item = {
sequence = sequence,
shuffle = shuffle and true or nil,
alts = { }
}
for _, alt in ipairs(alts) do
local alt_node = { }
table.insert(self.nodes_chain, alt_node)
constructor.add_node(self, alt, true)
table.remove(self.nodes_chain)
table.insert(item.alts, alt_node)
end
constructor.add_item(self, nil, item)
end
function constructor:add_return(value)
local item = {
return_value = value
}
constructor.add_item(self, nil, item)
end
function constructor:add_assignment(level, name, value, temp)
local item = {
temp = temp or nil,
var = name,
value = value
}
constructor.add_item(self, level, item)
end
function constructor:add_paragraph(level, label, parts, tags)
local items = constructor.convert_paragraph_parts_to_items(parts, true)
items = items or { }
-- If the paragraph has a label or tags we need to place them as the first text item.
if label ~= nil or tags ~= nil then
local first_item
if #items > 0 and items[1].condition == nil then
first_item = items[1]
else
first_item = { }
table.insert(items, first_item)
end
first_item.label = label
first_item.tags = tags
end
for _, item in ipairs(items) do
constructor.add_item(self, level, item)
end
end
function constructor.convert_paragraph_parts_to_items(parts, is_root)
if parts == nil then return nil end
local is_root = is_root ~= nil and is_root or false
local items = { }
local item
for index, part in ipairs(parts) do
if part.condition then -- Inline condition part
item = {
condition = part.condition.condition,
success = constructor.convert_paragraph_parts_to_items(part.condition.success),
failure = constructor.convert_paragraph_parts_to_items(part.condition.failure)
}
table.insert(items, item)
item = nil
elseif part.sequence then -- Inline sequence part
item = {
sequence = part.sequence.sequence,
shuffle = part.sequence.shuffle and true or nil,
alts = { }
}
for _, alt in ipairs(part.sequence.alts) do
table.insert(item.alts, constructor.convert_paragraph_parts_to_items(alt))
end
table.insert(items, item)
item = nil
else -- Text, expression and divert may be
local is_divert_only = part.divert ~= nil and part.text == nil
if item == nil then
item = { text = (is_root or is_divert_only) and '' or '<>' }
end
if part.text then
item.text = item.text .. part.text:gsub('%s+', ' ')
item.text = constructor.unescape(item.text)
elseif part.expression then
item.text = item.text .. '#' .. part.expression .. '#'
end
if part.divert or part.exit then
item.exit = part.exit and true or nil
item.divert = part.divert
item.text = #item.text > 0 and (item.text .. '<>') or nil
table.insert(items, item)
item = nil
else
local next = parts[index + 1]
local next_is_block = next and not (next.text or next.expression)
if not next or next_is_block then
if not is_root or next_is_block then
item.text = item.text .. '<>'
end
table.insert(items, item)
item = nil
end
end
end
end
if is_root then
-- Add a safe prefix and suffix for correct conditions gluing
local first_item = items[1]
if first_item.text == nil and first_item.divert == nil and first_item.exit == nil then
table.insert(items, 1, { text = '' } )
end
local last_item = items[#items]
if last_item.text == nil and last_item.divert == nil and last_item.exit == nil then
table.insert(items, { text = '' } )
elseif last_item.text ~= nil and last_item.divert == nil then
last_item.text = last_item.text:gsub('(.-)%s*$', '%1')
end
end
return items
end
function constructor:add_choice(level, sticky, label, condition, sentence, divert, tags)
local item = {
sticky = sticky or nil,
condition = condition,
label = label,
divert = divert,
tags = tags
}
if sentence == nil then
item.choice = 0
else
local prefix, divider, suffix = sentence:match('(.*)%[(.*)%](.*)')
prefix = prefix or sentence
divider = divider or ''
suffix = suffix or ''
local text = (prefix .. suffix):gsub('%s+', ' ')
local choice = (prefix .. divider):gsub('%s+', ' '):gsub('^%s*(.-)%s*$', '%1')
if divert and #text > 0 and text:match('%S+') then
text = text .. '<>'
else
text = text:gsub('^%s*(.-)%s*$', '%1')
end
item.text = constructor.unescape(text)
item.choice = constructor.unescape(choice)
end
constructor.add_item(self, level, item)
if divert == nil then
item.node = { }
table.insert(self.nodes_chain, item.node)
end
end
function constructor:add_item(level, item)
local level = (level ~= nil and level > 0) and level or #self.nodes_chain
while #self.nodes_chain > level do
table.remove(self.nodes_chain)
end
local node = self.nodes_chain[#self.nodes_chain]
table.insert(node, item)
end
function constructor:compute_variable(variable, value)
local constant = self.book.constants[value]
if constant then
self.book.variables[variable] = constant
return
end
local list_expression = value:match('%(([%s%w%.,_]*)%)')
local item_expressions = list_expression and lume.array(list_expression:gmatch('[%w_%.]+')) or { value }
local list_variable = list_expression and { } or nil
for _, item_expression in ipairs(item_expressions) do
local list_part, item_part = item_expression:match('([%w_]+)%.([%w_]+)')
item_part = item_part or item_expression
for list_name, list_items in pairs(self.book.lists) do
local list_is_valid = list_part == nil or list_part == list_name
local item_is_found = lume.find(list_items, item_part)
if list_is_valid and item_is_found then
list_variable = list_variable or { }
list_variable[list_name] = list_variable[list_name] or { }
list_variable[list_name][item_part] = true
end
end
end
if list_variable then
self.book.variables[variable] = list_variable
else
self.book.variables[variable] = lume.deserialize(value)
end
end
function constructor:compute_variables()
for variable, value in pairs(self.variables_to_compute) do
constructor.compute_variable(self, variable, value)
end
end
function constructor.clear(tree)
for knot, node in pairs(tree) do
for stitch, node in pairs(node) do
constructor.clear_node(node)
end
end
end
function constructor.clear_node(node)
for index, item in ipairs(node) do
-- Simplify text only items
if item.text ~= nil and lume.count(item) == 1 then
node[index] = item.text
end
if item.node ~= nil then
-- Clear choice nodes
if #item.node == 0 then
item.node = nil
else
constructor.clear_node(item.node)
end
end
if item.success ~= nil then
-- Simplify single condition
if type(item.condition) == 'table' and #item.condition == 1 then
item.condition = item.condition[1]
end
-- Clear success nodes
if item.success[1] ~= nil and item.success[1][1] ~= nil then
for index, success_node in ipairs(item.success) do
constructor.clear_node(success_node)
if #success_node == 1 and type(success_node[1]) == 'string' then
item.success[index] = success_node[1]
end
end
if #item.success == 1 then
item.success = item.success[1]
end
else
constructor.clear_node(item.success)
if #item.success == 1 and type(item.success[1]) == 'string' then
item.success = item.success[1]
end
end
-- Clear failure nodes
if item.failure ~= nil then
constructor.clear_node(item.failure)
if #item.failure == 1 and type(item.failure[1]) == 'string' then
item.failure = item.failure[1]
end
end
end
if item.alts ~= nil then
for index, alt_node in ipairs(item.alts) do
constructor.clear_node(alt_node)
if #alt_node == 1 and type(alt_node[1]) == 'string' then
item.alts[index] = alt_node[1]
end
end
end
end
end
return parser