This page is out of date

You've reached a page on the Ren'Py wiki. Due to massive spam, the wiki hasn't been updated in over 5 years, and much of the information here is very out of date. We've kept it because some of it is of historic interest, but all the information relevant to modern versions of Ren'Py has been moved elsewhere.

Some places to look are:

Please do not create new links to this page.


Summary

This code allows using any arbitrary text tag within say and menu statements and, with some extra coding, UI widgets. Due to its nature, you'll need some programming skills to make use of it, since you have to define the behavior for the tags you want to create. Basic knowledge of python should be enough (you don't need to understand the parser's code, but you should be able to understand the examples before trying to define you own tags).

Warning

The code relies on some undocumented parts of Ren'py (the renpy.display.text.text_tags dictionary and the renpy.display.text.text_tokenizer function), which seem to be made with mostly internal usage in mind. This means that these elements might change without warning in later versions of Ren'py, causing this code to stop working. The code was tested on Ren'py 6.6.

The parser

You should include this code as is within your game, either pasting it on an existing script file or creating a new file for it. It defines the function that parses the tags and the variable to hold all the needed information for the custom tags; and tells Ren'py to call this parser function for each menu and say statement.

    init python:
        
        # This dictionary holds all the custom text tags to be processed. Each element is a tuple in the form
        #  tag_name => (reqs_close, function) where:
        #  tag_name (the dictionary key) is the name for the tag, exactly as it should appear when used (string).
        #  reqs_close is a boolean defining whether the tag requires a matching closing tag or not.
        #  function is the actual function to be called. It will always be called with two positional arguments:
        #  The first argument 'arg' is the argument provided in the form {tag=arg} within the tag, or None if there was no argument.
        #  The second argument 'text' is the text enclosed in the form {tag}text{/tag} by the tag, or None for empty tags.
        #  Note that for non-empty tags that enclose no text (like in "{b}{/b}"), an empty string ("") is passed, rather than None.
        custom_text_tags = {}
        
        # This function is the heart of the module: it takes a string argument and returns that string with all the custom tags
        #  already parsed. It can be easily called from any python block; from Ren'py code there is a Ren'py mechanism to get it
        #  called for ALL say and menu statements: just assign it to the proper config variable and Ren'py will do everything else:
        # $ config.say_menu_text_filter = cttfilter
        def cttfilter(what): # stands for Custom Text-Tags filter
            """ A custom filter for say/menu statements that allows custom text tags in an extensible way.
                Note that this only enables text-manipulation tags (ie: tags that transform the text into some other text).
                It is posible for a tag's implementation to rely on other tags (like the money relying on color).
            """
            # Interpolation should take place before any text tag is processed.
            # We will not make custom text tags an exception to this rule, so let's start by interpolating:
            what = what%globals() # That will do the entire interpolation work, but there is a subtle point:
            # If a %% was included to represent a literal '%', we've already replaced it, and will be later
            #  missinterpreted by renpy itself; so we have to fix that:
            what = what.replace("%", "%%")
            
            # Ok, now let's go to the juicy part: find and process the text tags.
            # Rather than reinventing the wheel, we'll rely on renpy's own tokenizer to tokenize the text.
            # However, before doing that, we should make sure we don't mess up with built-in tags, so we'll need to identify them:
            # We'll add them to the list of custom tags, having None as their function:
            global custom_text_tags
            for k, v in renpy.display.text.text_tags.iteritems():
                # (Believe me, I know Ren'py has the list of text tags there :P )
                custom_text_tags[k] = (v, None)
            # This also makes sure none of the built-in tags is overriden.
            # Note that we cannot call None and expect it to magically reconstruct the tag.
            #  Rather than that, we'll check for that special value to avoid messing with these tags, one by one.
            # This one will be used for better readability:
            def Split(s): # Splits a tag token into a tuple that makes sense for us
                tag, arg, closing = None, None, False
                if s[0]=="/":
                    closing, s = True, s[1:]
                if s.find("=") != -1:
                    if closing:
                        raise Exception("A closing tag cannot provide arguments. Tag given: \"{/%s}\"."%s)
                    else:
                        tag, arg = s.split("=", 1)
                else:
                    tag = s
                return (tag, arg, closing)
            # We will need to keep track of what we are doing:
            # tagstack, textstack, argstack, current_text = [], [], [], ""
            # stack is a list (stack) of tuples in the form (tag, arg, preceeding text)
            stack, current_text = [], ""
            for token in renpy.display.text.text_tokenizer(what, None):
                if token[0] == 'tag':
                    tag, arg, closing = Split(token[1])
                    if closing: # closing tag
                        if len(stack)==0:
                            raise Exception("Closing tag {/%s} was found without any tag currently open. (Did you define the {%s} tag as empty?)"%(tag, tag))
                        stag, sarg, stext = stack.pop()
                        if tag==stag: # good nesting, no need to crash yet
                            if custom_text_tags[tag][1] is None: # built-in tag
                                if sarg is None: # The tag didn't take any argument
                                    current_text = "%s{%s}%s{/%s}"%(stext, tag, current_text, tag) # restore and go on
                                else: # the tag had an argument which must be restored as well
                                    current_text = "%s{%s=%s}%s{/%s}"%(stext, tag, sarg, current_text, tag) # restore and go on
                            else: # custom tag
                                current_text = "%s%s"%(stext, custom_text_tags[tag][1](sarg, current_text)) # process the tag and go on
                        else: # bad nesting, crash for good
                            raise Exception("Closing tag %s doesn't match currently open tag %s."%(tagdata[0], tagstack[-1]))
                    else: # not closing
                        if tag in custom_text_tags: # the tag exists, good news
                            if custom_text_tags[tag][0]: # the tag requires closing: just stack and it will be handled once closed
                                stack.append((tag, arg, current_text))
                                current_text = ""
                            else: # empty tag: parse without stacking
                                if custom_text_tags[tag][1] is None: # built-in tag
                                    if arg is None: # no argument
                                        current_text = "%s{%s}"%(current_text, tag)
                                    else: # there is an argument that also must be kept
                                        current_text = "%s{%s=%s}"%(current_text, tag, arg)
                                else: # custom tag
                                    current_text = "%s%s"%(current_text, custom_text_tags[tag][1](arg, None))
                        else: # the tag doesn't exist: crash accordingly
                            raise Exception("Unknown text tag \"{%s}\"."%tag)
                else: # no tag: just accumulate the text
                    current_text+=token[1]
            return current_text
        # This line tells Ren'py to replace the text ('what') of each say and menu by the result of calling cttfilter(what)
        config.say_menu_text_filter = cttfilter

That's all about the parser itself. You don't need to understand how it works to use it.

Defining tags

Custom tags are expected to produce text, optionally relying on argument and/or content values. They cannot just produce arbitrary displayables, but they can rely on built-in tags to achieve formating. Each tag is defined by a function that must:

Once the function for a tag is defined, the parser must be made aware of it. This is done by including an entry in the custom_text_tags dictionary. The key used must be the text that identifies the tag (for example, for a tag "{myTag}", the key would be "myTag"). The value is a tuple with two fields: the first one is a boolean indicating whether the tag requires a closing tag (like {/myTag}) (True) or not (False); the second field is the name of the function that should be used to process this tag.

Usage example

The following code defines two tags, feeds them to the parser's dictionary, and makes some use of them in a say statement to allow testing them. To test this example, put this code in a .rpy script file and the parser code above into another file, or just paste the parser code followed by the example's code in a single file.

    init:
        image bg black = Solid("#000")
        # Declare characters used by this game.
        $ test = Character('The Tester', color="#ffc")
        
        python:
            def tab(arg, text): # This function will handle the custom {tab=something} tag
                return " " * int(arg)
            def money(arg, text):
                total = int(arg)
                c = total % 100
                s = int(total/100) % 100
                g = int(total/10000)
                if g>0:
                    return "%d{color=#fc0}g{/color}%d{color=#999}s{/color}%d{color=#f93}c{/color}"%(g, s, c)
                elif s>0:
                    return "%d{color=#999}s{/color}%d{color=#f93}c{/color}"%(s, c)
                else:
                    return "%d{color=#f93}c{/color}"%(c)
            
            custom_text_tags["tab"]   = (False,   tab) # "Registers" the function, pairing it with the "{tab}" tag
            custom_text_tags["money"] = (False, money) # idem, for the {money} tag
            
    label start:
        scene bg black
        $ indent, cash = renpy.random.randint(0, 8), renpy.random.randint(0, 100000000)
        test "{tab=%(indent)d}(%(indent)d space indentation)\n{tab=4}{b}%(cash)d{color=#f93}c{/color}{/b}={money=%(cash)d}"
        jump start

The example combines the two custom tags with some built-in tags, and makes heavy use of interpolation, to demonstrate that the parser behaves as should be expected. So much markup and interpolation in a single statement doesn't reflect normal usage.

Compatibility considerations

As mentioned above, forward compatibility with future versions of Ren'py shouldn't be asumed, because the code relies on elements that might change without any warning. In addition, there are some points worth mentioning.

Interpolation

To makes sure that interpolation is handled before (custom) text tag parsing, the parser function simply takes care of the interpolation task itself, before starting the actual parsing task.

There is a catch, however: if the function implementing a tag includes %'s in its return value, these would be treated by Python as introducing interpolation. This might be useful on some cases, but literal %'s should be doubled "%%" if no interpolation is intended.

Escape sequences

Python escape sequences in strings, such as \n or \", are handled by python, so they will be appropriatelly interpreted.
Ren'py escape sequences such as escaped spaces (\ ) are replaced before the parser is called; so those sequences introduced by the tag functions will not be replaced.

Space compacting

Ren'py space compacting is done before the parser is called, so space introduced by the tag functions will be kept. This is the reason why the {tab} tag in the example works.

Multiple argument tags

While multiple argument tags are not explicitly supported, they can be easily emulated using python's split() function to break the argument into chunks of separate data.