Clean up docs and more funcparser fixes
This commit is contained in:
parent
c65c68e4c2
commit
c9d9e9c6f8
21 changed files with 438 additions and 512 deletions
|
|
@ -1,7 +1,5 @@
|
||||||
# The Inline Function Parser
|
# The Inline Function Parser
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
The [FuncParser](api:evennia.utils.funcparser#evennia.utils.funcparser.FuncParser) extracts and executes
|
The [FuncParser](api:evennia.utils.funcparser#evennia.utils.funcparser.FuncParser) extracts and executes
|
||||||
'inline functions'
|
'inline functions'
|
||||||
embedded in a string on the form `$funcname(args, kwargs)`. Under the hood, this will
|
embedded in a string on the form `$funcname(args, kwargs)`. Under the hood, this will
|
||||||
|
|
@ -11,26 +9,25 @@ the return from the function.
|
||||||
```python
|
```python
|
||||||
from evennia.utils.funcparser import FuncParser
|
from evennia.utils.funcparser import FuncParser
|
||||||
|
|
||||||
def _square_callable(*args, **kwargs):
|
def _power_callable(*args, **kwargs):
|
||||||
"""This will be callable as $square(number) in string"""
|
"""This will be callable as $square(number, power=<num>) in string"""
|
||||||
return float(args[0]) ** 2
|
pow = int(kwargs.get('power', 2))
|
||||||
|
return float(args[0]) ** pow
|
||||||
|
|
||||||
parser = FuncParser({"square": _square_callable})
|
parser = FuncParser({"pow": _power_callable})
|
||||||
|
|
||||||
```
|
```
|
||||||
Next, just pass a string into the parser, optionally containing `$func(...)` markers:
|
Next, just pass a string into the parser, optionally containing `$func(...)` markers:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
parser.parse("We have that 4 x 4 x 4 is $pow(4, power=3).")
|
||||||
parser.parse("We have that 4 x 4 is $square(4).")
|
"We have that 4 x 4 x 4 is 64."
|
||||||
"We have that 4 x 4 is 16."
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Normally the return is always converted to a string but you can also get the actual data type from the call:
|
Normally the return is always converted to a string but you can also get the actual data type from the call:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
parser.parse_to_any("$square(4)")
|
parser.parse_to_any("$pow(4)")
|
||||||
16
|
16
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -38,8 +35,8 @@ To show a `$func()` verbatim in your code without parsing it, escape it as eithe
|
||||||
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
parser.parse("This is an escaped $$square(4) and so is this \$square(3)")
|
parser.parse("This is an escaped $$pow(4) and so is this \$pow(3)")
|
||||||
"This is an escaped $square(4) and so is this $square(3)"
|
"This is an escaped $pow(4) and so is this $pow(3)"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Uses in default Evennia
|
## Uses in default Evennia
|
||||||
|
|
@ -47,14 +44,14 @@ parser.parse("This is an escaped $$square(4) and so is this \$square(3)")
|
||||||
The FuncParser can be applied to any string. Out of the box it's applied in a few situations:
|
The FuncParser can be applied to any string. Out of the box it's applied in a few situations:
|
||||||
|
|
||||||
- _Outgoing messages_. All messages sent from the server is processed through FuncParser and every
|
- _Outgoing messages_. All messages sent from the server is processed through FuncParser and every
|
||||||
callable is provided the [Session](Sessions) of the object receiving the message. This potentially
|
callable is provided the [Session](./Sessions) of the object receiving the message. This potentially
|
||||||
allows a message to be modified on the fly to look different for different recipients.
|
allows a message to be modified on the fly to look different for different recipients.
|
||||||
- _Prototype values_. A [Prototype](Prototypes) dict's values are run through the parser such that every
|
- _Prototype values_. A [Prototype](./Prototypes) dict's values are run through the parser such that every
|
||||||
callable gets a reference to the rest of the prototype. In the Prototype ORM, this would allow builders
|
callable gets a reference to the rest of the prototype. In the Prototype ORM, this would allow builders
|
||||||
to safely call functions to set non-string values to prototype values, get random values, reference
|
to safely call functions to set non-string values to prototype values, get random values, reference
|
||||||
other fields of the prototype, and more.
|
other fields of the prototype, and more.
|
||||||
- _Actor-stance in messages to others_. In the
|
- _Actor-stance in messages to others_. In the
|
||||||
[Object.msg_contents](api:evennia.objects.objects#objects.objects.DefaultObject.msg_contents) method,
|
[Object.msg_contents](api:evennia.objects.objects#DefaultObject.msg_contents) method,
|
||||||
the outgoing string is parsed for special `$You()` and `$conj()` callables to decide if a given recipient
|
the outgoing string is parsed for special `$You()` and `$conj()` callables to decide if a given recipient
|
||||||
should see "You" or the character's name.
|
should see "You" or the character's name.
|
||||||
|
|
||||||
|
|
@ -115,20 +112,18 @@ The other arguments to the parser:
|
||||||
Here's an example of using the default/reserved keywords:
|
Here's an example of using the default/reserved keywords:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|
||||||
def _test(*args, **kwargs):
|
def _test(*args, **kwargs):
|
||||||
# do stuff
|
# do stuff
|
||||||
return something
|
return something
|
||||||
|
|
||||||
parser = funcparser.FuncParser({"test": _test}, mydefault=2)
|
parser = funcparser.FuncParser({"test": _test}, mydefault=2)
|
||||||
result = parser.parse("$test(foo, bar=4)", myreserved=[1, 2, 3])
|
result = parser.parse("$test(foo, bar=4)", myreserved=[1, 2, 3])
|
||||||
|
|
||||||
```
|
```
|
||||||
Here the callable will be called as
|
Here the callable will be called as
|
||||||
|
|
||||||
```python
|
```python
|
||||||
_test('foo', bar='4', mydefault=2, myreserved=[1, 2, 3],
|
_test('foo', bar='4', mydefault=2, myreserved=[1, 2, 3],
|
||||||
funcparser=<FuncParser>, raise_errrors=False)
|
funcparser=<FuncParser>, raise_errrors=False)
|
||||||
```
|
```
|
||||||
|
|
||||||
The `mydefault=2` kwarg could be overwritten if we made the call as `$test(mydefault=...)`
|
The `mydefault=2` kwarg could be overwritten if we made the call as `$test(mydefault=...)`
|
||||||
|
|
@ -154,8 +149,8 @@ an example of an `$toint` function; it converts numbers to integers.
|
||||||
"There's a $toint(22.0)% chance of survival."
|
"There's a $toint(22.0)% chance of survival."
|
||||||
|
|
||||||
What will enter the `$toint` callable (as `args[0]`) is the _string_ `"22.0"`. The function is responsible
|
What will enter the `$toint` callable (as `args[0]`) is the _string_ `"22.0"`. The function is responsible
|
||||||
for converting this to a float so it can operate on it. And also to properly handle invalid inputs (like
|
for converting this to a number so that we can convert it to an integer. We must also properly handle invalid
|
||||||
non-numbers). Common is to just return the input as-is or return the empty string.
|
inputs (like non-numbers).
|
||||||
|
|
||||||
If you want to mark an error, raise `evennia.utils.funcparser.ParsingError`. This stops the entire parsing
|
If you want to mark an error, raise `evennia.utils.funcparser.ParsingError`. This stops the entire parsing
|
||||||
of the string and may or may not raise the exception depending on what you set `raise_errors` to when you
|
of the string and may or may not raise the exception depending on what you set `raise_errors` to when you
|
||||||
|
|
@ -167,12 +162,12 @@ Python's `literal_eval` and/or `simple_eval`.
|
||||||
|
|
||||||
"There's a $toint($eval(10 * 2.2))% chance of survival."
|
"There's a $toint($eval(10 * 2.2))% chance of survival."
|
||||||
|
|
||||||
Since the `$eval` is the innermost call, it will get a sring as input - the string `"10 * 2.2"`.
|
Since the `$eval` is the innermost call, it will get a string as input - the string `"10 * 2.2"`.
|
||||||
It evaluates this and returns the `float` `22.0`. This time the outermost `$toint` will be called with
|
It evaluates this and returns the `float` `22.0`. This time the outermost `$toint` will be called with
|
||||||
this `float` instead of with a string.
|
this `float` instead of with a string.
|
||||||
|
|
||||||
> Since you don't know in which order users will nest or not nest your callables, it's important to
|
> It's important to safely validate your inputs since users may end up nesting your callables in any order.
|
||||||
> safely validate your inputs. See the next section for useful tools to do this.
|
> See the next section for useful tools to help with this.
|
||||||
|
|
||||||
In these examples, the result will be embedded in the larger string, so the result of the entire parsing
|
In these examples, the result will be embedded in the larger string, so the result of the entire parsing
|
||||||
will be a string:
|
will be a string:
|
||||||
|
|
@ -195,16 +190,16 @@ parser.parse_to_any("$toint($eval(10 * 2.2)")
|
||||||
### Safe convertion of inputs
|
### Safe convertion of inputs
|
||||||
|
|
||||||
Since you don't know in which order users may use your callables, they should always check the types
|
Since you don't know in which order users may use your callables, they should always check the types
|
||||||
of its inputs and convert it to type the callable needs. Note also that this limits what inputs you can
|
of its inputs and convert to the type the callable needs. Note also that when converting from strings,
|
||||||
support since some things (such as complex classes/callables etc) are just not safe/possible to
|
there are limits what inputs you can support. This is because FunctionParser strings are often used by
|
||||||
convert from string representation.
|
non-developer players/builders and some things (such as complex classes/callables etc) are just not
|
||||||
|
safe/possible to convert from string representation.
|
||||||
|
|
||||||
In `evennia.utils.utils` is a helper called
|
In `evennia.utils.utils` is a helper called
|
||||||
[safe_convert_to_types](api.evennia.utils.utils#evennia.utils.utils.safe_convert_to_types). This function
|
[safe_convert_to_types](api:evennia.utils.utils#evennia.utils.utils.safe_convert_to_types). This function
|
||||||
automates the conversion of simple data types in a safe way:
|
automates the conversion of simple data types in a safe way:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|
||||||
from evennia.utils.utils import safe_convert_to_types
|
from evennia.utils.utils import safe_convert_to_types
|
||||||
|
|
||||||
def _process_callable(*args, **kwargs):
|
def _process_callable(*args, **kwargs):
|
||||||
|
|
@ -225,7 +220,6 @@ def _process_callable(*args, **kwargs):
|
||||||
In other words,
|
In other words,
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|
||||||
args, kwargs = safe_convert_to_type(
|
args, kwargs = safe_convert_to_type(
|
||||||
(tuple_of_arg_converters, dict_of_kwarg_converters), *args, **kwargs)
|
(tuple_of_arg_converters, dict_of_kwarg_converters), *args, **kwargs)
|
||||||
```
|
```
|
||||||
|
|
@ -245,12 +239,12 @@ following tools (which you may also find useful to experiment with on your own):
|
||||||
containers like lists/dicts etc, so this and `literal_eval` are complementary to each other.
|
containers like lists/dicts etc, so this and `literal_eval` are complementary to each other.
|
||||||
|
|
||||||
```warning::
|
```warning::
|
||||||
It may be tempting to run use Python's in-built `eval()` or `exec()` functions as converters since these
|
It may be tempting to run use Python's in-built ``eval()`` or ``exec()`` functions as converters since
|
||||||
are able to convert any valid Python source code to Python. NEVER DO THIS unless you really, really
|
these are able to convert any valid Python source code to Python. NEVER DO THIS unless you really, really
|
||||||
know ONLY developers will ever send input to the callable. The parser is intended
|
know that ONLY developers will ever modify the string going into the callable. The parser is intended
|
||||||
for untrusted users (if you were trusted you'd have access to Python already). Letting untrusted users
|
for untrusted users (if you were trusted you'd have access to Python already). Letting untrusted users
|
||||||
pass strings to eval/exec is a MAJOR security risk. It allows the caller to effectively run arbitrary
|
pass strings to ``eval``/``exec`` is a MAJOR security risk. It allows the caller to run arbitrary
|
||||||
Python code on your server. This is the way to maliciously deleted hard drives. Just don't do it and
|
Python code on your server. This is the path to maliciously deleted hard drives. Just don't do it and
|
||||||
sleep better at night.
|
sleep better at night.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -264,16 +258,16 @@ more to them when you create your `FuncParser` instance to have those callables
|
||||||
|
|
||||||
These are the 'base' callables.
|
These are the 'base' callables.
|
||||||
|
|
||||||
- `$eval(expression)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_eval) -
|
- `$eval(expression)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_eval)) -
|
||||||
this uses `literal_eval` and `simple_eval` (see previous section) attemt to convert a string expression
|
this uses `literal_eval` and `simple_eval` (see previous section) attemt to convert a string expression
|
||||||
to a python object. This handles e.g. lists of literals `[1, 2, 3]` and simple expressions like `"1 + 2"`.
|
to a python object. This handles e.g. lists of literals `[1, 2, 3]` and simple expressions like `"1 + 2"`.
|
||||||
- `$toint(number)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_toint)) -
|
- `$toint(number)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_toint)) -
|
||||||
always converts an output to an integer, if possible.
|
always converts an output to an integer, if possible.
|
||||||
- `$add/sub/mult/div(obj1, obj2)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_add))) -
|
- `$add/sub/mult/div(obj1, obj2)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_add)) -
|
||||||
this adds/subtracts/multiplies and divides to elements together. While simple addition could be done with
|
this adds/subtracts/multiplies and divides to elements together. While simple addition could be done with
|
||||||
`$eval`, this could for example be used also to add two lists together, which is not possible with `eval`;
|
`$eval`, this could for example be used also to add two lists together, which is not possible with `eval`;
|
||||||
for example `$add($eval([1,2,3]), $eval([4,5,6])) -> [1, 2, 3, 4, 5, 6]`.
|
for example `$add($eval([1,2,3]), $eval([4,5,6])) -> [1, 2, 3, 4, 5, 6]`.
|
||||||
- `$round(float, significant)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_round) -
|
- `$round(float, significant)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_round)) -
|
||||||
rounds an input float into the number of provided significant digits. For example `$round(3.54343, 3) -> 3.543`.
|
rounds an input float into the number of provided significant digits. For example `$round(3.54343, 3) -> 3.543`.
|
||||||
- `$random([start, [end]])` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_random)) -
|
- `$random([start, [end]])` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_random)) -
|
||||||
this works like the Python `random()` function, but will randomize to an integer value if both start/end are
|
this works like the Python `random()` function, but will randomize to an integer value if both start/end are
|
||||||
|
|
@ -296,7 +290,7 @@ These are the 'base' callables.
|
||||||
- `$ljust` - shortcut to justify-left. Takes all other kwarg of `$just`.
|
- `$ljust` - shortcut to justify-left. Takes all other kwarg of `$just`.
|
||||||
- `$rjust` - shortcut to right justify.
|
- `$rjust` - shortcut to right justify.
|
||||||
- `$cjust` - shortcut to center justify.
|
- `$cjust` - shortcut to center justify.
|
||||||
- `$clr(startcolor, text[, endcolor])`([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_clr)) -
|
- `$clr(startcolor, text[, endcolor])` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_clr)) -
|
||||||
color text. The color is given with one or two characters without the preceeding `|`. If no endcolor is
|
color text. The color is given with one or two characters without the preceeding `|`. If no endcolor is
|
||||||
given, the string will go back to neutral, so `$clr(r, Hello)` is equivalent to `|rHello|n`.
|
given, the string will go back to neutral, so `$clr(r, Hello)` is equivalent to `|rHello|n`.
|
||||||
|
|
||||||
|
|
@ -310,7 +304,7 @@ parser.parse_to_any(string, caller=<object or account>, access="control", ...)`
|
||||||
|
|
||||||
```
|
```
|
||||||
The `caller` is required, it's the the object to do the access-check for. The `access` kwarg is the
|
The `caller` is required, it's the the object to do the access-check for. The `access` kwarg is the
|
||||||
[lock type](Locks) to check, default being `"control"`.
|
[lock type](./Locks) to check, default being `"control"`.
|
||||||
|
|
||||||
- `$search(query,type=account|script,return_list=False)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_search)) -
|
- `$search(query,type=account|script,return_list=False)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_search)) -
|
||||||
this will look up and try to match an object by key or alias. Use the `type` kwarg to
|
this will look up and try to match an object by key or alias. Use the `type` kwarg to
|
||||||
|
|
@ -342,9 +336,9 @@ references to other objects accessible via these callables.
|
||||||
result of `you_obj.get_display_name(looker=receiver)`. This allows for a single string to echo differently
|
result of `you_obj.get_display_name(looker=receiver)`. This allows for a single string to echo differently
|
||||||
depending on who sees it, and also to reference other people in the same way.
|
depending on who sees it, and also to reference other people in the same way.
|
||||||
- `$You([key])` - same as `$you` but always capitalized.
|
- `$You([key])` - same as `$you` but always capitalized.
|
||||||
- `$conj(verb)` - conjugates a verb between 2nd person presens to 3rd person presence depending on who
|
- `$conj(verb)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_conjugate)) -- conjugates a verb between 4nd person presens to 3rd person presence depending on who
|
||||||
sees the string. For example `$You() $conj(smiles).` will show as "You smile." and "Tom smiles." depending
|
sees the string. For example `"$You() $conj(smiles)".` will show as "You smile." and "Tom smiles." depending
|
||||||
on who sees it. This makes use of the tools in [evennia.utils.verb_conjugation](api.evennia.utils.verb_conjugation)
|
on who sees it. This makes use of the tools in [evennia.utils.verb_conjugation](api:evennia.utils.verb_conjugation)
|
||||||
to do this, and only works for English verbs.
|
to do this, and only works for English verbs.
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
@ -384,7 +378,5 @@ all the defaults (like `$toint()`).
|
||||||
|
|
||||||
The parsed result of the above would be something like this:
|
The parsed result of the above would be something like this:
|
||||||
|
|
||||||
```
|
This is the current uptime:
|
||||||
This is the current uptime:
|
------- 343 seconds -------
|
||||||
------- 343 seconds -------
|
|
||||||
```
|
|
||||||
21
docs/source/Concepts/Clickable-Links.md
Normal file
21
docs/source/Concepts/Clickable-Links.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
## Clickable links
|
||||||
|
|
||||||
|
Evennia supports clickable links for clients that supports it. This marks certain text so it can be
|
||||||
|
clicked by a mouse and trigger a given Evennia command. To support clickable links, Evennia requires
|
||||||
|
the webclient or an third-party telnet client with [MXP](http://www.zuggsoft.com/zmud/mxp.htm)
|
||||||
|
support (*Note: Evennia only supports clickable links, no other MXP features*).
|
||||||
|
|
||||||
|
- `|lc` to start the link, by defining the command to execute.
|
||||||
|
- `|lt` to continue with the text to show to the user (the link text).
|
||||||
|
- `|le` to end the link text and the link definition.
|
||||||
|
|
||||||
|
All elements must appear in exactly this order to make a valid link. For example,
|
||||||
|
|
||||||
|
```
|
||||||
|
"If you go |lcnorth|ltto the north|le you will find a cottage."
|
||||||
|
```
|
||||||
|
|
||||||
|
This will display as "If you go __to the north__ you will find a cottage." where clicking the link
|
||||||
|
will execute the command `north`. If the client does not support clickable links, only the link text
|
||||||
|
will be shown.
|
||||||
|
|
||||||
183
docs/source/Concepts/Colors.md
Normal file
183
docs/source/Concepts/Colors.md
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
# Colors
|
||||||
|
|
||||||
|
*Note that the Documentation does not display colour the way it would look on the screen.*
|
||||||
|
|
||||||
|
Color can be a very useful tool for your game. It can be used to increase readability and make your
|
||||||
|
game more appealing visually.
|
||||||
|
|
||||||
|
Remember however that, with the exception of the webclient, you generally don't control the client
|
||||||
|
used to connect to the game. There is, for example, one special tag meaning "yellow". But exactly
|
||||||
|
*which* hue of yellow is actually displayed on the user's screen depends on the settings of their
|
||||||
|
particular mud client. They could even swap the colours around or turn them off altogether if so
|
||||||
|
desired. Some clients don't even support color - text games are also played with special reading
|
||||||
|
equipment by people who are blind or have otherwise diminished eyesight.
|
||||||
|
|
||||||
|
So a good rule of thumb is to use colour to enhance your game but don't *rely* on it to display
|
||||||
|
critical information. If you are coding the game, you can add functionality to let users disable
|
||||||
|
colours as they please, as described [here](../Howto/Manually-Configuring-Color).
|
||||||
|
|
||||||
|
To see which colours your client support, use the default `@color` command. This will list all
|
||||||
|
available colours for ANSI and Xterm256 along with the codes you use for them. You can find a list
|
||||||
|
of all the parsed `ANSI`-colour codes in `evennia/utils/ansi.py`.
|
||||||
|
|
||||||
|
### ANSI colours
|
||||||
|
|
||||||
|
Evennia supports the `ANSI` standard for text. This is by far the most supported MUD-color standard,
|
||||||
|
available in all but the most ancient mud clients. The ANSI colours are **r**ed, **g**reen,
|
||||||
|
**y**ellow, **b**lue, **m**agenta, **c**yan, **w**hite and black. They are abbreviated by their
|
||||||
|
first letter except for black which is abbreviated with the letter **x**. In ANSI there are "bright"
|
||||||
|
and "normal" (darker) versions of each color, adding up to a total of 16 colours to use for
|
||||||
|
foreground text. There are also 8 "background" colours. These have no bright alternative in ANSI
|
||||||
|
(but Evennia uses the [Xterm256](./TextTags#xterm256-colours) extension behind the scenes to offer
|
||||||
|
them anyway).
|
||||||
|
|
||||||
|
To colour your text you put special tags in it. Evennia will parse these and convert them to the
|
||||||
|
correct markup for the client used. If the user's client/console/display supports ANSI colour, they
|
||||||
|
will see the text in the specified colour, otherwise the tags will be stripped (uncolored text).
|
||||||
|
This works also for non-terminal clients, such as the webclient. For the webclient, Evennia will
|
||||||
|
translate the codes to HTML RGB colors.
|
||||||
|
|
||||||
|
Here is an example of the tags in action:
|
||||||
|
|
||||||
|
|rThis text is bright red.|n This is normal text.
|
||||||
|
|RThis is a dark red text.|n This is normal text.
|
||||||
|
|[rThis text has red background.|n This is normal text.
|
||||||
|
|b|[yThis is bright blue text on yellow background.|n This is normal text.
|
||||||
|
|
||||||
|
- `|n` - this tag will turn off all color formatting, including background colors.
|
||||||
|
- `|#`- markup marks the start of foreground color. The case defines if the text is "bright" or
|
||||||
|
"normal". So `|g` is a bright green and `|G` is "normal" (darker) green.
|
||||||
|
- `|[#` is used to add a background colour to the text. The case again specifies if it is "bright"
|
||||||
|
or "normal", so `|[c` starts a bright cyan background and `|[C` a darker cyan background.
|
||||||
|
- `|!#` is used to add foreground color without any enforced brightness/normal information.
|
||||||
|
These are normal-intensity and are thus always given as uppercase, such as
|
||||||
|
`|!R` for red. The difference between e.g. `|!R` and `|R` is that
|
||||||
|
`|!R` will "inherit" the brightness setting from previously set color tags, whereas `|R` will
|
||||||
|
always reset to the normal-intensity red. The `|#` format contains an implicit `|h`/`|H` tag in it:
|
||||||
|
disabling highlighting when switching to a normal color, and enabling it for bright ones. So `|btest
|
||||||
|
|!Rtest2` will result in a bright red `test2` since the brightness setting from `|b` "bleeds over".
|
||||||
|
You could use this to for example quickly switch the intensity of a multitude of color tags. There
|
||||||
|
is no background-color equivalent to `|!` style tags.
|
||||||
|
- `|h` is used to make any following foreground ANSI colors bright (it has no effect on Xterm
|
||||||
|
colors). This is only relevant to use with `|!` type tags and will be valid until the next `|n`,
|
||||||
|
`|H` or normal (upper-case) `|#` tag. This tag will never affect background colors, those have to be
|
||||||
|
set bright/normal explicitly. Technically, `|h|!G` is identical to `|g`.
|
||||||
|
- `|H` negates the effects `|h` and returns all ANSI foreground colors (`|!` and `|` types) to
|
||||||
|
'normal' intensity. It has no effect on background and Xterm colors.
|
||||||
|
|
||||||
|
> Note: The ANSI standard does not actually support bright backgrounds like `|[r` - the standard
|
||||||
|
only supports "normal" intensity backgrounds. To get around this Evennia instead implements these
|
||||||
|
as [Xterm256 colours](./TextTags#xterm256-colours) behind the scenes. If the client does not support
|
||||||
|
Xterm256 the ANSI colors will be used instead and there will be no visible difference between using
|
||||||
|
upper- and lower-case background tags.
|
||||||
|
|
||||||
|
If you want to display an ANSI marker as output text (without having any effect), you need to escape
|
||||||
|
it by preceding its `|` with another `|`:
|
||||||
|
|
||||||
|
```
|
||||||
|
say The ||r ANSI marker changes text color to bright red.
|
||||||
|
```
|
||||||
|
|
||||||
|
This will output the raw `|r` without any color change. This can also be necessary if you are doing
|
||||||
|
ansi art that uses `|` with a letter directly following it.
|
||||||
|
|
||||||
|
Use the command
|
||||||
|
|
||||||
|
@color ansi
|
||||||
|
|
||||||
|
to get a list of all supported ANSI colours and the tags used to produce them.
|
||||||
|
|
||||||
|
A few additional ANSI codes are supported:
|
||||||
|
|
||||||
|
- `|/` A line break. You cannot put the normal Python `\n` line breaks in text entered inside the
|
||||||
|
game (Evennia will filter this for security reasons). This is what you use instead: use the `|/`
|
||||||
|
marker to format text with line breaks from the game command line.
|
||||||
|
- `` This will translate into a `TAB` character. This will not always show (or show differently) to
|
||||||
|
the client since it depends on their local settings. It's often better to use multiple spaces.
|
||||||
|
- `|_` This is a space. You can usually use the normal space character, but if the space is *at the
|
||||||
|
end of the line*, Evennia will likely crop it. This tag will not be cropped but always result in a
|
||||||
|
space.
|
||||||
|
- `|*` This will invert the current text/background colours. Can be useful to mark things (but see
|
||||||
|
below).
|
||||||
|
|
||||||
|
##### Caveats of `|*`
|
||||||
|
|
||||||
|
The `|*` tag (inverse video) is an old ANSI standard and should usually not be used for more than to
|
||||||
|
mark short snippets of text. If combined with other tags it comes with a series of potentially
|
||||||
|
confusing behaviors:
|
||||||
|
|
||||||
|
* The `|*` tag will only work once in a row:, ie: after using it once it won't have an effect again
|
||||||
|
until you declare another tag. This is an example:
|
||||||
|
|
||||||
|
```
|
||||||
|
Normal text, |*reversed text|*, still reversed text.
|
||||||
|
```
|
||||||
|
|
||||||
|
that is, it will not reverse to normal at the second `|*`. You need to reset it manually:
|
||||||
|
|
||||||
|
```
|
||||||
|
Normal text, |*reversed text|n, normal again.
|
||||||
|
```
|
||||||
|
|
||||||
|
* The `|*` tag does not take "bright" colors into account:
|
||||||
|
|
||||||
|
```
|
||||||
|
|RNormal red, |hnow brightened. |*BG is normal red.
|
||||||
|
```
|
||||||
|
|
||||||
|
So `|*` only considers the 'true' foreground color, ignoring any highlighting. Think of the bright
|
||||||
|
state (`|h`) as something like like `<strong>` in HTML: it modifies the _appearance_ of a normal
|
||||||
|
foreground color to match its bright counterpart, without changing its normal color.
|
||||||
|
* Finally, after a `|*`, if the previous background was set to a dark color (via `|[`), `|!#`) will
|
||||||
|
actually change the background color instead of the foreground:
|
||||||
|
|
||||||
|
```
|
||||||
|
|*reversed text |!R now BG is red.
|
||||||
|
```
|
||||||
|
For a detailed explanation of these caveats, see the [Understanding Color Tags](Understanding-Color-
|
||||||
|
Tags) tutorial. But most of the time you might be better off to simply avoid `|*` and mark your text
|
||||||
|
manually instead.
|
||||||
|
|
||||||
|
### Xterm256 Colours
|
||||||
|
|
||||||
|
The _Xterm256_ standard is a colour scheme that supports 256 colours for text and/or background.
|
||||||
|
While this offers many more possibilities than traditional ANSI colours, be wary that too many text
|
||||||
|
colors will be confusing to the eye. Also, not all clients support Xterm256 - these will instead see
|
||||||
|
the closest equivalent ANSI color. You can mix Xterm256 tags with ANSI tags as you please.
|
||||||
|
|
||||||
|
|555 This is pure white text.|n This is normal text.
|
||||||
|
|230 This is olive green text.
|
||||||
|
|[300 This text has a dark red background.
|
||||||
|
|005|[054 This is dark blue text on a bright cyan background.
|
||||||
|
|=a This is a greyscale value, equal to black.
|
||||||
|
|=m This is a greyscale value, midway between white and black.
|
||||||
|
|=z This is a greyscale value, equal to white.
|
||||||
|
|[=m This is a background greyscale value.
|
||||||
|
|
||||||
|
- `|###` - markup consists of three digits, each an integer from 0 to 5. The three digits describe
|
||||||
|
the amount of **r**ed, **g**reen and **b**lue (RGB) components used in the colour. So `|500` means
|
||||||
|
maximum red and none of the other colours - the result is a bright red. `|520` is red with a touch
|
||||||
|
of green - the result is orange. As opposed to ANSI colors, Xterm256 syntax does not worry about
|
||||||
|
bright/normal intensity, a brighter (lighter) color is just achieved by upping all RGB values with
|
||||||
|
the same amount.
|
||||||
|
- `|[###` - this works the same way but produces a coloured background.
|
||||||
|
- `|=#` - markup produces the xterm256 gray scale tones, where `#` is a letter from `a` (black) to
|
||||||
|
`z` (white). This offers many more nuances of gray than the normal `|###` markup (which only has
|
||||||
|
four gray tones between solid black and white (`|000`, `|111`, `|222`, `|333` and `|444`)).
|
||||||
|
- `|[=#` - this works in the same way but produces background gray scale tones.
|
||||||
|
|
||||||
|
If you have a client that supports Xterm256, you can use
|
||||||
|
|
||||||
|
@color xterm256
|
||||||
|
|
||||||
|
to get a table of all the 256 colours and the codes that produce them. If the table looks broken up
|
||||||
|
into a few blocks of colors, it means Xterm256 is not supported and ANSI are used as a replacement.
|
||||||
|
You can use the `@options` command to see if xterm256 is active for you. This depends on if your
|
||||||
|
client told Evennia what it supports - if not, and you know what your client supports, you may have
|
||||||
|
to activate some features manually.
|
||||||
|
|
||||||
|
## More reading
|
||||||
|
|
||||||
|
There is an [Understanding Color Tags](../Howto/Understanding-Color-Tags) tutorial which expands on the
|
||||||
|
use of ANSI color tags and the pitfalls of mixing ANSI and Xterms256 color tags in the same context.
|
||||||
|
|
||||||
|
|
@ -1,340 +1,19 @@
|
||||||
# TextTags
|
# In-text tags parsed by Evennia
|
||||||
|
|
||||||
|
Evennia understands various extra information embedded in text:
|
||||||
|
|
||||||
This documentation details the various text tags supported by Evennia, namely *colours*, *command
|
- [Colors](./Colors) - Using `|r`, `|n` etc can be used to mark parts of text with a color. The color will
|
||||||
links* and *inline functions*.
|
become ANSI/XTerm256 color tags for Telnet connections and CSS information for the webclient.
|
||||||
|
- [Clickable links](./Clickable-Links) - This allows you to provide a text the user can click to execute an
|
||||||
|
in-game command. This is on the form `|lc command |lt text |le`.
|
||||||
|
- [FuncParser callables](../Components/FuncParser) - These are full-fledged function calls on the form `$funcname(args, kwargs)`
|
||||||
|
that lead to calls to Python functions. The parser can be run with different available callables in different
|
||||||
|
circumstances. The parser is run on all outgoing messages if `settings.FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED=True`
|
||||||
|
(disabled by default).
|
||||||
|
|
||||||
There is also an [Understanding Color Tags](../Howto/Understanding-Color-Tags) tutorial which expands on the
|
```toctree::
|
||||||
use of ANSI color tags and the pitfalls of mixing ANSI and Xterms256 color tags in the same context.
|
|
||||||
|
|
||||||
## Coloured text
|
|
||||||
|
|
||||||
*Note that the Documentation does not display colour the way it would look on the screen.*
|
|
||||||
|
|
||||||
Color can be a very useful tool for your game. It can be used to increase readability and make your
|
|
||||||
game more appealing visually.
|
|
||||||
|
|
||||||
Remember however that, with the exception of the webclient, you generally don't control the client
|
|
||||||
used to connect to the game. There is, for example, one special tag meaning "yellow". But exactly
|
|
||||||
*which* hue of yellow is actually displayed on the user's screen depends on the settings of their
|
|
||||||
particular mud client. They could even swap the colours around or turn them off altogether if so
|
|
||||||
desired. Some clients don't even support color - text games are also played with special reading
|
|
||||||
equipment by people who are blind or have otherwise diminished eyesight.
|
|
||||||
|
|
||||||
So a good rule of thumb is to use colour to enhance your game but don't *rely* on it to display
|
|
||||||
critical information. If you are coding the game, you can add functionality to let users disable
|
|
||||||
colours as they please, as described [here](../Howto/Manually-Configuring-Color).
|
|
||||||
|
|
||||||
To see which colours your client support, use the default `@color` command. This will list all
|
|
||||||
available colours for ANSI and Xterm256 along with the codes you use for them. You can find a list
|
|
||||||
of all the parsed `ANSI`-colour codes in `evennia/utils/ansi.py`.
|
|
||||||
|
|
||||||
### ANSI colours
|
|
||||||
|
|
||||||
Evennia supports the `ANSI` standard for text. This is by far the most supported MUD-color standard,
|
|
||||||
available in all but the most ancient mud clients. The ANSI colours are **r**ed, **g**reen,
|
|
||||||
**y**ellow, **b**lue, **m**agenta, **c**yan, **w**hite and black. They are abbreviated by their
|
|
||||||
first letter except for black which is abbreviated with the letter **x**. In ANSI there are "bright"
|
|
||||||
and "normal" (darker) versions of each color, adding up to a total of 16 colours to use for
|
|
||||||
foreground text. There are also 8 "background" colours. These have no bright alternative in ANSI
|
|
||||||
(but Evennia uses the [Xterm256](./TextTags#xterm256-colours) extension behind the scenes to offer
|
|
||||||
them anyway).
|
|
||||||
|
|
||||||
To colour your text you put special tags in it. Evennia will parse these and convert them to the
|
|
||||||
correct markup for the client used. If the user's client/console/display supports ANSI colour, they
|
|
||||||
will see the text in the specified colour, otherwise the tags will be stripped (uncolored text).
|
|
||||||
This works also for non-terminal clients, such as the webclient. For the webclient, Evennia will
|
|
||||||
translate the codes to HTML RGB colors.
|
|
||||||
|
|
||||||
Here is an example of the tags in action:
|
|
||||||
|
|
||||||
|rThis text is bright red.|n This is normal text.
|
|
||||||
|RThis is a dark red text.|n This is normal text.
|
|
||||||
|[rThis text has red background.|n This is normal text.
|
|
||||||
|b|[yThis is bright blue text on yellow background.|n This is normal text.
|
|
||||||
|
|
||||||
- `|n` - this tag will turn off all color formatting, including background colors.
|
|
||||||
- `|#`- markup marks the start of foreground color. The case defines if the text is "bright" or
|
|
||||||
"normal". So `|g` is a bright green and `|G` is "normal" (darker) green.
|
|
||||||
- `|[#` is used to add a background colour to the text. The case again specifies if it is "bright"
|
|
||||||
or "normal", so `|[c` starts a bright cyan background and `|[C` a darker cyan background.
|
|
||||||
- `|!#` is used to add foreground color without any enforced brightness/normal information.
|
|
||||||
These are normal-intensity and are thus always given as uppercase, such as
|
|
||||||
`|!R` for red. The difference between e.g. `|!R` and `|R` is that
|
|
||||||
`|!R` will "inherit" the brightness setting from previously set color tags, whereas `|R` will
|
|
||||||
always reset to the normal-intensity red. The `|#` format contains an implicit `|h`/`|H` tag in it:
|
|
||||||
disabling highlighting when switching to a normal color, and enabling it for bright ones. So `|btest
|
|
||||||
|!Rtest2` will result in a bright red `test2` since the brightness setting from `|b` "bleeds over".
|
|
||||||
You could use this to for example quickly switch the intensity of a multitude of color tags. There
|
|
||||||
is no background-color equivalent to `|!` style tags.
|
|
||||||
- `|h` is used to make any following foreground ANSI colors bright (it has no effect on Xterm
|
|
||||||
colors). This is only relevant to use with `|!` type tags and will be valid until the next `|n`,
|
|
||||||
`|H` or normal (upper-case) `|#` tag. This tag will never affect background colors, those have to be
|
|
||||||
set bright/normal explicitly. Technically, `|h|!G` is identical to `|g`.
|
|
||||||
- `|H` negates the effects `|h` and returns all ANSI foreground colors (`|!` and `|` types) to
|
|
||||||
'normal' intensity. It has no effect on background and Xterm colors.
|
|
||||||
|
|
||||||
> Note: The ANSI standard does not actually support bright backgrounds like `|[r` - the standard
|
|
||||||
only supports "normal" intensity backgrounds. To get around this Evennia instead implements these
|
|
||||||
as [Xterm256 colours](./TextTags#xterm256-colours) behind the scenes. If the client does not support
|
|
||||||
Xterm256 the ANSI colors will be used instead and there will be no visible difference between using
|
|
||||||
upper- and lower-case background tags.
|
|
||||||
|
|
||||||
If you want to display an ANSI marker as output text (without having any effect), you need to escape
|
|
||||||
it by preceding its `|` with another `|`:
|
|
||||||
|
|
||||||
|
Colors.md
|
||||||
|
Clickable-Links.md
|
||||||
|
../Components/FuncParser.md
|
||||||
```
|
```
|
||||||
say The ||r ANSI marker changes text color to bright red.
|
|
||||||
```
|
|
||||||
|
|
||||||
This will output the raw `|r` without any color change. This can also be necessary if you are doing
|
|
||||||
ansi art that uses `|` with a letter directly following it.
|
|
||||||
|
|
||||||
Use the command
|
|
||||||
|
|
||||||
@color ansi
|
|
||||||
|
|
||||||
to get a list of all supported ANSI colours and the tags used to produce them.
|
|
||||||
|
|
||||||
A few additional ANSI codes are supported:
|
|
||||||
|
|
||||||
- `|/` A line break. You cannot put the normal Python `\n` line breaks in text entered inside the
|
|
||||||
game (Evennia will filter this for security reasons). This is what you use instead: use the `|/`
|
|
||||||
marker to format text with line breaks from the game command line.
|
|
||||||
- `` This will translate into a `TAB` character. This will not always show (or show differently) to
|
|
||||||
the client since it depends on their local settings. It's often better to use multiple spaces.
|
|
||||||
- `|_` This is a space. You can usually use the normal space character, but if the space is *at the
|
|
||||||
end of the line*, Evennia will likely crop it. This tag will not be cropped but always result in a
|
|
||||||
space.
|
|
||||||
- `|*` This will invert the current text/background colours. Can be useful to mark things (but see
|
|
||||||
below).
|
|
||||||
|
|
||||||
##### Caveats of `|*`
|
|
||||||
|
|
||||||
The `|*` tag (inverse video) is an old ANSI standard and should usually not be used for more than to
|
|
||||||
mark short snippets of text. If combined with other tags it comes with a series of potentially
|
|
||||||
confusing behaviors:
|
|
||||||
|
|
||||||
* The `|*` tag will only work once in a row:, ie: after using it once it won't have an effect again
|
|
||||||
until you declare another tag. This is an example:
|
|
||||||
|
|
||||||
```
|
|
||||||
Normal text, |*reversed text|*, still reversed text.
|
|
||||||
```
|
|
||||||
|
|
||||||
that is, it will not reverse to normal at the second `|*`. You need to reset it manually:
|
|
||||||
|
|
||||||
```
|
|
||||||
Normal text, |*reversed text|n, normal again.
|
|
||||||
```
|
|
||||||
|
|
||||||
* The `|*` tag does not take "bright" colors into account:
|
|
||||||
|
|
||||||
```
|
|
||||||
|RNormal red, |hnow brightened. |*BG is normal red.
|
|
||||||
```
|
|
||||||
|
|
||||||
So `|*` only considers the 'true' foreground color, ignoring any highlighting. Think of the bright
|
|
||||||
state (`|h`) as something like like `<strong>` in HTML: it modifies the _appearance_ of a normal
|
|
||||||
foreground color to match its bright counterpart, without changing its normal color.
|
|
||||||
* Finally, after a `|*`, if the previous background was set to a dark color (via `|[`), `|!#`) will
|
|
||||||
actually change the background color instead of the foreground:
|
|
||||||
|
|
||||||
```
|
|
||||||
|*reversed text |!R now BG is red.
|
|
||||||
```
|
|
||||||
For a detailed explanation of these caveats, see the [Understanding Color Tags](Understanding-Color-
|
|
||||||
Tags) tutorial. But most of the time you might be better off to simply avoid `|*` and mark your text
|
|
||||||
manually instead.
|
|
||||||
|
|
||||||
### Xterm256 Colours
|
|
||||||
|
|
||||||
The _Xterm256_ standard is a colour scheme that supports 256 colours for text and/or background.
|
|
||||||
While this offers many more possibilities than traditional ANSI colours, be wary that too many text
|
|
||||||
colors will be confusing to the eye. Also, not all clients support Xterm256 - these will instead see
|
|
||||||
the closest equivalent ANSI color. You can mix Xterm256 tags with ANSI tags as you please.
|
|
||||||
|
|
||||||
|555 This is pure white text.|n This is normal text.
|
|
||||||
|230 This is olive green text.
|
|
||||||
|[300 This text has a dark red background.
|
|
||||||
|005|[054 This is dark blue text on a bright cyan background.
|
|
||||||
|=a This is a greyscale value, equal to black.
|
|
||||||
|=m This is a greyscale value, midway between white and black.
|
|
||||||
|=z This is a greyscale value, equal to white.
|
|
||||||
|[=m This is a background greyscale value.
|
|
||||||
|
|
||||||
- `|###` - markup consists of three digits, each an integer from 0 to 5. The three digits describe
|
|
||||||
the amount of **r**ed, **g**reen and **b**lue (RGB) components used in the colour. So `|500` means
|
|
||||||
maximum red and none of the other colours - the result is a bright red. `|520` is red with a touch
|
|
||||||
of green - the result is orange. As opposed to ANSI colors, Xterm256 syntax does not worry about
|
|
||||||
bright/normal intensity, a brighter (lighter) color is just achieved by upping all RGB values with
|
|
||||||
the same amount.
|
|
||||||
- `|[###` - this works the same way but produces a coloured background.
|
|
||||||
- `|=#` - markup produces the xterm256 gray scale tones, where `#` is a letter from `a` (black) to
|
|
||||||
`z` (white). This offers many more nuances of gray than the normal `|###` markup (which only has
|
|
||||||
four gray tones between solid black and white (`|000`, `|111`, `|222`, `|333` and `|444`)).
|
|
||||||
- `|[=#` - this works in the same way but produces background gray scale tones.
|
|
||||||
|
|
||||||
If you have a client that supports Xterm256, you can use
|
|
||||||
|
|
||||||
@color xterm256
|
|
||||||
|
|
||||||
to get a table of all the 256 colours and the codes that produce them. If the table looks broken up
|
|
||||||
into a few blocks of colors, it means Xterm256 is not supported and ANSI are used as a replacement.
|
|
||||||
You can use the `@options` command to see if xterm256 is active for you. This depends on if your
|
|
||||||
client told Evennia what it supports - if not, and you know what your client supports, you may have
|
|
||||||
to activate some features manually.
|
|
||||||
|
|
||||||
## Clickable links
|
|
||||||
|
|
||||||
Evennia supports clickable links for clients that supports it. This marks certain text so it can be
|
|
||||||
clicked by a mouse and trigger a given Evennia command. To support clickable links, Evennia requires
|
|
||||||
the webclient or an third-party telnet client with [MXP](http://www.zuggsoft.com/zmud/mxp.htm)
|
|
||||||
support (*Note: Evennia only supports clickable links, no other MXP features*).
|
|
||||||
|
|
||||||
- `|lc` to start the link, by defining the command to execute.
|
|
||||||
- `|lt` to continue with the text to show to the user (the link text).
|
|
||||||
- `|le` to end the link text and the link definition.
|
|
||||||
|
|
||||||
All elements must appear in exactly this order to make a valid link. For example,
|
|
||||||
|
|
||||||
```
|
|
||||||
"If you go |lcnorth|ltto the north|le you will find a cottage."
|
|
||||||
```
|
|
||||||
|
|
||||||
This will display as "If you go __to the north__ you will find a cottage." where clicking the link
|
|
||||||
will execute the command `north`. If the client does not support clickable links, only the link text
|
|
||||||
will be shown.
|
|
||||||
|
|
||||||
## Inline functions
|
|
||||||
|
|
||||||
> Note: Inlinefuncs are **not** activated by default. To use them you need to add
|
|
||||||
`INLINEFUNC_ENABLED=True` to your settings file.
|
|
||||||
|
|
||||||
Evennia has its own inline text formatting language, known as *inlinefuncs*. It allows the builder
|
|
||||||
to include special function calls in code. They are executed dynamically by each session that
|
|
||||||
receives them.
|
|
||||||
|
|
||||||
To add an inlinefunc, you embed it in a text string like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
"A normal string with $funcname(arg, arg, ...) embedded inside it."
|
|
||||||
```
|
|
||||||
|
|
||||||
When this string is sent to a session (with the `msg()` method), these embedded inlinefuncs will be
|
|
||||||
parsed. Their return value (which always is a string) replace their call location in the finalized
|
|
||||||
string. The interesting thing with this is that the function called will have access to which
|
|
||||||
session is seeing the string, meaning the string can end up looking different depending on who is
|
|
||||||
looking. It could of course also vary depending on other factors like game time.
|
|
||||||
|
|
||||||
Any number of comma-separated arguments can be given (or none). No keywords are supported. You can
|
|
||||||
also nest inlinefuncs by letting an argument itself also be another `$funcname(arg, arg, ...)` call
|
|
||||||
(down to any depth of nesting). Function call resolution happens as in all programming languages
|
|
||||||
inside-out, with the nested calls replacing the argument with their return strings before calling he
|
|
||||||
parent.
|
|
||||||
|
|
||||||
```
|
|
||||||
> say "This is $pad(a center-padded text, 30,c,-) of width 30."
|
|
||||||
You say, "This is ---- a center-padded text----- of width 30."
|
|
||||||
```
|
|
||||||
|
|
||||||
A special case happens if wanting to use an inlinefunc argument that itself includes a comma - this
|
|
||||||
would be parsed as an argument separator. To escape commas you can either escape each comma manually
|
|
||||||
with a backslash `\,`, or you can embed the entire string in python triple-quotes `"""` or `'''` -
|
|
||||||
this will escape the entire argument, including commas and any nested inlinefunc calls within.
|
|
||||||
|
|
||||||
Only certain functions are available to use as inlinefuncs and the game developer may add their own
|
|
||||||
functions as needed.
|
|
||||||
|
|
||||||
### New inlinefuncs
|
|
||||||
|
|
||||||
To add new inlinefuncs, edit the file `mygame/server/conf/inlinefuncs.py`.
|
|
||||||
|
|
||||||
*All globally defined functions in this module* are considered inline functions by the system. The
|
|
||||||
only exception is functions whose name starts with an underscore `_`. An inlinefunc must be of the
|
|
||||||
following form:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def funcname(*args, **kwargs):
|
|
||||||
# ...
|
|
||||||
return modified_text
|
|
||||||
```
|
|
||||||
|
|
||||||
where `*args` denotes all the arguments this function will accept as an `$inlinefunc`. The inline
|
|
||||||
function is expected to clean arguments and check that they are valid. If needed arguments are not
|
|
||||||
given, default values should be used. The function should always return a string (even if it's
|
|
||||||
empty). An inlinefunc should never cause a traceback regardless of the input (but it could log
|
|
||||||
errors if desired).
|
|
||||||
|
|
||||||
Note that whereas the function should accept `**kwargs`, keyword inputs are *not* usable in the call
|
|
||||||
to the inlinefunction. The `kwargs` part is instead intended for Evennia to be able to supply extra
|
|
||||||
information. Currently Evennia sends a single keyword to every inline function and that is
|
|
||||||
`session`, which holds the [serversession](../Components/Sessions) this text is targeted at. Through the session
|
|
||||||
object, a lot of dynamic possibilities are opened up for your inline functions.
|
|
||||||
|
|
||||||
The `settings.INLINEFUNC_MODULES` configuration option is a list that decides which modules should
|
|
||||||
be parsed for inline function definitions. This will include `mygame/server/conf/inlinefuncs.py` but
|
|
||||||
more could be added. The list is read from left to right so if you want to overload default
|
|
||||||
functions you just have to put your custom module-paths later in the list and name your functions
|
|
||||||
the same as default ones.
|
|
||||||
|
|
||||||
Here is an example, the `crop` default inlinefunction:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from evennia.utils import utils
|
|
||||||
|
|
||||||
def crop(*args, **kwargs):
|
|
||||||
"""
|
|
||||||
Inlinefunc. Crops ingoing text to given widths.
|
|
||||||
Args:
|
|
||||||
text (str, optional): Text to crop.
|
|
||||||
width (str, optional): Will be converted to an integer. Width of
|
|
||||||
crop in characters.
|
|
||||||
suffix (str, optional): End string to mark the fact that a part
|
|
||||||
of the string was cropped. Defaults to `[...]`.
|
|
||||||
Kwargs:
|
|
||||||
session (Session): Session performing the crop.
|
|
||||||
Example:
|
|
||||||
`$crop(text, 50, [...])`
|
|
||||||
|
|
||||||
"""
|
|
||||||
text, width, suffix = "", 78, "[...]"
|
|
||||||
nargs = len(args)
|
|
||||||
if nargs > 0:
|
|
||||||
text = args[0]
|
|
||||||
if nargs > 1:
|
|
||||||
width = int(args[1]) if args[1].strip().isdigit() else 78
|
|
||||||
if nargs > 2:
|
|
||||||
suffix = args[2]
|
|
||||||
return utils.crop(text, width=width, suffix=suffix)
|
|
||||||
```
|
|
||||||
Another example, making use of the Session:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def charactername(*args, **kwargs):
|
|
||||||
"""
|
|
||||||
Inserts the character name of whomever sees the string
|
|
||||||
(so everyone will see their own name). Uses the account
|
|
||||||
name for OOC communications.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
say "This means YOU, $charactername()!"
|
|
||||||
|
|
||||||
"""
|
|
||||||
session = kwargs["session"]
|
|
||||||
if session.puppet:
|
|
||||||
return kwargs["session"].puppet.key
|
|
||||||
else:
|
|
||||||
return session.account.key
|
|
||||||
```
|
|
||||||
|
|
||||||
Evennia itself offers the following default inline functions (mostly as examples):
|
|
||||||
|
|
||||||
* `crop(text, width, suffix)` - See above.
|
|
||||||
* `pad(text, width, align, fillchar)` - this pads the text to `width` (default 78), alignment ("c",
|
|
||||||
"l" or "r", defaulting to "c") and fill-in character (defaults to space). Example: `$pad(40,l,-)`
|
|
||||||
* `clr(startclr, text, endclr)` - A programmatic way to enter colored text for those who don't want
|
|
||||||
to use the normal `|c` type color markers for some reason. The `color` argument is the same as the
|
|
||||||
color markers except without the actual pre-marker, so `|r` would be just `r`. If `endclr` is not
|
|
||||||
given, it defaults to resetting the color (`n`). Example: `$clr(b, A blue text)`
|
|
||||||
* `space(number)` - Inserts the given number of spaces. If no argument is given, use 4 spaces.
|
|
||||||
|
|
@ -77,6 +77,7 @@ The flat API is defined in `__init__.py` [viewable here](github:evennia/__init__
|
||||||
- [evennia.EvForm](api:evennia.utils.evform#evennia.utils.evform.EvForm) - text form creator
|
- [evennia.EvForm](api:evennia.utils.evform#evennia.utils.evform.EvForm) - text form creator
|
||||||
- Evennia.EvMore - text paginator
|
- Evennia.EvMore - text paginator
|
||||||
- [evennia.EvEditor](api:evennia.utils.eveditor#evennia.utils.eveditor.EvEditor) - in game text line editor ([docs](Components/EvEditor))
|
- [evennia.EvEditor](api:evennia.utils.eveditor#evennia.utils.eveditor.EvEditor) - in game text line editor ([docs](Components/EvEditor))
|
||||||
|
- [evennia.utils.funcparser.Funcparser](api:evennia.utils.funcparser) - inline parsing of functions ([docs](Components/FuncParser))
|
||||||
|
|
||||||
### Global singleton handlers
|
### Global singleton handlers
|
||||||
|
|
||||||
|
|
|
||||||
7
docs/source/api/evennia.utils.funcparser.rst
Normal file
7
docs/source/api/evennia.utils.funcparser.rst
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
evennia.utils.funcparser
|
||||||
|
===============================
|
||||||
|
|
||||||
|
.. automodule:: evennia.utils.funcparser
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
evennia.utils.inlinefuncs
|
|
||||||
================================
|
|
||||||
|
|
||||||
.. automodule:: evennia.utils.inlinefuncs
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
@ -21,8 +21,8 @@ evennia.utils
|
||||||
evennia.utils.evmenu
|
evennia.utils.evmenu
|
||||||
evennia.utils.evmore
|
evennia.utils.evmore
|
||||||
evennia.utils.evtable
|
evennia.utils.evtable
|
||||||
|
evennia.utils.funcparser
|
||||||
evennia.utils.gametime
|
evennia.utils.gametime
|
||||||
evennia.utils.inlinefuncs
|
|
||||||
evennia.utils.logger
|
evennia.utils.logger
|
||||||
evennia.utils.optionclasses
|
evennia.utils.optionclasses
|
||||||
evennia.utils.optionhandler
|
evennia.utils.optionhandler
|
||||||
|
|
@ -38,3 +38,4 @@ evennia.utils
|
||||||
:maxdepth: 6
|
:maxdepth: 6
|
||||||
|
|
||||||
evennia.utils.idmapper
|
evennia.utils.idmapper
|
||||||
|
evennia.utils.verb_conjugation
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
evennia.utils.verb\_conjugation.conjugate
|
||||||
|
================================================
|
||||||
|
|
||||||
|
.. automodule:: evennia.utils.verb_conjugation.conjugate
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
15
docs/source/api/evennia.utils.verb_conjugation.rst
Normal file
15
docs/source/api/evennia.utils.verb_conjugation.rst
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
evennia.utils.verb\_conjugation
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
.. automodule:: evennia.utils.verb_conjugation
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 6
|
||||||
|
|
||||||
|
evennia.utils.verb_conjugation.conjugate
|
||||||
|
evennia.utils.verb_conjugation.tests
|
||||||
7
docs/source/api/evennia.utils.verb_conjugation.tests.rst
Normal file
7
docs/source/api/evennia.utils.verb_conjugation.tests.rst
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
evennia.utils.verb\_conjugation.tests
|
||||||
|
============================================
|
||||||
|
|
||||||
|
.. automodule:: evennia.utils.verb_conjugation.tests
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Toc
|
# Toc
|
||||||
|
- [API root](api/evennia-api.rst)
|
||||||
- [Coding/Coding Introduction](Coding/Coding-Introduction)
|
- [Coding/Coding Introduction](Coding/Coding-Introduction)
|
||||||
- [Coding/Coding Overview](Coding/Coding-Overview)
|
- [Coding/Coding Overview](Coding/Coding-Overview)
|
||||||
- [Coding/Continuous Integration](Coding/Continuous-Integration)
|
- [Coding/Continuous Integration](Coding/Continuous-Integration)
|
||||||
|
|
@ -53,6 +53,8 @@
|
||||||
- [Concepts/Banning](Concepts/Banning)
|
- [Concepts/Banning](Concepts/Banning)
|
||||||
- [Concepts/Bootstrap & Evennia](Concepts/Bootstrap-&-Evennia)
|
- [Concepts/Bootstrap & Evennia](Concepts/Bootstrap-&-Evennia)
|
||||||
- [Concepts/Building Permissions](Concepts/Building-Permissions)
|
- [Concepts/Building Permissions](Concepts/Building-Permissions)
|
||||||
|
- [Concepts/Clickable Links](Concepts/Clickable-Links)
|
||||||
|
- [Concepts/Colors](Concepts/Colors)
|
||||||
- [Concepts/Concepts Overview](Concepts/Concepts-Overview)
|
- [Concepts/Concepts Overview](Concepts/Concepts-Overview)
|
||||||
- [Concepts/Custom Protocols](Concepts/Custom-Protocols)
|
- [Concepts/Custom Protocols](Concepts/Custom-Protocols)
|
||||||
- [Concepts/Guest Logins](Concepts/Guest-Logins)
|
- [Concepts/Guest Logins](Concepts/Guest-Logins)
|
||||||
|
|
|
||||||
|
|
@ -2384,7 +2384,7 @@ class CmdExamine(ObjManipCommand):
|
||||||
"""
|
"""
|
||||||
global _FUNCPARSER
|
global _FUNCPARSER
|
||||||
if not _FUNCPARSER:
|
if not _FUNCPARSER:
|
||||||
_FUNCPARSER = funcparser.FuncParser(settings.INLINEFUNC_MODULES)
|
_FUNCPARSER = funcparser.FuncParser(settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES)
|
||||||
|
|
||||||
if attr is None:
|
if attr is None:
|
||||||
return "No such attribute was found."
|
return "No such attribute was found."
|
||||||
|
|
@ -3464,7 +3464,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||||
"Python structures are allowed. \nMake sure to use correct "
|
"Python structures are allowed. \nMake sure to use correct "
|
||||||
"Python syntax. Remember especially to put quotes around all "
|
"Python syntax. Remember especially to put quotes around all "
|
||||||
"strings inside lists and dicts.|n For more advanced uses, embed "
|
"strings inside lists and dicts.|n For more advanced uses, embed "
|
||||||
"inlinefuncs in the strings."
|
"funcparser callables ($funcs) in the strings."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
string = "Expected {}, got {}.".format(expect, type(prototype))
|
string = "Expected {}, got {}.".format(expect, type(prototype))
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
"""
|
"""
|
||||||
Inlinefunc
|
Outgoing callables to apply with the FuncParser on outgoing messages.
|
||||||
|
|
||||||
Inline functions allow for direct conversion of text users mark in a
|
The functions in this module will become available as $funcname(args, kwargs)
|
||||||
special way. Inlinefuncs are deactivated by default. To activate, add
|
in all outgoing strings if you add
|
||||||
|
|
||||||
INLINEFUNC_ENABLED = True
|
FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED = True
|
||||||
|
|
||||||
to your settings file. The default inlinefuncs are found in
|
to your settings file. The default inlinefuncs are found at the bottom of
|
||||||
evennia.utils.inlinefunc.
|
`evennia.utils.funcparser`.
|
||||||
|
|
||||||
In text, usage is straightforward:
|
In text, usage is straightforward:
|
||||||
|
|
||||||
$funcname([arg1,[arg2,...]])
|
$funcname(arg1, arg2, ..., key=val, key2=val2, ...)
|
||||||
|
|
||||||
Example 1 (using the "pad" inlinefunc):
|
Example 1 (using the "pad" inlinefunc):
|
||||||
say This is $pad("a center-padded text", 50,c,-) of width 50.
|
say This is $pad("a center-padded text", 50,c,-) of width 50.
|
||||||
|
|
@ -26,26 +26,14 @@ Example 2 (using nested "pad" and "time" inlinefuncs):
|
||||||
To add more inline functions, add them to this module, using
|
To add more inline functions, add them to this module, using
|
||||||
the following call signature:
|
the following call signature:
|
||||||
|
|
||||||
def funcname(text, *args, **kwargs)
|
def funcname(*args, **kwargs)
|
||||||
|
...
|
||||||
where `text` is always the part between {funcname(args) and
|
|
||||||
{/funcname and the *args are taken from the appropriate part of the
|
|
||||||
call. If no {/funcname is given, `text` will be the empty string.
|
|
||||||
|
|
||||||
It is important that the inline function properly clean the
|
|
||||||
incoming `args`, checking their type and replacing them with sane
|
|
||||||
defaults if needed. If impossible to resolve, the unmodified text
|
|
||||||
should be returned. The inlinefunc should never cause a traceback.
|
|
||||||
|
|
||||||
While the inline function should accept **kwargs, the keyword is
|
|
||||||
never accepted as a valid call - this is only intended to be used
|
|
||||||
internally by Evennia, notably to send the `session` keyword to
|
|
||||||
the function; this is the session of the object viewing the string
|
|
||||||
and can be used to customize it to each session.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# def capitalize(text, *args, **kwargs):
|
# def capitalize(*args, **kwargs):
|
||||||
# "Silly capitalize example. Used as {capitalize() ... {/capitalize"
|
# "Silly capitalize example. Used as $capitalize
|
||||||
|
# if not args:
|
||||||
|
# return ''
|
||||||
# session = kwargs.get("session")
|
# session = kwargs.get("session")
|
||||||
# return text.capitalize()
|
# return args[0].capitalize()
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,12 @@ def protfunc_callable_protkey(*args, **kwargs):
|
||||||
|
|
||||||
prototype = kwargs.get("prototype", {})
|
prototype = kwargs.get("prototype", {})
|
||||||
prot_value = prototype[args[0]]
|
prot_value = prototype[args[0]]
|
||||||
return funcparser.funcparser_callable_eval(prot_value, **kwargs)
|
try:
|
||||||
|
return funcparser.funcparser_callable_eval(prot_value, **kwargs)
|
||||||
|
except funcparser.ParsingError:
|
||||||
|
return prot_value
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# this is picked up by FuncParser
|
# this is picked up by FuncParser
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,21 @@ def check_errors(settings):
|
||||||
"Update your settings file (see evennia/settings_default.py "
|
"Update your settings file (see evennia/settings_default.py "
|
||||||
"for more info)."
|
"for more info)."
|
||||||
)
|
)
|
||||||
|
depstring = (
|
||||||
|
"settings.{} was renamed to {}. Update your settings file (the FuncParser "
|
||||||
|
"replaces and generalizes that which inlinefuncs used to do).")
|
||||||
|
if hasattr(settings, "INLINEFUNC_ENABLED"):
|
||||||
|
raise DeprecationWarning(depstring.format(
|
||||||
|
"settings.INLINEFUNC_ENABLED", "FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLE"))
|
||||||
|
if hasattr(settings, "INLINEFUNC_STACK_MAXSIZE"):
|
||||||
|
raise DeprecationWarning(depstring.format(
|
||||||
|
"settings.INLINEFUNC_STACK_MAXSIZE", "FUNCPARSER_MAX_NESTING"))
|
||||||
|
if hasattr(settings, "INLINEFUNC_MODULES"):
|
||||||
|
raise DeprecationWarning(depstring.format(
|
||||||
|
"settings.INLINEFUNC_MODULES", "FUNCPARSER_OUTGOING_MESSAGES_MODULES"))
|
||||||
|
if hasattr(settings, "PROTFUNC_MODULES"):
|
||||||
|
raise DeprecationWarning(depstring.format(
|
||||||
|
"settings.PROTFUNC_MODULES", "FUNCPARSER_PROTOTYPE_VALUE_MODULES"))
|
||||||
|
|
||||||
gametime_deprecation = (
|
gametime_deprecation = (
|
||||||
"The settings TIME_SEC_PER_MIN, TIME_MIN_PER_HOUR,"
|
"The settings TIME_SEC_PER_MIN, TIME_MIN_PER_HOUR,"
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,9 @@ from evennia.utils.utils import (
|
||||||
from evennia.server.portal import amp
|
from evennia.server.portal import amp
|
||||||
from evennia.server.signals import SIGNAL_ACCOUNT_POST_LOGIN, SIGNAL_ACCOUNT_POST_LOGOUT
|
from evennia.server.signals import SIGNAL_ACCOUNT_POST_LOGIN, SIGNAL_ACCOUNT_POST_LOGOUT
|
||||||
from evennia.server.signals import SIGNAL_ACCOUNT_POST_FIRST_LOGIN, SIGNAL_ACCOUNT_POST_LAST_LOGOUT
|
from evennia.server.signals import SIGNAL_ACCOUNT_POST_FIRST_LOGIN, SIGNAL_ACCOUNT_POST_LAST_LOGOUT
|
||||||
# from evennia.utils.inlinefuncs import parse_inlinefunc
|
|
||||||
from codecs import decode as codecs_decode
|
from codecs import decode as codecs_decode
|
||||||
|
|
||||||
_INLINEFUNC_ENABLED = settings.INLINEFUNC_ENABLED
|
_FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED = settings.FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED
|
||||||
|
|
||||||
# delayed imports
|
# delayed imports
|
||||||
_AccountDB = None
|
_AccountDB = None
|
||||||
|
|
@ -154,7 +153,8 @@ class SessionHandler(dict):
|
||||||
|
|
||||||
def clean_senddata(self, session, kwargs):
|
def clean_senddata(self, session, kwargs):
|
||||||
"""
|
"""
|
||||||
Clean up data for sending across the AMP wire. Also apply INLINEFUNCS.
|
Clean up data for sending across the AMP wire. Also apply the
|
||||||
|
FuncParser using callables from `settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session (Session): The relevant session instance.
|
session (Session): The relevant session instance.
|
||||||
|
|
@ -170,14 +170,14 @@ class SessionHandler(dict):
|
||||||
Returns:
|
Returns:
|
||||||
kwargs (dict): A cleaned dictionary of cmdname:[[args],{kwargs}] pairs,
|
kwargs (dict): A cleaned dictionary of cmdname:[[args],{kwargs}] pairs,
|
||||||
where the keys, args and kwargs have all been converted to
|
where the keys, args and kwargs have all been converted to
|
||||||
send-safe entities (strings or numbers), and inlinefuncs have been
|
send-safe entities (strings or numbers), and funcparser parsing has been
|
||||||
applied.
|
applied.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
global _FUNCPARSER
|
global _FUNCPARSER
|
||||||
if not _FUNCPARSER:
|
if not _FUNCPARSER:
|
||||||
from evennia.utils.funcparser import FuncParser
|
from evennia.utils.funcparser import FuncParser
|
||||||
_FUNCPARSER = FuncParser(settings.INLINEFUNC_MODULES, raise_errors=True)
|
_FUNCPARSER = FuncParser(settings.FUNCPARSER_OUTGOING_MESSAGES_MODULE, raise_errors=True)
|
||||||
|
|
||||||
options = kwargs.pop("options", None) or {}
|
options = kwargs.pop("options", None) or {}
|
||||||
raw = options.get("raw", False)
|
raw = options.get("raw", False)
|
||||||
|
|
@ -210,8 +210,8 @@ class SessionHandler(dict):
|
||||||
elif isinstance(data, (str, bytes)):
|
elif isinstance(data, (str, bytes)):
|
||||||
data = _utf8(data)
|
data = _utf8(data)
|
||||||
|
|
||||||
if _INLINEFUNC_ENABLED and not raw and isinstance(self, ServerSessionHandler):
|
if _FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED and not raw and isinstance(self, ServerSessionHandler):
|
||||||
# only parse inlinefuncs on the outgoing path (sessionhandler->)
|
# only apply funcparser on the outgoing path (sessionhandler->)
|
||||||
# data = parse_inlinefunc(data, strip=strip_inlinefunc, session=session)
|
# data = parse_inlinefunc(data, strip=strip_inlinefunc, session=session)
|
||||||
data = _FUNCPARSER.parse(data, strip=strip_inlinefunc, session=session)
|
data = _FUNCPARSER.parse(data, strip=strip_inlinefunc, session=session)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -598,23 +598,31 @@ TIME_GAME_EPOCH = None
|
||||||
TIME_IGNORE_DOWNTIMES = False
|
TIME_IGNORE_DOWNTIMES = False
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Inlinefunc, PrototypeFuncs
|
# FuncParser
|
||||||
|
#
|
||||||
|
# Strings parsed with the FuncParser can contain 'callables' on the
|
||||||
|
# form $funcname(args,kwargs), which will lead to actual Python functions
|
||||||
|
# being executed.
|
||||||
######################################################################
|
######################################################################
|
||||||
# Evennia supports inline function preprocessing. This allows users
|
# This changes the start-symbol for the funcparser callable. Note that
|
||||||
# to supply inline calls on the form $func(arg, arg, ...) to do
|
# this will make a lot of documentation invalid and there may also be
|
||||||
# session-aware text formatting and manipulation on the fly. If
|
# other unexpected side effects, so change with caution.
|
||||||
# disabled, such inline functions will not be parsed.
|
FUNCPARSER_START_CHAR = '$'
|
||||||
INLINEFUNC_ENABLED = False
|
# The symbol to use to escape Func
|
||||||
# This defined how deeply nested inlinefuncs can be. Set to <=0 to
|
FUNCPARSER_ESCAPE_CHAR = '\\'
|
||||||
# disable (not recommended, this is a safeguard against infinite loops).
|
# This is the global max nesting-level for nesting functions in
|
||||||
INLINEFUNC_STACK_MAXSIZE = 20
|
# the funcparser. This protects against infinite loops.
|
||||||
|
FUNCPARSER_MAX_NESTING = 20
|
||||||
|
# Activate funcparser for all outgoing strings. The current Session
|
||||||
|
# will be passed into the parser (used to be called inlinefuncs)
|
||||||
|
FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED = False
|
||||||
# Only functions defined globally (and not starting with '_') in
|
# Only functions defined globally (and not starting with '_') in
|
||||||
# these modules will be considered valid inlinefuncs. The list
|
# these modules will be considered valid inlinefuncs. The list
|
||||||
# is loaded from left-to-right, same-named functions will overload
|
# is loaded from left-to-right, same-named functions will overload
|
||||||
INLINEFUNC_MODULES = ["evennia.utils.funcparser", "server.conf.inlinefuncs"]
|
FUNCPARSER_OUTGOING_MESSAGES_MODULES = ["evennia.utils.funcparser", "server.conf.inlinefuncs"]
|
||||||
# Module holding handlers for ProtFuncs. These allow for embedding
|
# Prototype values are also parsed with FuncParser. These modules
|
||||||
# functional code in prototypes and has the same syntax as inlinefuncs.
|
# define which $func callables are available to use in prototypes.
|
||||||
PROTOTYPEFUNC_MODULES = ["evennia.prototypes.protfuncs", "server.conf.prototypefuncs"]
|
FUNCPARSER_PROTOTYPE_PARSING_MODULES = ["evennia.prototypes.protfuncs", "server.conf.prototypefuncs"]
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Global Scripts
|
# Global Scripts
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,13 @@
|
||||||
Generic function parser for functions embedded in a string, on the form
|
Generic function parser for functions embedded in a string, on the form
|
||||||
`$funcname(*args, **kwargs)`, for example:
|
`$funcname(*args, **kwargs)`, for example:
|
||||||
|
|
||||||
"A string $foo() with $bar(a, b, c, $moo(), d=23) etc."
|
```
|
||||||
|
"A string $foo() with $bar(a, b, c, $moo(), d=23) etc."
|
||||||
|
```
|
||||||
|
|
||||||
Each arg/kwarg can also be another nested function. These will be executed from
|
Each arg/kwarg can also be another nested function. These will be executed
|
||||||
the deepest-nested first and used as arguments for the higher-level function.
|
inside-out and their return will used as arguments for the enclosing function
|
||||||
|
(so the same as for regular Python function execution).
|
||||||
|
|
||||||
This is the base for all forms of embedded func-parsing, like inlinefuncs and
|
This is the base for all forms of embedded func-parsing, like inlinefuncs and
|
||||||
protfuncs. Each function available to use must be registered as a 'safe'
|
protfuncs. Each function available to use must be registered as a 'safe'
|
||||||
|
|
@ -23,7 +26,6 @@ def funcname(*args, **kwargs):
|
||||||
# it must always accept *args and **kwargs.
|
# it must always accept *args and **kwargs.
|
||||||
...
|
...
|
||||||
return something
|
return something
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
@ -38,6 +40,7 @@ result = parser.parse("String with $funcname() in it")
|
||||||
|
|
||||||
The `FuncParser` also accepts a direct dict mapping of `{'name': callable, ...}`.
|
The `FuncParser` also accepts a direct dict mapping of `{'name': callable, ...}`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
|
|
@ -53,20 +56,21 @@ from evennia.utils.utils import (
|
||||||
from evennia.utils import search
|
from evennia.utils import search
|
||||||
from evennia.utils.verb_conjugation.conjugate import verb_actor_stance_components
|
from evennia.utils.verb_conjugation.conjugate import verb_actor_stance_components
|
||||||
|
|
||||||
_CLIENT_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
# setup
|
||||||
_MAX_NESTING = 20
|
|
||||||
|
|
||||||
_ESCAPE_CHAR = "\\"
|
_CLIENT_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
||||||
_START_CHAR = "$"
|
_MAX_NESTING = settings.FUNCPARSER_MAX_NESTING
|
||||||
|
_START_CHAR = settings.FUNCPARSER_START_CHAR
|
||||||
|
_ESCAPE_CHAR = settings.FUNCPARSER_ESCAPE_CHAR
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class ParsedFunc:
|
class _ParsedFunc:
|
||||||
"""
|
"""
|
||||||
Represents a function parsed from the string
|
Represents a function parsed from the string
|
||||||
|
|
||||||
"""
|
"""
|
||||||
prefix: str = "$"
|
prefix: str = _START_CHAR
|
||||||
funcname: str = ""
|
funcname: str = ""
|
||||||
args: list = dataclasses.field(default_factory=list)
|
args: list = dataclasses.field(default_factory=list)
|
||||||
kwargs: dict = dataclasses.field(default_factory=dict)
|
kwargs: dict = dataclasses.field(default_factory=dict)
|
||||||
|
|
@ -98,7 +102,8 @@ class ParsingError(RuntimeError):
|
||||||
|
|
||||||
class FuncParser:
|
class FuncParser:
|
||||||
"""
|
"""
|
||||||
Sets up a parser for strings containing $funcname(*args, **kwargs) substrings.
|
Sets up a parser for strings containing `$funcname(*args, **kwargs)`
|
||||||
|
substrings.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -113,26 +118,23 @@ class FuncParser:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
callables (str, module, list or dict): Where to find
|
callables (str, module, list or dict): Where to find
|
||||||
'safe' functions to make available in the parser. These modules
|
'safe' functions to make available in the parser. If a `dict`,
|
||||||
can have a dict `FUNCPARSER_CALLABLES = {"funcname": callable, ...}`.
|
it should be a direct mapping `{"funcname": callable, ...}`. If
|
||||||
If no such dict exists, all callables in provided modules (whose names
|
one or mode modules or module-paths, the module(s) are first checked
|
||||||
don't start with an underscore) will be loaded as callables. Each
|
for a dict `FUNCPARSER_CALLABLES = {"funcname", callable, ...}`. If
|
||||||
callable will will be available to call as `$funcname(*args, **kwags)`
|
no such variable exists, all callables in the module (whose name does
|
||||||
during parsing. If `callables` is a `str`, this should be the path
|
not start with an underscore) will be made available to the parser.
|
||||||
to such a module. A `list` can either be a list of paths or module
|
|
||||||
objects. If a `dict`, this should be a direct mapping
|
|
||||||
`{"funcname": callable, ...}` to use.
|
|
||||||
start_char (str, optional): A character used to identify the beginning
|
start_char (str, optional): A character used to identify the beginning
|
||||||
of a parseable function. Default is `$`.
|
of a parseable function. Default is `$`.
|
||||||
escape_char (str, optional): Prepend characters with this to have
|
escape_char (str, optional): Prepend characters with this to have
|
||||||
them not count as a function. Default is `\\`.
|
them not count as a function. Default is the backtick, `\\\\`.
|
||||||
max_nesting (int, optional): How many levels of nested function calls
|
max_nesting (int, optional): How many levels of nested function calls
|
||||||
are allowed, to avoid exploitation. Default is 20.
|
are allowed, to avoid exploitation. Default is 20.
|
||||||
**default_kwargs: These kwargs will be passed into all callables. These
|
**default_kwargs: These kwargs will be passed into all callables. These
|
||||||
kwargs can be overridden both by kwargs passed direcetly to `.parse` _and_
|
kwargs can be overridden both by kwargs passed direcetly to `.parse` *and*
|
||||||
by kwargs given directly in the string `$funcname` call. They are
|
by kwargs given directly in the string `$funcname` call. They are
|
||||||
suitable for global defaults that is intended to be changed by the
|
suitable for global defaults that is intended to be changed by the
|
||||||
user. To _guarantee_ a call always gets a particular kwarg, pass it
|
user. To guarantee a call always gets a particular kwarg, pass it
|
||||||
into `.parse` as `**reserved_kwargs` instead.
|
into `.parse` as `**reserved_kwargs` instead.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -194,7 +196,7 @@ class FuncParser:
|
||||||
Execute a parsed function
|
Execute a parsed function
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
parsedfunc (ParsedFunc): This dataclass holds the parsed details
|
parsedfunc (_ParsedFunc): This dataclass holds the parsed details
|
||||||
of the function.
|
of the function.
|
||||||
raise_errors (bool, optional): Raise errors. Otherwise return the
|
raise_errors (bool, optional): Raise errors. Otherwise return the
|
||||||
string with the function unparsed.
|
string with the function unparsed.
|
||||||
|
|
@ -254,9 +256,9 @@ class FuncParser:
|
||||||
def parse(self, string, raise_errors=False, escape=False,
|
def parse(self, string, raise_errors=False, escape=False,
|
||||||
strip=False, return_str=True, **reserved_kwargs):
|
strip=False, return_str=True, **reserved_kwargs):
|
||||||
"""
|
"""
|
||||||
Use parser to parse a string that may or may not have `$funcname(*args, **kwargs)`
|
Use parser to parse a string that may or may not have
|
||||||
- style tokens in it. Only the callables used to initiate the parser
|
`$funcname(*args, **kwargs)` - style tokens in it. Only the callables
|
||||||
will be eligible for parsing, others will remain un-parsed.
|
used to initiate the parser will be eligible for parsing.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
string (str): The string to parse.
|
string (str): The string to parse.
|
||||||
|
|
@ -357,7 +359,7 @@ class FuncParser:
|
||||||
callstack.append(curr_func)
|
callstack.append(curr_func)
|
||||||
|
|
||||||
# start a new func
|
# start a new func
|
||||||
curr_func = ParsedFunc(prefix=char, fullstr=char)
|
curr_func = _ParsedFunc(prefix=char, fullstr=char)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not curr_func:
|
if not curr_func:
|
||||||
|
|
@ -592,16 +594,16 @@ def funcparser_callable_eval(*args, **kwargs):
|
||||||
incoming string into a python object. If it fails, the return will be same
|
incoming string into a python object. If it fails, the return will be same
|
||||||
as the input.
|
as the input.
|
||||||
|
|
||||||
Args
|
Args:
|
||||||
string (str): The string to parse. Only simple literals or operators are allowed.
|
string (str): The string to parse. Only simple literals or operators are allowed.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
any: The string parsed into its Python form, or the same as input.
|
any: The string parsed into its Python form, or the same as input.
|
||||||
|
|
||||||
Example:
|
Examples:
|
||||||
`$py(1)`
|
- `$py(1) -> 1`
|
||||||
`$py([1,2,3,4])`
|
- `$py([1,2,3,4] -> [1, 2, 3]`
|
||||||
`$py(3 + 4)`
|
- `$py(3 + 4) -> 7`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
args, kwargs = safe_convert_to_types(("py", {}) , *args, **kwargs)
|
args, kwargs = safe_convert_to_types(("py", {}) , *args, **kwargs)
|
||||||
|
|
@ -652,22 +654,22 @@ def _apply_operation_two_elements(*args, operator="+", **kwargs):
|
||||||
|
|
||||||
|
|
||||||
def funcparser_callable_add(*args, **kwargs):
|
def funcparser_callable_add(*args, **kwargs):
|
||||||
"""Usage: $add(val1, val2) -> val1 + val2"""
|
"""Usage: `$add(val1, val2) -> val1 + val2`"""
|
||||||
return _apply_operation_two_elements(*args, operator='+', **kwargs)
|
return _apply_operation_two_elements(*args, operator='+', **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def funcparser_callable_sub(*args, **kwargs):
|
def funcparser_callable_sub(*args, **kwargs):
|
||||||
"""Usage: $sub(val1, val2) -> val1 - val2"""
|
"""Usage: ``$sub(val1, val2) -> val1 - val2`"""
|
||||||
return _apply_operation_two_elements(*args, operator='-', **kwargs)
|
return _apply_operation_two_elements(*args, operator='-', **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def funcparser_callable_mult(*args, **kwargs):
|
def funcparser_callable_mult(*args, **kwargs):
|
||||||
"""Usage: $mult(val1, val2) -> val1 * val2"""
|
"""Usage: `$mult(val1, val2) -> val1 * val2`"""
|
||||||
return _apply_operation_two_elements(*args, operator='*', **kwargs)
|
return _apply_operation_two_elements(*args, operator='*', **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def funcparser_callable_div(*args, **kwargs):
|
def funcparser_callable_div(*args, **kwargs):
|
||||||
"""Usage: $mult(val1, val2) -> val1 / val2"""
|
"""Usage: `$mult(val1, val2) -> val1 / val2`"""
|
||||||
return _apply_operation_two_elements(*args, operator='/', **kwargs)
|
return _apply_operation_two_elements(*args, operator='/', **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -686,8 +688,8 @@ def funcparser_callable_round(*args, **kwargs):
|
||||||
any: The rounded value or inp if inp was not a number.
|
any: The rounded value or inp if inp was not a number.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
- `$round(3.5434343, 3)` - gives 3.543
|
- `$round(3.5434343, 3) -> 3.543`
|
||||||
- `$round($random(), 2)` - rounds random result, e.g 0.22
|
- `$round($random(), 2)` - rounds random result, e.g `0.22`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not args:
|
if not args:
|
||||||
|
|
@ -738,7 +740,7 @@ def funcparser_callable_random(*args, **kwargs):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if isinstance(minval, float) or isinstance(maxval, float):
|
if isinstance(minval, float) or isinstance(maxval, float):
|
||||||
return minval + maxval * random.random()
|
return minval + ((maxval - minval) * random.random())
|
||||||
else:
|
else:
|
||||||
return random.randint(minval, maxval)
|
return random.randint(minval, maxval)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -794,8 +796,8 @@ def funcparser_callable_pad(*args, **kwargs):
|
||||||
fillchar (str, optional): Character used for padding. Defaults to a space.
|
fillchar (str, optional): Character used for padding. Defaults to a space.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
- `$pad(text, 12, l, ' ')`
|
- `$pad(text, 12, r, ' ') -> " text"`
|
||||||
- `$pad(text, width=12, align=c, fillchar=-)`
|
- `$pad(text, width=12, align=c, fillchar=-) -> "----text----"`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not args:
|
if not args:
|
||||||
|
|
@ -829,8 +831,8 @@ def funcparser_callable_crop(*args, **kwargs):
|
||||||
of the string was cropped. Defaults to `[...]`.
|
of the string was cropped. Defaults to `[...]`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
`$crop(text, 78, [...])`
|
- `$crop(A long text, 10, [...]) -> "A lon[...]"`
|
||||||
`$crop(text, width=78, suffix='[...]')`
|
- `$crop(text, width=11, suffix='[...]) -> "A long[...]"`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not args:
|
if not args:
|
||||||
|
|
@ -875,8 +877,8 @@ def funcparser_callable_justify(*args, **kwargs):
|
||||||
str: The justified text.
|
str: The justified text.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
- $just(text, width=40)
|
- `$just(text, width=40)`
|
||||||
- $just(text, align=r, indent=2)
|
- `$just(text, align=r, indent=2)`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not args:
|
if not args:
|
||||||
|
|
@ -925,9 +927,9 @@ def funcparser_callable_clr(*args, **kwargs):
|
||||||
color (str, optional): If given,
|
color (str, optional): If given,
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
- `$clr(r, text, n)`
|
- `$clr(r, text, n) -> "|rtext|n"`
|
||||||
- `$clr(r, text)`
|
- `$clr(r, text) -> "|rtext|n`
|
||||||
- `$clr(text, start=r, end=n)`
|
- `$clr(text, start=r, end=n) -> "|rtext|n"`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not args:
|
if not args:
|
||||||
|
|
@ -961,7 +963,7 @@ def funcparser_callable_search(*args, caller=None, access="control", **kwargs):
|
||||||
Args:
|
Args:
|
||||||
query (str): The key or dbref to search for.
|
query (str): The key or dbref to search for.
|
||||||
|
|
||||||
Kwargs:
|
Keyword Args:
|
||||||
return_list (bool): If set, return a list of objects with
|
return_list (bool): If set, return a list of objects with
|
||||||
0, 1 or more matches to `query`. Defaults to False.
|
0, 1 or more matches to `query`. Defaults to False.
|
||||||
type (str): One of 'obj', 'account', 'script'
|
type (str): One of 'obj', 'account', 'script'
|
||||||
|
|
@ -1037,7 +1039,7 @@ def funcparser_callable_you(*args, caller=None, receiver=None, mapping=None, cap
|
||||||
Replaces with you for the caller of the string, with the display_name
|
Replaces with you for the caller of the string, with the display_name
|
||||||
of the caller for others.
|
of the caller for others.
|
||||||
|
|
||||||
Kwargs:
|
Keyword Args:
|
||||||
caller (Object): The 'you' in the string. This is used unless another
|
caller (Object): The 'you' in the string. This is used unless another
|
||||||
you-key is passed to the callable in combination with `mapping`.
|
you-key is passed to the callable in combination with `mapping`.
|
||||||
receiver (Object): The recipient of the string.
|
receiver (Object): The recipient of the string.
|
||||||
|
|
@ -1093,10 +1095,9 @@ def funcparser_callable_You(*args, you=None, receiver=None, mapping=None, capita
|
||||||
|
|
||||||
def funcparser_callable_conjugate(*args, caller=None, receiver=None, **kwargs):
|
def funcparser_callable_conjugate(*args, caller=None, receiver=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
$conj(verb)
|
|
||||||
|
|
||||||
Conjugate a verb according to if it should be 2nd or third person.
|
Conjugate a verb according to if it should be 2nd or third person.
|
||||||
Kwargs:
|
|
||||||
|
Keyword Args:
|
||||||
caller (Object): The object who represents 'you' in the string.
|
caller (Object): The object who represents 'you' in the string.
|
||||||
receiver (Object): The recipient of the string.
|
receiver (Object): The recipient of the string.
|
||||||
|
|
||||||
|
|
@ -1107,13 +1108,12 @@ def funcparser_callable_conjugate(*args, caller=None, receiver=None, **kwargs):
|
||||||
ParsingError: If `you` and `recipient` were not both supplied.
|
ParsingError: If `you` and `recipient` were not both supplied.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
Note that it will not capitalized.
|
Note that the verb will not be capitalized. It also
|
||||||
This assumes that the active party (You) is the one performing the verb.
|
assumes that the active party (You) is the one performing the verb.
|
||||||
This automatic conjugation will fail if the active part is another person
|
This automatic conjugation will fail if the active part is another person
|
||||||
than 'you'.
|
than 'you'. The caller/receiver must be passed to the parser directly.
|
||||||
The you/receiver should be passed to the parser directly.
|
|
||||||
|
|
||||||
Exampels:
|
Examples:
|
||||||
This is often used in combination with the $you/You( callables.
|
This is often used in combination with the $you/You( callables.
|
||||||
|
|
||||||
- `With a grin, $you() $conj(jump)`
|
- `With a grin, $you() $conj(jump)`
|
||||||
|
|
|
||||||
|
|
@ -348,18 +348,19 @@ class TestDefaultCallables(TestCase):
|
||||||
|
|
||||||
def test_random(self):
|
def test_random(self):
|
||||||
string = "$random(1,10)"
|
string = "$random(1,10)"
|
||||||
ret = self.parser.parse_to_any(string, raise_errors=True)
|
for i in range(100):
|
||||||
self.assertTrue(1 <= ret <= 10)
|
ret = self.parser.parse_to_any(string, raise_errors=True)
|
||||||
|
self.assertTrue(1 <= ret <= 10)
|
||||||
|
|
||||||
string = "$random()"
|
string = "$random()"
|
||||||
ret = self.parser.parse_to_any(string, raise_errors=True)
|
for i in range(100):
|
||||||
self.assertTrue(0 <= ret <= 1)
|
ret = self.parser.parse_to_any(string, raise_errors=True)
|
||||||
|
self.assertTrue(0 <= ret <= 1)
|
||||||
|
|
||||||
string = "$random(1.0, 3.0)"
|
string = "$random(1.0, 3.0)"
|
||||||
for i in range(1000):
|
for i in range(100):
|
||||||
ret = self.parser.parse_to_any(string, raise_errors=True)
|
ret = self.parser.parse_to_any(string, raise_errors=True)
|
||||||
self.assertTrue(isinstance(ret, float))
|
self.assertTrue(isinstance(ret, float))
|
||||||
print("ret:", ret)
|
|
||||||
self.assertTrue(1.0 <= ret <= 3.0)
|
self.assertTrue(1.0 <= ret <= 3.0)
|
||||||
|
|
||||||
def test_randint(self):
|
def test_randint(self):
|
||||||
|
|
|
||||||
|
|
@ -1080,7 +1080,8 @@ def repeat(interval, callback, persistent=True, idstring="", stop=False,
|
||||||
store_key (tuple, optional): This is only used in combination with `stop` and
|
store_key (tuple, optional): This is only used in combination with `stop` and
|
||||||
should be the return given from the original `repeat` call. If this
|
should be the return given from the original `repeat` call. If this
|
||||||
is given, all other args except `stop` are ignored.
|
is given, all other args except `stop` are ignored.
|
||||||
*args, **kwargs: Used as arguments to `callback`.
|
*args: Used as arguments to `callback`.
|
||||||
|
**kwargs: Keyword-arguments to pass to `callback`.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple or None: The tuple is the `store_key` - the identifier for the
|
tuple or None: The tuple is the `store_key` - the identifier for the
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue