├── 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 | }
--------------------------------------------------------------------------------