diff --git a/anselme.lua b/anselme.lua index 17cff83..5d13315 100644 --- a/anselme.lua +++ b/anselme.lua @@ -157,7 +157,7 @@ local interpreter_methods = { r, e = eval(self.state, expr) end if not r then coroutine.yield("error", e) end - if self.state.interpreter.event_buffer then -- flush final events + if self.state.interpreter.current_event then -- flush final events local rf, re = run_line(self.state, { type = "flush_events" }) if re then coroutine.yield("error", re) end if rf then r = rf end @@ -478,15 +478,14 @@ local vm_mt = { coroutine = coroutine.create(function() return "return", interpreter:run(expr, namespace) end), -- status running_line = nil, - -- events - event_type = nil, - event_buffer = nil, -- choice event choice_selected = nil, -- skip next choices until next event change (to skip currently running choice block when resuming from a checkpoint) skip_choices_until_flush = nil, - -- captured events stack {[event type]=stack{fn, ...}, ...} - event_capture_stack = {}, + -- active event buffer stack + event_buffer_stack = {}, + -- current event waiting to be sent + current_event = nil, -- interrupt interrupt = nil, -- tag stack diff --git a/interpreter/common.lua b/interpreter/common.lua index e166e19..6127dd1 100644 --- a/interpreter/common.lua +++ b/interpreter/common.lua @@ -219,84 +219,95 @@ common = { --- event buffer management -- i.e. only for text and choice events events = { - --- add a new element to the event buffer - -- will flush if needed - -- returns true in case of success - -- returns nil, err in case of error + --- add a new element to the last event in the current buffer + -- will create new event if needed append = function(self, state, type, data) - if state.interpreter.event_capture_stack[type] then - local r, e = state.interpreter.event_capture_stack[type][#state.interpreter.event_capture_stack[type]](data) - if not r then return r, e end - else - local r, e = self:make_space_for(state, type) - if not r then return r, e end - - if not state.interpreter.event_buffer then - state.interpreter.event_type = type - state.interpreter.event_buffer = {} - end - - table.insert(state.interpreter.event_buffer, data) + local buffer = self:current_buffer(state) + local last = buffer[#buffer] + if not last or last.type ~= type then + last = { type = type } + table.insert(buffer, last) end - return true - end, - --- add a new item in the last element (a list of elements) of the event buffer - -- will flush if needed - -- will use default or a new list if buffer is empty - -- returns true in case of success - -- returns nil, err in case of error - append_in_last = function(self, state, type, data, default) - local r, e = self:make_space_for(state, type) - if not r then return r, e end - - if not state.interpreter.event_buffer then - r, e = self:append(state, type, default or {}) - if not r then return r, e end - end - - table.insert(state.interpreter.event_buffer[#state.interpreter.event_buffer], data) - - return true + table.insert(last, data) end, - --- start capturing events of a certain type - -- when an event of the type is appended, fn will be called with this event data - -- and the event will not be added to the event buffer - -- fn returns nil, err in case of error - push_capture = function(self, state, type, fn) - if not state.interpreter.event_capture_stack[type] then - state.interpreter.event_capture_stack[type] = {} - end - table.insert(state.interpreter.event_capture_stack[type], fn) + --- new events will be collected in this event buffer (any table) until the next pop + -- this is handled by a stack so nesting is allowed + push_buffer = function(self, state, buffer) + table.insert(state.interpreter.event_buffer_stack, buffer) end, --- stop capturing events of a certain type. - -- must be called after a push_capture - -- this is handled by a stack so nested capturing is allowed. - pop_capture = function(self, state, type) - table.remove(state.interpreter.event_capture_stack[type]) - if #state.interpreter.event_capture_stack[type] == 0 then - state.interpreter.event_capture_stack[type] = nil - end + -- must be called after a push_buffer + pop_buffer = function(self, state) + table.remove(state.interpreter.event_buffer_stack) + end, + --- returns the current buffer + current_buffer = function(self, state) + return state.interpreter.event_buffer_stack[#state.interpreter.event_buffer_stack] end, -- flush event buffer if it's neccessary to push an event of the given type -- returns true in case of success -- returns nil, err in case of error make_space_for = function(self, state, type) - if state.interpreter.event_buffer and state.interpreter.event_type ~= type and not state.interpreter.event_capture_stack[type] then - return self:flush(state) + if #state.interpreter.event_buffer_stack == 0 and state.interpreter.current_event and state.interpreter.current_event.type ~= type then -- FIXME useful? + return self:manual_flush(state) end return true end, + + --- write all the data in a buffer into the current buffer, or to the game is no buffer is currently set + write_buffer = function(self, state, buffer) + for _, event in ipairs(buffer) do + if #state.interpreter.event_buffer_stack == 0 then + if event.type == "flush" then + local r, e = self:manual_flush(state) + if not r then return r, e end + elseif state.interpreter.current_event then + if state.interpreter.current_event.type == event.type then + for _, v in ipairs(event) do + table.insert(state.interpreter.current_event, v) + end + else + local r, e = self:manual_flush(state) + if not r then return r, e end + state.interpreter.current_event = event + end + else + state.interpreter.current_event = event + end + else + local current_buffer = self:current_buffer(state) + table.insert(current_buffer, event) + end + end + return true + end, + + --- same as manual_flush but add the flush to the current buffer if one is set instead of directly to the game + flush = function(self, state) + if #state.interpreter.event_buffer_stack == 0 then + return self:manual_flush(state) + else + local current_buffer = self:current_buffer(state) + table.insert(current_buffer, { type = "flush" }) + return true + end + end, + --- flush events and send them to the game if possible -- returns true in case of success -- returns nil, err in case of error - flush = function(self, state) - while state.interpreter.event_buffer do - local type, buffer = state.interpreter.event_type, state.interpreter.event_buffer - state.interpreter.event_type = nil - state.interpreter.event_buffer = nil + manual_flush = function(self, state) + while state.interpreter.current_event do + local event = state.interpreter.current_event + state.interpreter.current_event = nil + + local type, buffer = event.type, event + buffer.type = nil + state.interpreter.skip_choices_until_flush = nil + -- choice processing local choices if type == "choice" then diff --git a/interpreter/expression.lua b/interpreter/expression.lua index 1526d1d..d0e1495 100644 --- a/interpreter/expression.lua +++ b/interpreter/expression.lua @@ -23,7 +23,7 @@ local function eval(state, exp) } -- string elseif exp.type == "string" then - local t, e = eval_text(state, exp.value) + local t, e = eval_text(state, exp.text) if not t then return t, e end return { type = "string", @@ -32,7 +32,7 @@ local function eval(state, exp) -- parentheses elseif exp.type == "parentheses" then return eval(state, exp.expression) - -- list parentheses + -- list defined in brackets elseif exp.type == "list_brackets" then if exp.expression then local v, e = eval(state, exp.expression) @@ -52,7 +52,7 @@ local function eval(state, exp) value = {} } end - -- list + -- list defined using , operator elseif exp.type == "list" then local flat = flatten_list(exp) local l = {} @@ -65,17 +65,19 @@ local function eval(state, exp) type = "list", value = l } - -- text: only triggered from choice/text lines + -- event buffer with from a text line elseif exp.type == "text" then - local currentTags = tags:current(state) + local l = {} + events:push_buffer(state, l) + local current_tags = tags:current(state) local v, e = eval_text_callback(state, exp.text, function(text) - local v2, e2 = events:append(state, "text", { text = text, tags = currentTags }) - if not v2 then return v2, e2 end + events:append(state, "text", { text = text, tags = current_tags }) end) + events:pop_buffer(state) if not v then return v, e end return { - type = "nil", - value = nil + type = "eventbuffer", + value = l } -- assignment elseif exp.type == ":=" then diff --git a/interpreter/interpreter.lua b/interpreter/interpreter.lua index cc67c33..950d9ff 100644 --- a/interpreter/interpreter.lua +++ b/interpreter/interpreter.lua @@ -33,17 +33,38 @@ run_line = function(state, line) elseif line.type == "choice" then local v, e = events:make_space_for(state, "choice") if not v then return v, ("%s; in automatic event flush at %s"):format(e, line.source) end - local currentTags = tags:current(state) - local choice_block_state = { tags = currentTags, block = line.child } - v, e = events:append(state, "choice", { _state = choice_block_state }) -- new choice - if not v then return v, e end - events:push_capture(state, "text", function(event) - local v2, e2 = events:append_in_last(state, "choice", event, { _state = choice_block_state }) - if not v2 then return v2, e2 end - end) v, e = eval(state, line.text) - events:pop_capture(state, "text") if not v then return v, ("%s; at %s"):format(e, line.source) end + -- convert text events to choices + if v.type == "eventbuffer" then + local current_tags = tags:current(state) + local choice_block_state = { tags = current_tags, block = line.child } + local final_buffer = {} + for _, event in ipairs(v.value) do + if event.type == "text" then + -- create new choice block if needed + local last_choice_block = final_buffer[#final_buffer] + if not last_choice_block or last_choice_block.type ~= "choice" then + last_choice_block = { type = "choice" } + table.insert(final_buffer, last_choice_block) + end + -- create new choice item in choice block if needed + local last_choice = last_choice_block[#last_choice_block] + if not last_choice then + last_choice = { _state = choice_block_state } + table.insert(last_choice_block, last_choice) + end + -- add text to last choice item + for _, txt in ipairs(event) do + table.insert(last_choice, txt) + end + else + table.insert(final_buffer, event) + end + end + v, e = events:write_buffer(state, final_buffer) + if not v then return v, ("%s; at %s"):format(e, line.source) end + end elseif line.type == "tag" then local v, e = eval(state, line.expression) if not v then return v, ("%s; at %s"):format(e, line.source) end @@ -61,6 +82,10 @@ run_line = function(state, line) if not v then return v, ("%s; in automatic event flush at %s"):format(e, line.source) end v, e = eval(state, line.text) if not v then return v, ("%s; at %s"):format(e, line.source) end + if v.type == "eventbuffer" then + v, e = events:write_buffer(state, v.value) + if not v then return v, ("%s; at %s"):format(e, line.source) end + end elseif line.type == "flush_events" then local v, e = events:flush(state) if not v then return v, ("%s; in event flush at %s"):format(e, line.source) end diff --git a/parser/common.lua b/parser/common.lua index 9a41cfb..da0738b 100644 --- a/parser/common.lua +++ b/parser/common.lua @@ -135,18 +135,17 @@ common = { return t end, -- parse interpolated expressions in a text + -- type sets the type of the returned expression (text is in text field) -- allow_subtext (bool) to enable or not [subtext] support -- if allow_binops is given, if one of the caracters of allow_binops appear unescaped in the text, it will interpreter a binary operator expression - -- * returns a text expression, remaining (if the right expression stop before the end of the text) - -- if allow_binops is not given: - -- * returns a list of strings and expressions (text elements) + -- * returns an expression with given type (string by default) and as a value a list of strings and expressions (text elements) + -- * if allow_binops is given, also returns remaining string (if the right expression stop before the end of the text) -- * nil, err: in case of error - parse_text = function(text, state, namespace, allow_binops, allow_subtext, in_subtext) + parse_text = function(text, state, namespace, type, allow_binops, allow_subtext, in_subtext) local l = {} - local text_exp + local text_exp = { type = type, text = l } local delimiters = "" if allow_binops then - text_exp = { type = "text", text = l } delimiters = allow_binops end if allow_subtext then @@ -182,7 +181,7 @@ common = { text = rem:match("^%s*}(.*)$") -- start subtext elseif allow_subtext and r:match("^%[") then - local exp, rem = common.parse_text(r:gsub("^%[", ""), state, namespace, allow_binops, allow_subtext, true) + local exp, rem = common.parse_text(r:gsub("^%[", ""), state, namespace, "text", allow_binops, allow_subtext, true) if not exp then return nil, rem end if not rem:match("^%]") then return nil, ("expected closing ] at end of subtext before %q"):format(rem) end -- add to text @@ -193,7 +192,7 @@ common = { if allow_binops then return text_exp, r else - return l + return text_exp end -- binop expression at the end of the text elseif allow_binops and r:match(("^[%s]"):format(allow_binops)) then @@ -209,7 +208,7 @@ common = { if allow_binops then return text_exp, "" else - return l + return text_exp end end, -- find compatible function variants from a fully qualified name diff --git a/parser/expression.lua b/parser/expression.lua index ab7462b..ed83ede 100644 --- a/parser/expression.lua +++ b/parser/expression.lua @@ -29,6 +29,30 @@ local unops_prio = { [11] = {}, } +local function get_text_in_litteral(s, start_pos) + local d, r + -- find end of string + start_pos = start_pos or 2 + local i = start_pos + while true do + local skip + skip = s:match("^[^%\\\"]-%b{}()", i) -- skip interpolated expressions + if skip then i = skip end + skip = s:match("^[^%\\\"]-\\.()", i) -- skip escape codes (need to skip every escape code in order to correctly parse \\": the " is not escaped) + if skip then i = skip end + if not skip then -- nothing skipped + local end_pos = s:match("^[^%\"]-\"()", i) -- search final double quote + if end_pos then + d, r = s:sub(start_pos, end_pos-2), s:sub(end_pos) + break + else + return nil, ("expected \" to finish string near %q"):format(s:sub(i)) + end + end + end + return d, r +end + --- parse an expression -- return expr, remaining if success -- returns nil, err if error @@ -48,32 +72,16 @@ local function expression(s, state, namespace, current_priority, operating_on) }) -- string elseif s:match("^%\"") then - local d, r - -- find end of string - local i = 2 - while true do - local skip - skip = s:match("^[^%\\\"]-%b{}()", i) -- skip interpolated expressions - if skip then i = skip end - skip = s:match("^[^%\\\"]-\\.()", i) -- skip escape codes (need to skip every escape code in order to correctly parse \\": the " is not escaped) - if skip then i = skip end - if not skip then -- nothing skipped - local end_pos = s:match("^[^%\"]-\"()", i) -- search final double quote - if end_pos then - d, r = s:sub(2, end_pos-2), s:sub(end_pos) - break - else - return nil, ("expected \" to finish string near %q"):format(s:sub(i)) - end - end - end - -- parse interpolated expressions - local l, e = parse_text(d, state, namespace) + local d, r = get_text_in_litteral(s) + local l, e = parse_text(d, state, namespace, "string") -- parse interpolated expressions if not l then return l, e end - return expression(r, state, namespace, current_priority, { - type = "string", - value = l - }) + return expression(r, state, namespace, current_priority, l) + -- text + elseif s:match("^t%\"") then + local d, r = get_text_in_litteral(s, 3) + local l, e = parse_text(d, state, namespace, "text", nil, true) -- parse interpolated expressions and subtext + if not l then return l, e end + return expression(r, state, namespace, current_priority, l) -- paranthesis elseif s:match("^%b()") then local content, r = s:match("^(%b())(.*)$") @@ -119,7 +127,7 @@ local function expression(s, state, namespace, current_priority, operating_on) type = "list", left = { type = "string", - value = { name } + text = { name } }, right = val } diff --git a/parser/postparser.lua b/parser/postparser.lua index f51eccc..f8d85a1 100644 --- a/parser/postparser.lua +++ b/parser/postparser.lua @@ -57,7 +57,7 @@ local function parse(state) end -- text (text & choice lines) if line.text then - local txt, err = parse_text(line.text, state, namespace, "#~", true) + local txt, err = parse_text(line.text, state, namespace, "text", "#~", true) if not txt then return nil, ("%s; at %s"):format(err, line.source) end if err:match("[^%s]") then return nil, ("expected end of expression in end-of-text expression before %q"):format(err) end line.text = txt diff --git a/stdlib/types.lua b/stdlib/types.lua index 4460b69..543e449 100644 --- a/stdlib/types.lua +++ b/stdlib/types.lua @@ -1,4 +1,4 @@ -local format, to_lua, from_lua +local format, to_lua, from_lua, events, anselme local types = {} types.lua = { @@ -131,6 +131,13 @@ types.anselme = { return { [k] = v } end }, + eventbuffer = { + format = function(val) + local v, e = events:write_buffer(anselme.running.state, val) + if not v then return v, e end + return "" + end, + }, type = { format = function(val) local k, ke = format(val[1]) @@ -149,6 +156,7 @@ types.anselme = { package.loaded[...] = types local common = require((...):gsub("stdlib%.types$", "interpreter.common")) -format, to_lua, from_lua = common.format, common.to_lua, common.from_lua +format, to_lua, from_lua, events = common.format, common.to_lua, common.from_lua, common.events +anselme = require((...):gsub("stdlib%.types$", "anselme")) return types diff --git a/test/tests/eventbuffer text litteral.ans b/test/tests/eventbuffer text litteral.ans new file mode 100644 index 0000000..c6edba8 --- /dev/null +++ b/test/tests/eventbuffer text litteral.ans @@ -0,0 +1,6 @@ +:a = t"Some text." +:b = t"Tagged text" # 1 + +a: {a} + +b: {b} diff --git a/test/tests/eventbuffer text litteral.lua b/test/tests/eventbuffer text litteral.lua new file mode 100644 index 0000000..6c41755 --- /dev/null +++ b/test/tests/eventbuffer text litteral.lua @@ -0,0 +1,32 @@ +local _={} +_[13]={1} +_[12]={} +_[11]={} +_[10]={} +_[9]={tags=_[13],text="Tagged text"} +_[8]={tags=_[12],text="b: "} +_[7]={tags=_[11],text="Some text."} +_[6]={tags=_[10],text="a: "} +_[5]={_[8],_[9]} +_[4]={_[6],_[7]} +_[3]={"return"} +_[2]={"text",_[5]} +_[1]={"text",_[4]} +return {_[1],_[2],_[3]} +--[[ +{ "text", { { + tags = {}, + text = "a: " + }, { + tags = {}, + text = "Some text." + } } } +{ "text", { { + tags = {}, + text = "b: " + }, { + tags = { 1 }, + text = "Tagged text" + } } } +{ "return" } +]]-- \ No newline at end of file