├── icon.png ├── .import ├── icon.png-487276ed1e3a0c39cad0279d744ee560.md5 └── icon.png-487276ed1e3a0c39cad0279d744ee560.stex ├── default_env.tres ├── UI.tscn ├── project.godot ├── icon.png.import ├── README.rst ├── example.gd └── tracery.gd /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iansparks/godot_tracery/HEAD/icon.png -------------------------------------------------------------------------------- /.import/icon.png-487276ed1e3a0c39cad0279d744ee560.md5: -------------------------------------------------------------------------------- 1 | source_md5="8dd9ff1eebf38898a54579d8c01b0a88" 2 | dest_md5="da70afec3c66d4e872db67f808e12edb" 3 | 4 | -------------------------------------------------------------------------------- /.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iansparks/godot_tracery/HEAD/.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | 5 | [resource] 6 | background_mode = 2 7 | background_sky = SubResource( 1 ) 8 | -------------------------------------------------------------------------------- /UI.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://example.gd" type="Script" id=1] 4 | 5 | [node name="Node2D" type="Node2D"] 6 | script = ExtResource( 1 ) 7 | 8 | [node name="Label" type="Label" parent="."] 9 | margin_right = 40.0 10 | margin_bottom = 14.0 11 | text = "Run this scene to see output in the console!" 12 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=4 10 | 11 | _global_script_classes=[ { 12 | "base": "Reference", 13 | "class": "Tracery", 14 | "language": "GDScript", 15 | "path": "res://tracery.gd" 16 | } ] 17 | _global_script_class_icons={ 18 | "Tracery": "" 19 | } 20 | 21 | [application] 22 | 23 | config/name="Tracery" 24 | run/main_scene="res://UI.tscn" 25 | config/icon="res://icon.png" 26 | 27 | [rendering] 28 | 29 | environment/default_environment="res://default_env.tres" 30 | -------------------------------------------------------------------------------- /icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://icon.png" 13 | dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Tracery for Godot 2 | ================= 3 | 4 | This is a port of `Kate Compton `_'s text generation library `Tracery `_ to 5 | `Godot `_. 6 | 7 | This port is based on the `Python port of Tracery `_ by `Allison Parrish `_. 8 | 9 | 10 | Installation & Usage 11 | -------------------- 12 | 13 | See `Kate Compton's Tracery 14 | tutorial `_ for information 15 | about how Tracery works. In the Godot port, you use Godot dictionaries 16 | instead of JavaScript objects for the rules, but the concept and syntax is the same. 17 | 18 | Example usage: 19 | 20 | :: 21 | 22 | extends Node2D 23 | 24 | # Load the tracery class, 25 | var tracery_class = load("res://tracery.gd") 26 | 27 | func _ready(): 28 | # Ensure we get random values 29 | randomize() 30 | 31 | var rules = { 32 | 'origin': '#hello.capitalize#, #location#!', 33 | 'hello': ['hello', 'greetings', 'howdy', 'hey'], 34 | 'location': ['world', 'solar system', 'galaxy', 'universe'] 35 | } 36 | 37 | var tracery = tracery_class.new() 38 | var grammar = tracery.get_grammar(rules) 39 | print(grammar.flatten("#origin#")) # prints, e.g., "Hello, world!" 40 | 41 | 42 | Any valid Tracery grammar should work in this port. The ``base_english`` 43 | modifiers from Tracery are added automatically in the grammar but you can add your own. 44 | See the ``tModifiers.base_english`` func in ``tracery.gd`` for an idea of how to create 45 | modifiers. 46 | 47 | Note that many aspects of Tracery are not standardized, so in some edge cases 48 | you may get output that doesn't exactly conform to what you would get if you 49 | used the same grammar with the JavaScript version. (e.g., "null" in strings 50 | where in JavaScript you might see "undefined") 51 | 52 | 53 | Advanced Usage 54 | -------------- 55 | 56 | Tracery can be used to create more than nice text. You can also extract data from the grammar tree generated. 57 | Example 4 in the project (shown here, see the code in ```example.gd```) demonstrates saving selections in the 58 | data and then getting them back from the symbols in the grammar: 59 | 60 | :: 61 | 62 | #Saving data 63 | var example4 = { 64 | "name": ["Arjun","Yuuma","Darcy","Mia","Chiaki","Izzi","Azra","Lina"], 65 | "animal": ["unicorn","raven","sparrow","scorpion","coyote","eagle","owl","lizard","zebra","duck","kitten"], 66 | "mood": ["vexed","indignant","impassioned","wistful","astute","courteous"], 67 | "story": ["#hero# traveled with her pet #heroPet#. #hero# was never #mood#, for the #heroPet# was always too #mood#."], 68 | "origin": ["#[hero:#name#][heroPet:#animal#]story#"], 69 | } 70 | 71 | var grammar4 = self.tracery('#origin#', example4) 72 | print("Stored values >>") 73 | for name in grammar4.symbols.keys(): 74 | print(" " + name + ' = ' + grammar4.symbols[name].selected_value) 75 | 76 | 77 | Generates text like: 78 | 79 | :: 80 | 81 | Lina traveled with her pet coyote. Lina was never vexed, for the coyote was always too impassioned. 82 | Stored values >> 83 | name = Lina 84 | animal = coyote 85 | mood = impassioned 86 | story = #hero# traveled with her pet #heroPet#. #hero# was never #mood#, for the #heroPet# was always too #mood#. 87 | origin = #[hero:#name#][heroPet:#animal#]story# 88 | hero = Lina 89 | heroPet = coyote 90 | 91 | License 92 | ------- 93 | 94 | This port inherits Tracery's original Apache License 2.0 and the license of Allison Parrish's version. 95 | Note that the Apache 2.0 license means you can use this in a commercial product (but please read the license) 96 | 97 | :: 98 | 99 | Copyright 2019 Ian Sparks 100 | Based on code by Kate Compton and Allison Parrish 101 | 102 | Licensed under the Apache License, Version 2.0 (the "License"); 103 | you may not use this file except in compliance with the License. 104 | You may obtain a copy of the License at 105 | 106 | http://www.apache.org/licenses/LICENSE-2.0 107 | 108 | Unless required by applicable law or agreed to in writing, software 109 | distributed under the License is distributed on an "AS IS" BASIS, 110 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 111 | See the License for the specific language governing permissions and 112 | limitations under the License. 113 | -------------------------------------------------------------------------------- /example.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | # Load the tracery class, 4 | var tracery_class = load("res://tracery.gd") 5 | 6 | # Called when the node enters the scene tree for the first time. 7 | func _ready(): 8 | # Ensure we get random values 9 | randomize() 10 | 11 | # Running all of these will give you a [output overflow, print less text!] error 12 | # self.example1() 13 | # self.example2() 14 | # self.example3() 15 | self.example4() 16 | # self.example5() 17 | # self.example6() 18 | 19 | # Wait half a second and quit 20 | yield(get_tree().create_timer(0.5), "timeout") 21 | get_tree().quit() 22 | 23 | 24 | 25 | func example1(): 26 | # Simple example 27 | var example1 = {"animal": ["unicorn","raven","sparrow","scorpion","coyote","eagle","owl","lizard","zebra","duck","kitten"]} 28 | self.tracery('#animal#', example1) 29 | 30 | func example2(): 31 | # Building a more complex example 32 | var example2 = { 33 | "sentence": ["The #color# #animal# of the #natureNoun# is called #name#"], 34 | "color": ["orange","blue","white","black","grey","purple","indigo","turquoise"], 35 | "animal": ["unicorn","raven","sparrow","scorpion","coyote","eagle","owl","lizard","zebra","duck","kitten"], 36 | "natureNoun": ["ocean","mountain","forest","cloud","river","tree","sky","sea","desert"], 37 | "name": ["Arjun","Yuuma","Darcy","Mia","Chiaki","Izzi","Azra","Lina"], 38 | } 39 | self.tracery('#sentence#', example2) 40 | 41 | func example3(): 42 | # Modifiers (.a, .capitalize..) 43 | var example3 = { 44 | "sentence": ["#color.capitalize# #animal.s# are #often# #mood#.","#animal.a.capitalize# is #often# #mood#, unless it is #color.a# one."], 45 | "often": ["rarely","never","often","almost always","always","sometimes"], 46 | "color": ["orange","blue","white","black","grey","purple","indigo","turquoise"], 47 | "animal": ["unicorn","raven","sparrow","scorpion","coyote","eagle","owl","lizard","zebra","duck","kitten"], 48 | "mood": ["vexed","indignant","impassioned","wistful","astute","courteous"], 49 | "natureNoun": ["ocean","mountain","forest","cloud","river","tree","sky","earth","void","desert"], 50 | } 51 | self.tracery('#sentence#', example3) 52 | 53 | func example4(): 54 | #Saving data 55 | var example4 = { 56 | "name": ["Arjun","Yuuma","Darcy","Mia","Chiaki","Izzi","Azra","Lina"], 57 | "animal": ["unicorn","raven","sparrow","scorpion","coyote","eagle","owl","lizard","zebra","duck","kitten"], 58 | "mood": ["vexed","indignant","impassioned","wistful","astute","courteous"], 59 | "story": ["#hero# traveled with her pet #heroPet#. #hero# was never #mood#, for the #heroPet# was always too #mood#."], 60 | "origin": ["#[hero:#name#][heroPet:#animal#]story#"], 61 | } 62 | var grammar4 = self.tracery('#origin#', example4) 63 | print("Stored values >>") 64 | for name in grammar4.symbols.keys(): 65 | print(" " + name + ' = ' + grammar4.symbols[name].selected_value) 66 | 67 | func example5(): 68 | # Super advanced 69 | var example5 = { 70 | "name": ["Cheri","Fox","Morgana","Jedoo","Brick","Shadow","Krox","Urga","Zelph"], 71 | "story": ["#hero.capitalize# was a great #occupation#, and this song tells of #heroTheir# adventure. #hero.capitalize# #didStuff#, then #heroThey# #didStuff#, then #heroThey# went home to read a book."], 72 | "monster": ["dragon","ogre","witch","wizard","goblin","golem","giant","sphinx","warlord"], 73 | "setPronouns": ["[heroThey:they][heroThem:them][heroTheir:their][heroTheirs:theirs]","[heroThey:she][heroThem:her][heroTheir:her][heroTheirs:hers]","[heroThey:he][heroThem:him][heroTheir:his][heroTheirs:his]"], 74 | "setOccupation": ["[occupation:baker][didStuff:baked bread,decorated cupcakes,folded dough,made croissants,iced a cake]","[occupation:warrior][didStuff:fought #monster.a#,saved a village from #monster.a#,battled #monster.a#,defeated #monster.a#]"], 75 | "origin": ["#[#setPronouns#][#setOccupation#][hero:#name#]story#"] 76 | } 77 | 78 | var grammar5 = self.tracery('#origin#', example5) 79 | print("Stored values >>") 80 | for name in grammar5.symbols.keys(): 81 | print(" " + name + ' = ' + grammar5.symbols[name].selected_value) 82 | 83 | func example6(): 84 | # Nested stories 85 | var example6 = { 86 | "name": ["Arjun","Yuuma","Darcy","Mia","Chiaki","Izzi","Azra","Lina"], 87 | "animal": ["unicorn","raven","sparrow","scorpion","coyote","eagle","owl","lizard","zebra","duck","kitten"], 88 | "occupationBase": ["wizard","witch","detective","ballerina","criminal","pirate","lumberjack","spy","doctor","scientist","captain","priest"], 89 | "occupationMod": ["occult ","space ","professional ","gentleman ","erotic ","time ","cyber","paleo","techno","super"], 90 | "strange": ["mysterious","portentous","enchanting","strange","eerie"], 91 | "tale": ["story","saga","tale","legend"], 92 | "occupation": ["#occupationMod##occupationBase#"], 93 | "mood": ["vexed","indignant","impassioned","wistful","astute","courteous"], 94 | "setPronouns": ["[heroThey:they][heroThem:them][heroTheir:their][heroTheirs:theirs]","[heroThey:she][heroThem:her][heroTheir:her][heroTheirs:hers]","[heroThey:he][heroThem:him][heroTheir:his][heroTheirs:his]"], 95 | "setSailForAdventure": ["set sail for adventure","left #heroTheir# home","set out for adventure","went to seek #heroTheir# forture"], 96 | "setCharacter": ["[#setPronouns#][hero:#name#][heroJob:#occupation#]"], 97 | "openBook": ["An old #occupation# told #hero# a story. 'Listen well' she said to #hero#, 'to this #strange# #tale#. ' #origin#'","#hero# went home.","#hero# found an ancient book and opened it. As #hero# read, the book told #strange.a# #tale#: #origin#"], 98 | "story": ["#hero# the #heroJob# #setSailForAdventure#. #openBook#"], 99 | "origin": ["Once upon a time, #[#setCharacter#]story#"], 100 | } 101 | var grammar6 = self.tracery('#origin#', example6) 102 | print("Stored values >>") 103 | for name in grammar6.symbols.keys(): 104 | print(" " + name + ' = ' + grammar6.symbols[name].selected_value) 105 | 106 | 107 | func tracery(entry_point, rules): 108 | # Run tracery 109 | var tracery = tracery_class.new() 110 | var grammar = tracery.get_grammar(rules) 111 | print("\n---------------- " + entry_point + " ----------------") 112 | print(grammar.flatten(entry_point)) 113 | return grammar 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /tracery.gd: -------------------------------------------------------------------------------- 1 | class_name Tracery 2 | 3 | func get_grammar(input_rules:Dictionary): 4 | var grammar = tGrammar.new(input_rules) 5 | return grammar 6 | 7 | class tGrammar: 8 | 9 | var rules = {} 10 | var errors = [] 11 | var modifiers = {} 12 | var symbols = {} 13 | var subgrammars:Array = [] 14 | 15 | func _init(rules:Dictionary): 16 | self.rules = rules 17 | 18 | # Load default rules 19 | self.modifiers = tModifiers.base_english() 20 | self.errors = [] 21 | self.symbols = {} 22 | self.subgrammars = [] 23 | 24 | # Load symbolds from the passed in rules 25 | for k in rules.keys(): 26 | self.symbols[k] = tSymbol.new(self, k, rules[k]) 27 | 28 | func flatten(rule:String, allow_escape_chars:bool = false): 29 | var root = self.expand(rule, allow_escape_chars) 30 | return root.finished_text 31 | 32 | func create_root(rule): 33 | return tNode.new(self, 0, {'type': -1, 'raw': rule}) 34 | 35 | func expand(rule:String, allow_escape_chars:bool): 36 | var root = self.create_root(rule) 37 | root.expand() 38 | if not allow_escape_chars: 39 | root.clear_escape_chars() 40 | Utils.extend_array(self.errors, root.errors) 41 | return root 42 | 43 | func push_rules(key:String, raw_rules): 44 | if !(key in self.symbols): 45 | self.symbols[key] = tSymbol.new(self, key, raw_rules) 46 | else: 47 | self.symbols[key].push_rules(raw_rules) 48 | 49 | func pop_rules(key:String): 50 | if !(key in self.symbols): 51 | self.errors.append("Can't pop: no symbol for key " + key) 52 | else: 53 | self.symbols[key].pop_rules() 54 | 55 | func select_rule(key:String, node:tNode, errors:Array): 56 | if key in self.symbols: 57 | return self.symbols[key].select_rule(node, errors) 58 | else: 59 | if key == null: 60 | key = "null" 61 | self.errors.append("No symbol for " + key) 62 | return "((" + key + "))" 63 | 64 | # Utilities 65 | class Utils: 66 | static func extend_array(array:Array, with_elements:Array): 67 | # Extend an array with a set of elements 68 | for element in with_elements: 69 | array.append(element) 70 | 71 | static func slice_string(input_string:String, start:int, end:int): 72 | var return_string = "" 73 | if end == -1: 74 | end = input_string.length() 75 | 76 | for i in range(0, input_string.length()): 77 | if i>= start and i <= end-1: 78 | return_string += input_string[i] 79 | 80 | return return_string 81 | 82 | static func slice_array(input_array:Array, start:int, end:int): 83 | var return_array = [] 84 | 85 | if end == -1: 86 | end = input_array.size() 87 | 88 | for i in range(0, input_array.size()): 89 | if i >= start and i <= end: 90 | return_array.append(input_array[i]) 91 | 92 | return return_array 93 | 94 | static func parse_tag(tag_contents): 95 | # returns a dictionary with 'symbol', 'modifiers', 'preactions', 'postactions' 96 | var parsed = { 97 | "symbol" : null, 98 | "preactions" : [], 99 | "postactions" : [], 100 | "modifiers" : [] 101 | } 102 | var parse_result = Utils.parse(tag_contents) 103 | var sections = parse_result.sections 104 | #var errors = parse_result.errors 105 | 106 | var symbol_section = null 107 | for section in sections: 108 | if section['type'] == 0: 109 | if symbol_section == null: 110 | symbol_section = section['raw'] 111 | else: 112 | # Exception 113 | print("EXCEPTION! multiple main sections in " + tag_contents) 114 | 115 | else: 116 | parsed['preactions'].append(section) 117 | if symbol_section != null: 118 | var components = symbol_section.split(".") 119 | parsed.symbol = components[0] 120 | parsed.modifiers = Utils.slice_array(components, 1,-1) 121 | return parsed 122 | 123 | static func create_section(start, end, type, errors, rule, last_escaped_char, escaped_substring): 124 | if end - start < 1: 125 | if type == 1: 126 | errors.append("{}:empty tag".format(start)) 127 | elif type == 2: 128 | errors.append("{}:empty action".format(start)) 129 | var raw_substring = null 130 | if last_escaped_char != null: 131 | raw_substring = escaped_substring + "\\" + slice_string(rule, last_escaped_char+1, end) 132 | else: 133 | raw_substring = slice_string(rule, start, end) 134 | var ret = {'type': type, 'raw': raw_substring} 135 | return ret 136 | 137 | static func parse(rule:String) -> Dictionary: 138 | var depth = 0 139 | var in_tag = false 140 | var sections = [] 141 | var escaped = false 142 | var errors = [] 143 | var start = 0 144 | var escaped_substring = "" 145 | var last_escaped_char = null 146 | 147 | if rule == null: 148 | return sections 149 | 150 | for i in range(0, rule.length()): 151 | var c = rule[i] 152 | if !escaped: 153 | if c == '[': 154 | if depth == 0 and !in_tag: 155 | if start < i: 156 | var s = Utils.create_section(start, i, 0, errors, rule, last_escaped_char, escaped_substring) 157 | sections.append(s) 158 | last_escaped_char = null 159 | escaped_substring = "" 160 | start = i + 1 161 | depth += 1 162 | elif c == ']': 163 | depth -= 1 164 | if depth == 0 and !in_tag: 165 | var s = Utils.create_section(start, i, 2, errors, rule, last_escaped_char, escaped_substring) 166 | sections.append(s) 167 | last_escaped_char = null 168 | escaped_substring = "" 169 | start = i + 1 170 | elif c == '#': 171 | if depth == 0: 172 | if in_tag: 173 | var s = Utils.create_section(start, i, 1, errors, rule, last_escaped_char, escaped_substring) 174 | sections.append(s) 175 | last_escaped_char = null 176 | escaped_substring = "" 177 | else: 178 | if start < i: 179 | var s = Utils.create_section(start, i, 0, errors, rule, last_escaped_char, escaped_substring) 180 | sections.append(s) 181 | last_escaped_char = null 182 | escaped_substring = "" 183 | start = i + 1 184 | in_tag = !in_tag 185 | elif c == '\\': 186 | escaped = true 187 | escaped_substring = escaped_substring + slice_string(rule, start, i) 188 | start = i + 1 189 | last_escaped_char = i 190 | else: 191 | escaped = false 192 | 193 | if start < rule.length(): 194 | var s = Utils.create_section(start, rule.length(), 0, errors, rule, last_escaped_char, escaped_substring) 195 | sections.append(s) 196 | last_escaped_char = null 197 | escaped_substring = "" 198 | 199 | if in_tag: 200 | errors.append("unclosed tag") 201 | if depth > 0: 202 | errors.append("too many [") 203 | if depth < 0: 204 | errors.append("too many ]") 205 | 206 | # Filter sections to remove those of type 0 and length 0 207 | var return_sections = [] 208 | for s in sections: 209 | if !(s.type == 0 and s.raw.length() == 0): 210 | return_sections.append(s) 211 | 212 | return {'sections':return_sections, 'errors':errors} 213 | 214 | class tNode: 215 | var action = null 216 | var parent = null 217 | var errors:Array = [] 218 | var preactions:Array = [] 219 | var postactions:Array = [] 220 | var expansion_errors:Array = [] 221 | var is_expanded:bool = false 222 | var children:Array = [] 223 | var child_rule:String 224 | var raw:String = '' 225 | var type = null 226 | var grammar = null 227 | var depth = 0 228 | var child_index = 0 229 | var symbol 230 | var modifiers 231 | var finished_text:String = "" 232 | 233 | func _init(_parent, child_index:int, settings:Dictionary ): 234 | self.parent = _parent 235 | if settings.get('raw') == null: 236 | self.errors.append("Empty input for node") 237 | settings.raw = "" 238 | if parent is tGrammar: 239 | self.grammar = parent 240 | self.parent = null 241 | self.depth = 0 242 | self.child_index = 0 243 | else: 244 | self.grammar = parent.grammar 245 | self.parent = parent 246 | self.depth = parent.depth + 1 247 | self.child_index = child_index 248 | self.raw = settings['raw'] 249 | self.type = settings.get('type', null) 250 | self.is_expanded = false 251 | 252 | func expand_tag(prevent_recursion): 253 | self.preactions = [] 254 | self.postactions = [] 255 | var parsed = Utils.parse_tag(self.raw) 256 | self.symbol = parsed['symbol'] 257 | self.modifiers = parsed['modifiers'] 258 | for preaction in parsed['preactions']: 259 | var node_action = tNodeAction.new(self, preaction['raw']) 260 | self.preactions.append(node_action) 261 | for preaction in self.preactions: 262 | if preaction.type == 0: 263 | self.postactions.append(preaction.create_undo()) 264 | for preaction in self.preactions: 265 | preaction.activate() 266 | 267 | self.finished_text = self.raw 268 | var selected_rule = self.grammar.select_rule(self.symbol, self, self.errors) 269 | self.expand_children(selected_rule, prevent_recursion) 270 | 271 | # apply modifiers (capitalization, pluralization etc) 272 | for mod_name in self.modifiers: 273 | var mod_params = [] 274 | if '(' in mod_name: 275 | var regex = RegEx.new() 276 | # Invalid regex - fix me later 277 | var regexp = regex.compile('[^]+') 278 | var matches = regexp.search_all(mod_name) 279 | if len(matches) > 0: 280 | mod_params = matches[0].split(",") 281 | mod_name = mod_name.substr(0, mod_name.find('(')) 282 | 283 | var mod:FuncRef = self.grammar.modifiers.get(mod_name, null) 284 | if mod == null: 285 | self.errors.append("Missing modifier " + mod_name) 286 | self.finished_text += "((." + mod_name + "))" 287 | else: 288 | var value = mod.call_func(self.finished_text, mod_params) 289 | self.finished_text = value 290 | 291 | func expand(prevent_recursion=false): 292 | if !self.is_expanded: 293 | self.is_expanded = true 294 | self.expansion_errors = [] 295 | # Types of nodes 296 | # -1: raw, needs parsing 297 | # 0: Plaintext 298 | # 1: Tag ("#symbol.mod.mod2.mod3#" or 299 | # "#[pushTarget:pushRule]symbol.mod") 300 | # 2: Action ("[pushTarget:pushRule], [pushTarget:POP]", 301 | # more in the future) 302 | match self.type: 303 | -1: 304 | self.expand_children(self.raw, prevent_recursion) 305 | 0: 306 | self.finished_text = self.raw 307 | 1: 308 | self.expand_tag(prevent_recursion) 309 | 2: 310 | self.action = tNodeAction.new(self, self.raw) 311 | self.action.activate() 312 | self.finished_text = "" 313 | 314 | 315 | func expand_children(child_rule, prevent_recursion=false): 316 | self.children = [] 317 | self.finished_text = "" 318 | 319 | self.child_rule = child_rule 320 | if self.child_rule != null: 321 | var parse_result = Utils.parse(child_rule) 322 | for error in parse_result.errors: 323 | self.errors.append(errors) 324 | for i in range(0, parse_result.sections.size()): 325 | var section = parse_result.sections[i] 326 | var node = tNode.new(self, i, section) 327 | self.children.append(node) 328 | if !prevent_recursion: 329 | node.expand(prevent_recursion) 330 | self.finished_text += node.finished_text 331 | else: 332 | self.errors.append("No child rule provided, can't expand children") 333 | 334 | func clear_escape_chars(): 335 | self.finished_text = self.finished_text.replace("\\\\", "DOUBLEBACKSLASH").replace("\\", "").replace("DOUBLEBACKSLASH", "\\") 336 | 337 | 338 | 339 | class tNodeAction: 340 | 341 | var node = null 342 | var target 343 | var rule:String 344 | var rule_sections:Array 345 | var rule_nodes:Array 346 | var finished_rules:Array 347 | var type 348 | 349 | func _init(node:tNode, raw:String): 350 | self.node = node 351 | var sections = raw.split(':') 352 | self.target = sections[0] 353 | if sections.size() == 1: 354 | self.type = 2 355 | else: 356 | self.rule = sections[1] 357 | if self.rule == "POP": 358 | self.type = 1 359 | else: 360 | self.type = 0 361 | 362 | func create_undo(): 363 | if self.type == 0: 364 | var na = tNodeAction.new(self.node, self.target + ":POP") 365 | return na 366 | return null 367 | 368 | func activate(): 369 | var grammar = self.node.grammar 370 | if self.type == 0: 371 | self.rule_sections = self.rule.split(",") 372 | self.finished_rules = [] 373 | self.rule_nodes = [] 374 | for rule_section in self.rule_sections: 375 | var n = tNode.new(grammar, 0, {'type': -1, 'raw': rule_section}) 376 | n.expand() 377 | self.finished_rules.append(n.finished_text) 378 | grammar.push_rules(self.target, self.finished_rules) 379 | elif self.type == 1: 380 | grammar.pop_rules(self.target) 381 | elif self.type == 2: 382 | grammar.flatten(self.target, true) 383 | 384 | 385 | class tSymbol: 386 | 387 | var grammar:tGrammar 388 | var key:String 389 | 390 | # The value that gets selected for this symbol 391 | var selected_value:String 392 | # Stack is an array of tRuleSet 393 | var stack:Array = [] 394 | var raw_rules 395 | var uses:Array = [] 396 | var base_rules:tRuleSet 397 | 398 | 399 | func _init(grammar:tGrammar, key:String, raw_rules): 400 | self.grammar = grammar 401 | self.key = key 402 | self.raw_rules = raw_rules 403 | self.base_rules = tRuleSet.new(grammar, raw_rules) 404 | self.clear_state() 405 | 406 | func clear_state(): 407 | self.stack = [self.base_rules] 408 | self.uses = [] 409 | self.base_rules.clear_state() 410 | 411 | func push_rules(raw_rules): 412 | var rules = tRuleSet.new(self.grammar, raw_rules) 413 | self.stack.append(rules) 414 | 415 | func pop_rules(): 416 | self.stack.pop_back() 417 | 418 | func select_rule(node, errors): 419 | self.uses.append({'node': node}) 420 | if self.stack.size() == 0: 421 | errors.append("The rule stack for '%s' is empty, too many pops?" % self.key) 422 | self.selected_value = self.stack.back().select_rule() 423 | return self.selected_value 424 | 425 | # func get_active_rules(): 426 | # if self.stack.size() == 0: 427 | # return null 428 | # return self.stack.back().select_rule() 429 | 430 | 431 | class tRuleSet: 432 | 433 | var raw 434 | var grammar:tGrammar 435 | var default_uses:Array = [] 436 | var default_rules:Array = [] 437 | 438 | func _init(grammar, raw): 439 | self.raw = raw 440 | self.grammar = grammar 441 | self.default_uses = [] 442 | if raw is Array: 443 | self.default_rules = raw 444 | elif raw is String: 445 | self.default_rules = [raw] 446 | else: 447 | self.default_rules = [] 448 | 449 | func clear_state(): 450 | self.default_uses = [] 451 | 452 | func select_rule(): 453 | # The method for selecting a rule is just to take a random one. 454 | # This makes it easy to deal with arrays that come from JSON 455 | # but you could have more complex rules like removing an option from the 456 | # array once it's been used or has been used N times and having weightings on 457 | # the entries. You could use self.grammer and pass the rules into the grammar 458 | return self.default_rules[randi() % self.default_rules.size()] 459 | 460 | 461 | class tModifiers: 462 | 463 | const VOWELS:Array = ["a","e","i","o","u"] 464 | 465 | static func replace(text:String, params_list:Array) -> String: 466 | return text.replace(params_list[0], params_list[1]) 467 | 468 | static func capitalizeAll(text:String, params_list:Array) -> String: 469 | return text.capitalize() 470 | 471 | static func capitalize(text:String, params_list:Array) -> String: 472 | var first = text[0] 473 | var rest = text.right(1) 474 | return first.to_upper() + rest 475 | 476 | static func a(text:String, params_list:Array) -> String: 477 | if text.length() > 0: 478 | if text[0].to_lower() == "u": 479 | if text.length() > 2: 480 | if text[2].to_lower() == "i": 481 | return "a " + text 482 | if text[0].to_lower() in VOWELS: 483 | return "an " + text 484 | return "a " + text 485 | 486 | static func firstS(text:String, params_list:Array) -> String: 487 | var return_string:String = "" 488 | var text2 = text.split(" ") 489 | 490 | return_string = tModifiers.s(text2[0], []) 491 | return_string += text2.right(1) 492 | return return_string 493 | 494 | static func s(text:String, params_list:Array) -> String: 495 | var last_char = text[text.length()-1].to_lower() 496 | if last_char in ["s","h","x"]: 497 | return text + "es" 498 | elif last_char == "y": 499 | var last_but_one_char = text[text.length()-2].to_lower() 500 | if !(last_but_one_char in VOWELS): 501 | return text.substr(0, text.length()-2) + "ies" 502 | else: 503 | return text + "s" 504 | return text + "s" 505 | 506 | static func ed(text:String, params_list:Array) -> String: 507 | var last_char = text[text.length()-1].to_lower() 508 | if last_char == "e": 509 | return text + "d" 510 | elif last_char == "y": 511 | var last_but_one_char = text[text.length()-2].to_lower() 512 | if !(last_but_one_char in VOWELS): 513 | return text.substr(0, text.length()-2) + "ied" 514 | return text + "ed" 515 | 516 | static func uppercase(text:String, params_list:Array) -> String: 517 | return text.to_upper() 518 | 519 | static func lowercase(text:String, params_list:Array) -> String: 520 | return text.to_lower() 521 | 522 | static func base_english() -> Dictionary: 523 | # Get dictionary of function references to our modifier functions 524 | return { 525 | 'replace': funcref(tModifiers, "replace"), 526 | 'capitalizeAll': funcref(tModifiers, "capitalizeAll"), 527 | 'capitalize': funcref(tModifiers, "capitalize"), 528 | 'a': funcref(tModifiers, "a"), 529 | 'firstS': funcref(tModifiers, "firstS"), 530 | 's': funcref(tModifiers, "s"), 531 | 'ed': funcref(tModifiers, "ed"), 532 | 'uppercase': funcref(tModifiers, "uppercase"), 533 | 'lowercase': funcref(tModifiers, "lowercase") 534 | } --------------------------------------------------------------------------------