Just edited for Ren'py 6.18 compatibility.
Code: Select all
init -1 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.text.extras.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.text.textsupport.tokenize(unicode(what)):
if token[0] == 2:
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