From 876135401c4dd134e6e0ca54d7dded67d010516c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Reuh=20Fildadut?= Date: Mon, 11 Nov 2024 10:23:21 +0100 Subject: [PATCH 1/2] [api] allow adding tags when running/evaluating --- anselme/parser/expression/block.lua | 1 + anselme/state/State.lua | 33 +++++++++++++++++------------ doc/api.md | 31 +++++++++++++++------------ 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/anselme/parser/expression/block.lua b/anselme/parser/expression/block.lua index ae00861..5a72146 100644 --- a/anselme/parser/expression/block.lua +++ b/anselme/parser/expression/block.lua @@ -1,5 +1,6 @@ local expression_to_ast = require("anselme.parser.expression.to_ast") +local utf8 = utf8 or require("lua-utf8") local ast = require("anselme.ast") local PartialScope, Block, Call, Identifier = ast.PartialScope, ast.Block, ast.Call, ast.Identifier diff --git a/anselme/state/State.lua b/anselme/state/State.lua index f3c4d35..69acfcd 100644 --- a/anselme/state/State.lua +++ b/anselme/state/State.lua @@ -7,11 +7,11 @@ local tag_manager = require("anselme.state.tag_manager") local event_manager = require("anselme.state.event_manager") local translation_manager = require("anselme.state.translation_manager") local persistent_manager = require("anselme.state.persistent_manager") -local uuid = require("anselme.common").uuid local parser = require("anselme.parser") local binser = require("anselme.lib.binser") -local assert0 = require("anselme.common").assert0 -local operator_priority = require("anselme.common").operator_priority +local common = require("anselme.common") +local uuid, assert0, operator_priority = common.uuid, common.assert0, common.operator_priority +local to_anselme = require("anselme.common.to_anselme") local anselme local Identifier, Return, Node @@ -178,12 +178,13 @@ State = class { --- Load a script in this branch. It will become the active script. -- -- `code` is the code string or AST to run. If `code` is a string, `source` is the source name string to show in errors (optional). + -- `tags` is an optional Lua table; its content will be added to the tags for the duration of the script. -- -- Note that this will only load the script; execution will only start by using the `:step` method. Will error if a script is already active in this State. - run = function(self, code, source) + run = function(self, code, source, tags) assert(not self:active(), "a script is already active") self._coroutine = coroutine.create(function() - local r = assert0(self:eval_local(code, source)) + local r = assert0(self:eval_local(code, source, tags)) event_manager:complete_flush(self) if Return:is(r) then r = r.expression end return "return", r @@ -191,11 +192,11 @@ State = class { end, --- Same as `:run`, but read the code from a file. -- `source` will be set as the file path. - run_file = function(self, path) + run_file = function(self, path, tags) local f = assert(io.open(path, "r")) local block = parser(f:read("a"), path) f:close() - return self:run(block) + return self:run(block, nil, tags) end, --- When a script is active, will resume running it until the next event. -- @@ -218,16 +219,17 @@ State = class { -- -- Will error if no script is active. -- - -- If `code` is given, the script will not be disabled but instead will be immediately replaced with this new script. + -- `code`, `source` and `tags` are all optional and have the same behaviour as in `:run`. + -- If they are given, the script will not be disabled but instead will be immediately replaced with this new script. -- The new script will then be started on the next `:step` and will preserve the current scope. This can be used to trigger an exit function or similar in the active script. -- -- If this is called from within a running script, this will raise an `interrupt` event in order to stop the current script execution. - interrupt = function(self, code, source) + interrupt = function(self, code, source, tags) assert(self:active(), "trying to interrupt but no script is currently active") local called_from_script = self:state() == "running" if code then self._coroutine = coroutine.create(function() - local r = assert0(self:eval_local(code, source)) + local r = assert0(self:eval_local(code, source, tags)) event_manager:complete_flush(self) self.scope:reset() -- scope stack is probably messed up after the switch if Return:is(r) then r = r.expression end @@ -244,19 +246,24 @@ State = class { -- -- This can be called from outside a running script, but an error will be triggered the expression raise any event other than return. -- + -- `code` is the code string or AST to run. If `code` is a string, `source` is the source name string to show in errors (optional). + -- `tags` is an optional Lua table; its content will be added to the tags for the duration of the expression. + -- -- * returns AST in case of success. Run `:to_lua(state)` on it to convert to a Lua value. -- * returns `nil, error message` in case of error. - eval = function(self, code, source) + eval = function(self, code, source, tags) self.scope:push_global() - local r, e = self:eval_local(code, source) + local r, e = self:eval_local(code, source, tags) self.scope:pop() return r, e end, --- Same as `:eval`, but evaluate the expression in the current scope. - eval_local = function(self, code, source) + eval_local = function(self, code, source, tags) if type(code) == "string" then code = parser(code, source) end local stack_size = self.scope:size() + if tags then tag_manager:push(self, to_anselme(tags)) end local s, e = pcall(code.eval, code, self) + if tags then tag_manager:pop(self) end if not s then self.scope:reset(stack_size) return nil, e diff --git a/doc/api.md b/doc/api.md index bde4601..eeea936 100644 --- a/doc/api.md +++ b/doc/api.md @@ -249,22 +249,23 @@ Returns `"inactive"` if no script is loaded. _defined at line 171 of [anselme/state/State.lua](../anselme/state/State.lua):_ `state = function(self)` -### :run (code, source) +### :run (code, source, tags) Load a script in this branch. It will become the active script. `code` is the code string or AST to run. If `code` is a string, `source` is the source name string to show in errors (optional). +`tags` is an optional Lua table; its content will be added to the tags for the duration of the script. Note that this will only load the script; execution will only start by using the `:step` method. Will error if a script is already active in this State. -_defined at line 183 of [anselme/state/State.lua](../anselme/state/State.lua):_ `run = function(self, code, source)` +_defined at line 184 of [anselme/state/State.lua](../anselme/state/State.lua):_ `run = function(self, code, source, tags)` -### :run_file (path) +### :run_file (path, tags) Same as `:run`, but read the code from a file. `source` will be set as the file path. -_defined at line 194 of [anselme/state/State.lua](../anselme/state/State.lua):_ `run_file = function(self, path)` +_defined at line 195 of [anselme/state/State.lua](../anselme/state/State.lua):_ `run_file = function(self, path, tags)` ### :step () @@ -274,37 +275,41 @@ Will error if no script is active. Returns `event type string, event data`. -_defined at line 205 of [anselme/state/State.lua](../anselme/state/State.lua):_ `step = function(self)` +_defined at line 206 of [anselme/state/State.lua](../anselme/state/State.lua):_ `step = function(self)` -### :interrupt (code, source) +### :interrupt (code, source, tags) Stops the currently active script. Will error if no script is active. -If `code` is given, the script will not be disabled but instead will be immediately replaced with this new script. +`code`, `source` and `tags` are all optional and have the same behaviour as in `:run`. +If they are given, the script will not be disabled but instead will be immediately replaced with this new script. The new script will then be started on the next `:step` and will preserve the current scope. This can be used to trigger an exit function or similar in the active script. If this is called from within a running script, this will raise an `interrupt` event in order to stop the current script execution. -_defined at line 225 of [anselme/state/State.lua](../anselme/state/State.lua):_ `interrupt = function(self, code, source)` +_defined at line 227 of [anselme/state/State.lua](../anselme/state/State.lua):_ `interrupt = function(self, code, source, tags)` -### :eval (code, source) +### :eval (code, source, tags) Evaluate an expression in the global scope. This can be called from outside a running script, but an error will be triggered the expression raise any event other than return. +`code` is the code string or AST to run. If `code` is a string, `source` is the source name string to show in errors (optional). +`tags` is an optional Lua table; its content will be added to the tags for the duration of the expression. + * returns AST in case of success. Run `:to_lua(state)` on it to convert to a Lua value. * returns `nil, error message` in case of error. -_defined at line 249 of [anselme/state/State.lua](../anselme/state/State.lua):_ `eval = function(self, code, source)` +_defined at line 254 of [anselme/state/State.lua](../anselme/state/State.lua):_ `eval = function(self, code, source, tags)` -### :eval_local (code, source) +### :eval_local (code, source, tags) Same as `:eval`, but evaluate the expression in the current scope. -_defined at line 256 of [anselme/state/State.lua](../anselme/state/State.lua):_ `eval_local = function(self, code, source)` +_defined at line 261 of [anselme/state/State.lua](../anselme/state/State.lua):_ `eval_local = function(self, code, source, tags)` If you want to perform more advanced manipulation of the resulting AST nodes, look at the `ast` modules. In particular, every Node inherits the methods from [ast.abstract.Node](../ast/abstract/Node.lua). @@ -312,4 +317,4 @@ Otherwise, each Node has its own module file defined in the [ast/](../ast) direc --- -_file generated at 2024-11-09T17:00:43Z_ \ No newline at end of file +_file generated at 2024-11-11T09:22:49Z_ \ No newline at end of file From 77c6ac6ba29daaa4d7e6b7c5888baf11c6ff68a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Reuh=20Fildadut?= Date: Mon, 11 Nov 2024 14:35:46 +0100 Subject: [PATCH 2/2] [api] add LuaText and document built-in event data --- anselme/ast/Choice.lua | 50 ++++++- anselme/ast/Struct.lua | 2 +- anselme/ast/Text.lua | 103 ++++++++++++++- anselme/init.lua | 13 +- anselme/state/State.lua | 2 + anselme/stdlib/language/frFR.lua | 2 +- doc/api.md | 217 +++++++++++++++++++++++++++---- doc/api.md.template | 39 +++++- doc/language.md | 4 + test/run.lua | 8 +- 10 files changed, 391 insertions(+), 49 deletions(-) diff --git a/anselme/ast/Choice.lua b/anselme/ast/Choice.lua index 82f6907..1f8d995 100644 --- a/anselme/ast/Choice.lua +++ b/anselme/ast/Choice.lua @@ -5,8 +5,56 @@ local Event = ast.abstract.Event local operator_priority = require("anselme.common").operator_priority +--- ChoiceEventData represent the data returned by an event with the type `"choice"`. +-- See the [language documentation](language.md#choices) for more details on how to create a choice event. +-- +-- A ChoiceEventData contains a list of [LuaText](#luatext), each LuaText representing a separate choice of the choice event. +-- +-- For example, the following Anselme script: +-- +-- ``` +-- *| Yes! +-- *| No. +-- ``` +-- will return a choice event containing two LuaTexts, the first containing the text "Yes!" and the second "No.". +-- +-- Usage: +-- ```lua +-- current_choice = nil +-- waiting_for_choice = false +-- +-- -- in your anselem event handling loop: +-- if not waiting_for_choice then +-- local event_type, event_data = run_state:step() +-- if event_type == "choice" then +-- -- event_data is a ChoiceEventData, i.e. a list of LuaText +-- for i, luatext in ipairs(event_data) do +-- write(("Choice number %s:"):format(i)) +-- -- luatext is a list of text parts { text = "text string", tags = { ... } } +-- for _, textpart in ipairs(luatext) do +-- write_choice_part_with_color(textpart.text, textpart.tags.color) +-- end +-- else +-- -- handle other event types... +-- end +-- current_choice = event_data +-- waiting_for_choice = true +-- end +-- end +-- +-- -- somewhere in your code where choices are selected +-- current_choice:select(choice_number) +-- waiting_for_choice = false +-- ``` +-- @title ChoiceEventData local ChoiceEventData = class { + -- [1] = LuaText, ... + _selected = nil, + + --- Choose the choice at position `choice` (number). + -- + -- A choice must be selected after receiving a choice event and before calling `:step` again. choose = function(self, choice) self._selected = choice end @@ -36,7 +84,7 @@ Choice = ast.abstract.Runtime(Event) { build_event_data = function(self, state, event_buffer) local l = ChoiceEventData:new() for _, c in event_buffer:iter(state) do - table.insert(l, c.text) + table.insert(l, c.text:to_lua(state)) end return l end, diff --git a/anselme/ast/Struct.lua b/anselme/ast/Struct.lua index 2dd8a53..be1ec2e 100644 --- a/anselme/ast/Struct.lua +++ b/anselme/ast/Struct.lua @@ -102,7 +102,7 @@ Struct = ast.abstract.Runtime { to_lua = function(self, state) local l = {} - for _, e in ipairs(self.table) do + for _, e in pairs(self.table) do l[e[1]:to_lua(state)] = e[2]:to_lua(state) end return l diff --git a/anselme/ast/Text.lua b/anselme/ast/Text.lua index 018a073..2e10edb 100644 --- a/anselme/ast/Text.lua +++ b/anselme/ast/Text.lua @@ -5,16 +5,101 @@ local ArgumentTuple, Struct local to_anselme = require("anselme.common.to_anselme") +--- A Lua-friendly representation of an Anselme Text value. +-- They appear in both TextEventData and ChoiceEventData to represent the text that has to be shown. +-- +-- It contains a list of _text parts_, which are parts of a single text, each part potentially having differrent tags attached. +-- A text will typically only consist of a single part unless it was built using text interpolation. +-- +-- Each text part is a table containing `text` (string) and `tags` (table) properties, for example: `{ text = "text part string", tags = { color = "red" } }`. +-- @title LuaText +-- @defer lua text +local LuaText +LuaText = class { + -- [1] = { text = "string", tags = { tag_name = value, ... } }, ... + + _state = nil, + + --- Anselme Text value this was created from. For advanced usage only. See the source file [Text.lua](anselme/ast/Text.lua) for more information. + -- @defer lua text + raw = nil, + + init = function(self, text, state) + self._state = state + self.raw = text + for _, e in ipairs(text.list) do + table.insert(self, { text = e[1]:to_lua(state), tags = e[2]:to_lua(state) }) + end + end, + + --- Returns a text representation of the LuaText, using Anselme's default formatting. Useful for debugging. + -- + -- Usage: `print(luatext)` + -- @defer lua text + __tostring = function(self) + return self.raw:format(self._state) + end +} + +--- TextEventData represent the data returned by an event with the type `"text"`. +-- See the [language documentation](language.md#texts) for more details on how to create a text event. +-- +-- A TextEventData contains a list of [LuaText](#luatext), each LuaText representing a separate line of the text event. +-- +-- For example, the following Anselme script: +-- +-- ``` +-- | Hi! +-- | My name's John. +-- ``` +-- will return a text event containing two LuaTexts, the first containing the text "Hi!" and the second "My name's John.". +-- +-- Usage: +-- ```lua +-- local event_type, event_data = run_state:step() +-- if event_type == "text" then +-- -- event_data is a TextEventData, i.e. a list of LuaText +-- for _, luatext in ipairs(event_data) do +-- -- luatext is a list of text parts { text = "text string", tags = { ... } } +-- for _, textpart in ipairs(luatext) do +-- write_text_part_with_color(textpart.text, textpart.tags.color) +-- end +-- write_text("\n") -- for example, if we want a newline between each text line +-- end +-- else +-- -- handle other event types... +-- end +-- ``` +-- @title TextEventData local TextEventData TextEventData = class { - -- returns a list of TextEventData where the first element of each text of each TextEventData has the same value for the tag tag_name + -- [1] = LuaText, ... + + --- Returns a list of TextEventData where the first part of each LuaText of each TextEventData has the same value for the tag `tag_name`. + -- + -- In other words, this groups all the LuaTexts contained in this TextEventData using the `tag_name` tag and returns a list containing these groups. + -- + -- For example, with the following Anselme script: + -- ``` + -- speaker: "John" # + -- | A + -- | B + -- speaker: "Lana" # + -- | C + -- speaker: "John" # + -- | D + -- ``` + -- calling `text_event_data:group_by("speaker")` will return a list of three TextEventData: + -- * the first with the texts "A" and "B"; both with the tag `speaker="John"` + -- * the second with the text "C"; with the tag `speaker="Lana"` + -- * the last with the text "D"; wiith the tag `speaker="John"` group_by = function(self, tag_name) local l = {} local current_group local tag_key = to_anselme(tag_name) local last_value - for _, event in ipairs(self) do - local list = event.list + for _, luatext in ipairs(self) do + local list = luatext.raw.list if #list > 0 then local value = list[1][2]:get_strict(tag_key) if (not current_group) or (last_value == nil and value) or (last_value and value == nil) or (last_value and value and last_value:hash() ~= value:hash()) then -- new group @@ -22,11 +107,11 @@ TextEventData = class { table.insert(l, current_group) last_value = value end - table.insert(current_group, event) -- add to current group + table.insert(current_group, luatext) -- add to current group end end return l - end, + end } local Text @@ -68,6 +153,10 @@ Text = Runtime(Event) { return ("| %s |"):format(table.concat(t, " ")) end, + to_lua = function(self, state) + return LuaText:new(self, state) + end, + -- autocall when used directly as a statement eval_statement = function(self, state) return self:call(state, ArgumentTuple:new()) @@ -77,8 +166,8 @@ Text = Runtime(Event) { build_event_data = function(self, state, event_buffer) local l = TextEventData:new() - for _, event in event_buffer:iter(state) do - table.insert(l, event) + for _, text in event_buffer:iter(state) do + table.insert(l, text:to_lua(state)) end return l end, diff --git a/anselme/init.lua b/anselme/init.lua index 90f6cc6..6a34c57 100644 --- a/anselme/init.lua +++ b/anselme/init.lua @@ -13,25 +13,20 @@ -- local state = anselme.new() -- state:load_stdlib() -- --- -- read an anselme script file --- local f = assert(io.open("script.ans")) --- local script = anselme.parse(f:read("a"), "script.ans") --- f:close() --- --- -- load the script in a new branch +-- -- load an anselme script file in a new branch -- local run_state = state:branch() --- run_state:run(script) +-- run_state:run_file("script.ans") -- -- -- run the script -- while run_state:active() do -- local e, data = run_state:step() -- if e == "text" then -- for _, l in ipairs(data) do --- print(l:format(run_state)) +-- print(l) -- end -- elseif e == "choice" then -- for i, l in ipairs(data) do --- print(("%s> %s"):format(i, l:format(run_state))) +-- print(("%s> %s"):format(i, l)) -- end -- local choice = tonumber(io.read("l")) -- data:choose(choice) diff --git a/anselme/state/State.lua b/anselme/state/State.lua index 69acfcd..d2f3c57 100644 --- a/anselme/state/State.lua +++ b/anselme/state/State.lua @@ -203,6 +203,8 @@ State = class { -- Will error if no script is active. -- -- Returns `event type string, event data`. + -- + -- See the [events](#events) section for details on event data types for built-in events. step = function(self) assert(self:active(), "trying to step but no script is currently active") local success, type, data = coroutine.resume(self._coroutine) diff --git a/anselme/stdlib/language/frFR.lua b/anselme/stdlib/language/frFR.lua index e5bd1cd..3e4242e 100644 --- a/anselme/stdlib/language/frFR.lua +++ b/anselme/stdlib/language/frFR.lua @@ -56,7 +56,7 @@ return [[ :@arrondi = stdlib.round :@aléatoire = stdlib.rand -:@égal = stdlib.equal +:@est égal = stdlib.is equal :@est = stdlib.is :@est une ancre = stdlib.is anchor :@est un booléen = stdlib.is boolean diff --git a/doc/api.md b/doc/api.md index eeea936..e61c28f 100644 --- a/doc/api.md +++ b/doc/api.md @@ -1,6 +1,6 @@ This document describes how to use the main Anselme modules. This is generated automatically from the source files. -Note that this file only describes the `anselme` and `state.State` modules, which are only a selection of what I consider to be the "public API" of Anselme that I will try to keep stable. +Note that this file only describes the `anselme` and `state.State` modules, as well as the `TextEventData` and `ChoiceEventData` classes, which are only a selection of what I consider to be the "public API" of Anselme that I will try to keep stable. If you need more advanced control on Anselme, feel free to look into the other source files to find more; the most useful functions should all be reasonably commented. # anselme @@ -15,25 +15,20 @@ local anselme = require("anselme") local state = anselme.new() state:load_stdlib() --- read an anselme script file -local f = assert(io.open("script.ans")) -local script = anselme.parse(f:read("a"), "script.ans") -f:close() - --- load the script in a new branch +-- load an anselme script file in a new branch local run_state = state:branch() -run_state:run(script) +run_state:run_file("script.ans") -- run the script while run_state:active() do local e, data = run_state:step() if e == "text" then for _, l in ipairs(data) do - print(l:format(run_state)) + print(l) end elseif e == "choice" then for i, l in ipairs(data) do - print(("%s> %s"):format(i, l:format(run_state))) + print(("%s> %s"):format(i, l)) end local choice = tonumber(io.read("l")) data:choose(choice) @@ -56,31 +51,31 @@ Anselme expects that `require("anselme.module")` will try loading both `anselme/ Global version string. Follow semver. -_defined at line 58 of [anselme/init.lua](../anselme/init.lua):_ `version = "2.0.0-rc1",` +_defined at line 53 of [anselme/init.lua](../anselme/init.lua):_ `version = "2.0.0-rc1",` ### .versions Table containing per-category version numbers. Incremented by one for any change that may break compatibility. -_defined at line 61 of [anselme/init.lua](../anselme/init.lua):_ `versions = {` +_defined at line 56 of [anselme/init.lua](../anselme/init.lua):_ `versions = {` #### .language Version number for language and standard library changes. -_defined at line 63 of [anselme/init.lua](../anselme/init.lua):_ `language = 31,` +_defined at line 58 of [anselme/init.lua](../anselme/init.lua):_ `language = 31,` #### .save Version number for save/AST format changes. -_defined at line 65 of [anselme/init.lua](../anselme/init.lua):_ `save = 7,` +_defined at line 60 of [anselme/init.lua](../anselme/init.lua):_ `save = 7,` #### .api Version number for Lua API changes. -_defined at line 67 of [anselme/init.lua](../anselme/init.lua):_ `api = 10` +_defined at line 62 of [anselme/init.lua](../anselme/init.lua):_ `api = 10` ### .parse (code, source) @@ -94,14 +89,14 @@ local ast = anselme.parse("1 + 2", "test") ast:eval(state) ``` -_defined at line 79 of [anselme/init.lua](../anselme/init.lua):_ `parse = function(code, source)` +_defined at line 74 of [anselme/init.lua](../anselme/init.lua):_ `parse = function(code, source)` ### .parse_file (path) Same as `:parse`, but reads the code from a file. `source` will be set as the file path. -_defined at line 84 of [anselme/init.lua](../anselme/init.lua):_ `parse_file = function(path)` +_defined at line 79 of [anselme/init.lua](../anselme/init.lua):_ `parse_file = function(path)` ### .generate_translation_template (code, source) @@ -109,20 +104,20 @@ Generates and return Anselme code (as a string) that can be used as a base for a This will include every translatable element found in this code. `source` is an optional string; it will be used as the code source name in translation contexts. -_defined at line 93 of [anselme/init.lua](../anselme/init.lua):_ `generate_translation_template = function(code, source)` +_defined at line 88 of [anselme/init.lua](../anselme/init.lua):_ `generate_translation_template = function(code, source)` ### .generate_translation_template_file (path) Same as `:generate_translation_template`, but reads the code from a file. `source` will be set as the file path. -_defined at line 98 of [anselme/init.lua](../anselme/init.lua):_ `generate_translation_template_file = function(path)` +_defined at line 93 of [anselme/init.lua](../anselme/init.lua):_ `generate_translation_template_file = function(path)` ### .new () Return a new [State](#state). -_defined at line 102 of [anselme/init.lua](../anselme/init.lua):_ `new = function()` +_defined at line 97 of [anselme/init.lua](../anselme/init.lua):_ `new = function()` # State @@ -275,7 +270,9 @@ Will error if no script is active. Returns `event type string, event data`. -_defined at line 206 of [anselme/state/State.lua](../anselme/state/State.lua):_ `step = function(self)` +See the [events](#events) section for details on event data types for built-in events. + +_defined at line 208 of [anselme/state/State.lua](../anselme/state/State.lua):_ `step = function(self)` ### :interrupt (code, source, tags) @@ -289,7 +286,7 @@ The new script will then be started on the next `:step` and will preserve the cu If this is called from within a running script, this will raise an `interrupt` event in order to stop the current script execution. -_defined at line 227 of [anselme/state/State.lua](../anselme/state/State.lua):_ `interrupt = function(self, code, source, tags)` +_defined at line 229 of [anselme/state/State.lua](../anselme/state/State.lua):_ `interrupt = function(self, code, source, tags)` ### :eval (code, source, tags) @@ -303,18 +300,188 @@ This can be called from outside a running script, but an error will be triggered * returns AST in case of success. Run `:to_lua(state)` on it to convert to a Lua value. * returns `nil, error message` in case of error. -_defined at line 254 of [anselme/state/State.lua](../anselme/state/State.lua):_ `eval = function(self, code, source, tags)` +_defined at line 256 of [anselme/state/State.lua](../anselme/state/State.lua):_ `eval = function(self, code, source, tags)` ### :eval_local (code, source, tags) Same as `:eval`, but evaluate the expression in the current scope. -_defined at line 261 of [anselme/state/State.lua](../anselme/state/State.lua):_ `eval_local = function(self, code, source, tags)` +_defined at line 263 of [anselme/state/State.lua](../anselme/state/State.lua):_ `eval_local = function(self, code, source, tags)` If you want to perform more advanced manipulation of the resulting AST nodes, look at the `ast` modules. In particular, every Node inherits the methods from [ast.abstract.Node](../ast/abstract/Node.lua). Otherwise, each Node has its own module file defined in the [ast/](../ast) directory. +# Events + +Anselme scripts communicate with the game by sending events. See the [language documentation](language.md#events) for more details on events. + +Custom events can be defined; to do so, simply yield the coroutine with your custom event type (using `coroutine.yield("event type", event_data)`) from a function called in the anselme script. + +For example, to add a `wait` event that pauses the script for some time, you could do something along these lines: +```lua +state:define("wait", "(duration::is number)", function(duration) coroutine.yield("wait", duration) end) +waiting = false + +-- and edit your Anselme event handler with something like: +if not waiting then + local event_type, event_data = run_state = run_state:step() + if e == "wait" then + waiting = true + call_after_duration(event_data, function() waiting = false end) + else + -- handle other event types... + end +end +``` + +And then from your Anselme script: +``` +| Hello... --- -_file generated at 2024-11-11T09:22:49Z_ \ No newline at end of file +wait(5) +| ...world ! +``` + +## TextEventData + +TextEventData represent the data returned by an event with the type `"text"`. +See the [language documentation](language.md#texts) for more details on how to create a text event. + +A TextEventData contains a list of [LuaText](#luatext), each LuaText representing a separate line of the text event. + +For example, the following Anselme script: + +``` +| Hi! +| My name's John. +``` +will return a text event containing two LuaTexts, the first containing the text "Hi!" and the second "My name's John.". + +Usage: +```lua +local event_type, event_data = run_state:step() +if event_type == "text" then + -- event_data is a TextEventData, i.e. a list of LuaText + for _, luatext in ipairs(event_data) do + -- luatext is a list of text parts { text = "text string", tags = { ... } } + for _, textpart in ipairs(luatext) do + write_text_part_with_color(textpart.text, textpart.tags.color) + end + write_text("\n") -- for example, if we want a newline between each text line + end +else +-- handle other event types... +end +``` + +_defined at line 74 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `local TextEventData` + +### :group_by (tag_name) + +Returns a list of TextEventData where the first part of each LuaText of each TextEventData has the same value for the tag `tag_name`. + +In other words, this groups all the LuaTexts contained in this TextEventData using the `tag_name` tag and returns a list containing these groups. + +For example, with the following Anselme script: +``` +speaker: "John" # + | A + | B +speaker: "Lana" # + | C +speaker: "John" # + | D +``` +calling `text_event_data:group_by("speaker")` will return a list of three TextEventData: +* the first with the texts "A" and "B"; both with the tag `speaker="John"` +* the second with the text "C"; with the tag `speaker="Lana"` +* the last with the text "D"; wiith the tag `speaker="John"` + +_defined at line 96 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `group_by = function(self, tag_name)` + + +## ChoiceEventData + +ChoiceEventData represent the data returned by an event with the type `"choice"`. +See the [language documentation](language.md#choices) for more details on how to create a choice event. + +A ChoiceEventData contains a list of [LuaText](#luatext), each LuaText representing a separate choice of the choice event. + +For example, the following Anselme script: + +``` +*| Yes! +*| No. +``` +will return a choice event containing two LuaTexts, the first containing the text "Yes!" and the second "No.". + +Usage: +```lua +current_choice = nil +waiting_for_choice = false + +-- in your anselem event handling loop: +if not waiting_for_choice then + local event_type, event_data = run_state:step() + if event_type == "choice" then + -- event_data is a ChoiceEventData, i.e. a list of LuaText + for i, luatext in ipairs(event_data) do + write(("Choice number %s:"):format(i)) + -- luatext is a list of text parts { text = "text string", tags = { ... } } + for _, textpart in ipairs(luatext) do + write_choice_part_with_color(textpart.text, textpart.tags.color) + end + else + -- handle other event types... + end + current_choice = event_data + waiting_for_choice = true + end +end + +-- somewhere in your code where choices are selected +current_choice:select(choice_number) +waiting_for_choice = false +``` + +_defined at line 50 of [anselme/ast/Choice.lua](../anselme/ast/Choice.lua):_ `local ChoiceEventData = class {` + +### :choose (choice) + +Choose the choice at position `choice` (number). + +A choice must be selected after receiving a choice event and before calling `:step` again. + +_defined at line 58 of [anselme/ast/Choice.lua](../anselme/ast/Choice.lua):_ `choose = function(self, choice)` + + +## LuaText + +A Lua-friendly representation of an Anselme Text value. +They appear in both TextEventData and ChoiceEventData to represent the text that has to be shown. + +It contains a list of _text parts_, which are parts of a single text, each part potentially having differrent tags attached. +A text will typically only consist of a single part unless it was built using text interpolation. + +Each text part is a table containing `text` (string) and `tags` (table) properties, for example: `{ text = "text part string", tags = { color = "red" } }`. + +_defined at line 17 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `local LuaText` + +### .raw + +Anselme Text value this was created from. For advanced usage only. See the source file [Text.lua](anselme/ast/Text.lua) for more information. + +_defined at line 25 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `raw = nil,` + +### :__tostring () + +Returns a text representation of the LuaText, using Anselme's default formatting. Useful for debugging. + +Usage: `print(luatext)` + +_defined at line 39 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `__tostring = function(self)` + +--- +_file generated at 2024-11-11T13:33:43Z_ \ No newline at end of file diff --git a/doc/api.md.template b/doc/api.md.template index e5bea15..635034a 100644 --- a/doc/api.md.template +++ b/doc/api.md.template @@ -1,6 +1,6 @@ This document describes how to use the main Anselme modules. This is generated automatically from the source files. -Note that this file only describes the `anselme` and `state.State` modules, which are only a selection of what I consider to be the "public API" of Anselme that I will try to keep stable. +Note that this file only describes the `anselme` and `state.State` modules, as well as the `TextEventData` and `ChoiceEventData` classes, which are only a selection of what I consider to be the "public API" of Anselme that I will try to keep stable. If you need more advanced control on Anselme, feel free to look into the other source files to find more; the most useful functions should all be reasonably commented. # anselme @@ -10,3 +10,40 @@ If you need more advanced control on Anselme, feel free to look into the other s # State {{anselme/state/State.lua}} + +# Events + +Anselme scripts communicate with the game by sending events. See the [language documentation](language.md#events) for more details on events. + +Custom events can be defined; to do so, simply yield the coroutine with your custom event type (using `coroutine.yield("event type", event_data)`) from a function called in the anselme script. + +For example, to add a `wait` event that pauses the script for some time, you could do something along these lines: +```lua +state:define("wait", "(duration::is number)", function(duration) coroutine.yield("wait", duration) end) +waiting = false + +-- and edit your Anselme event handler with something like: +if not waiting then + local event_type, event_data = run_state = run_state:step() + if e == "wait" then + waiting = true + call_after_duration(event_data, function() waiting = false end) + else + -- handle other event types... + end +end +``` + +And then from your Anselme script: +``` +| Hello... +--- +wait(5) +| ...world ! +``` + +{{anselme/ast/Text.lua}} + +{{anselme/ast/Choice.lua}} + +{{:lua text}} \ No newline at end of file diff --git a/doc/language.md b/doc/language.md index c534bc9..bd5fdca 100644 --- a/doc/language.md +++ b/doc/language.md @@ -887,6 +887,8 @@ A text event value can be created using the [text literal](#text). A text is wri text! ``` +How the data returned by a Text event is structured is detailled in the [API documentation](api.md#texteventdata). + ### Choices Choice events are intended to represent a player choice in the host game. Each choice event in the buffer list is intended to represent a distinct choice. @@ -902,6 +904,8 @@ A choice event can be written to the buffer using the `*_` operator on a text ev | Choice B has been selected. ``` +How the data returned by a Text event is structured is detailled in the [API documentation](api.md#choiceeventdata). + ### Tags Text and choice events can also carry metadata through tags. Tags are stored as a [struct](#struct). diff --git a/test/run.lua b/test/run.lua index 3a6cb7b..dea9aac 100644 --- a/test/run.lua +++ b/test/run.lua @@ -38,14 +38,14 @@ local function run_loop(run_state, write_output, interactive) for _, v in ipairs(grouped) do if groups then write_output(":: group ::") end for _, l in ipairs(v) do - write_output(l:format(run_state)) + write_output(l.raw:format(run_state)) end end elseif e == "choice" then local choice if interactive then for i, l in ipairs(data) do - write_output(("%s> %s"):format(i, l:format(run_state))) + write_output(("%s> %s"):format(i, l.raw:format(run_state))) end io.write(("Select choice (1-%s): "):format(#data)) choice = tonumber(io.read("l")) @@ -53,9 +53,9 @@ local function run_loop(run_state, write_output, interactive) choice = assert(run_state:eval_local("choice"), "no choice selected"):to_lua() for i, l in ipairs(data) do if i == choice then - write_output(("=> %s"):format(l:format(run_state))) + write_output(("=> %s"):format(l.raw:format(run_state))) else - write_output((" > %s"):format(l:format(run_state))) + write_output((" > %s"):format(l.raw:format(run_state))) end end end