tracery 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1ec31ad1ba40cf47f031700475119b6a474300f0
4
+ data.tar.gz: 5efe8a2fbe3751bbbdbef4545336b86b84063ae3
5
+ SHA512:
6
+ metadata.gz: 4e9f09c43cbb638172af396452399c04cadb338a2414ea25625b995ddace2ceef805fce1a5595b56d13f2f19855850face9f0eef00ad8775adcf73e081a21e5b
7
+ data.tar.gz: be63c6d1c29154a101cd3a91cc838be9b9f4fbfd6031a0489ffd474851a39ff71b7eedbace080dc017a826ffa564e14d7f9c99b7597a331188de459a72210fa9
@@ -0,0 +1,88 @@
1
+ module Modifiers
2
+ def self.isVowel(c)
3
+ return ['a', 'e', 'i', 'o', 'u'].member?(c.downcase)
4
+ end
5
+
6
+ def self.pluralize(s)
7
+ case(s[-1])
8
+ when 's' then
9
+ return s + "es"
10
+ when 'h' then
11
+ return s + "es"
12
+ when 'x' then
13
+ return s + "es"
14
+ when 'y' then
15
+ if(!isVowel(s[-2])) then
16
+ return s[0...-1] + "ies"
17
+ else
18
+ return s + "s"
19
+ end
20
+ else
21
+ return s + "s"
22
+ end
23
+ end
24
+
25
+ def self.baseEngModifiers
26
+ {
27
+ "replace" => lambda do |s, parameters|
28
+ return s.gsub(/#{Regexp.quote(parameters[0])}/, parameters[1])
29
+ end,
30
+
31
+ "capitalizeAll" => lambda do |s, parameters|
32
+ return s.gsub(/\w+/) {|word| word.capitalize}
33
+ end,
34
+
35
+ "capitalize" => lambda do |s, parameters|
36
+ return s.capitalize
37
+ end,
38
+
39
+ "a" => lambda do |s, parameters|
40
+ if(s.length > 0) then
41
+ if(s =~ /^u((\wi)|(\W))/) then
42
+ #catches "university" and "u-boat"
43
+ return "a #{s}"
44
+ end
45
+
46
+ if(isVowel(s[0])) then
47
+ return "an #{s}"
48
+ end
49
+ end
50
+
51
+ return "a #{s}"
52
+ end,
53
+
54
+ "firstS" => lambda do |s, parameters|
55
+ words = s.split(" ")
56
+ if(words.length > 0) then
57
+ words[0] = pluralize words[0]
58
+ end
59
+ return words.join " "
60
+ end,
61
+
62
+ "s" => lambda do |s, parameters|
63
+ return pluralize(s)
64
+ end,
65
+
66
+ "ed" => lambda do |s, parameters|
67
+ case(s[-1])
68
+ when 's' then
69
+ return s + "ed"
70
+ when 'e' then
71
+ return s + "d"
72
+ when 'h' then
73
+ return s + "ed"
74
+ when 'x' then
75
+ return s + "ed"
76
+ when 'y' then
77
+ if(!isVowel(s[-2])) then
78
+ return s[0...-1] + "ied"
79
+ else
80
+ return s + "d"
81
+ end
82
+ else
83
+ return s + "ed"
84
+ end
85
+ end
86
+ }
87
+ end
88
+ end
@@ -0,0 +1,637 @@
1
+ require 'json'
2
+
3
+
4
+ module Tracery
5
+ # Parses a plaintext rule in the tracery syntax
6
+ def createGrammar(raw)
7
+ return Grammar.new(raw)
8
+ end
9
+
10
+ def parseTag(tagContents)
11
+ parsed = {
12
+ symbol: nil,
13
+ preactions: [],
14
+ postactions: [],
15
+ modifiers: []
16
+ }
17
+
18
+ sections = parse(tagContents)[:sections]
19
+ symbolSection = nil;
20
+ sections.each do |section|
21
+ if(section[:type] == 0) then
22
+ if(symbolSection.nil?) then
23
+ symbolSection = section[:raw]
24
+ else
25
+ raise "multiple main sections in #{tagContents}"
26
+ end
27
+ else
28
+ parsed[:preactions].push(section)
29
+ end
30
+ end
31
+
32
+ if(symbolSection.nil?) then
33
+ # raise "no main section in #{tagContents}"
34
+ else
35
+
36
+ components = symbolSection.split(".");
37
+ parsed[:symbol] = components.first
38
+ parsed[:modifiers] = components.drop(1)
39
+ end
40
+
41
+ return parsed
42
+ end
43
+
44
+ #TODO_: needs heavy refactoring -- no nesting in ruby (ie. move entire parser to another class w/ shared state)
45
+ def createSection(start, finish, type, results, lastEscapedChar, escapedSubstring, rule, errors)
46
+ if(finish - start < 1) then
47
+ if(type == 1) then
48
+ errors << "#{start}: empty tag"
49
+ else
50
+ if(type == 2) then
51
+ errors << "#{start}: empty action"
52
+ end
53
+ end
54
+ end
55
+ rawSubstring = ""
56
+ if(!lastEscapedChar.nil?) then
57
+ rawSubstring = escapedSubstring + "\\" + rule[(lastEscapedChar+1)..-1]
58
+ else
59
+ rawSubstring = rule[start...finish]
60
+ end
61
+
62
+ results[:sections] << {
63
+ type: type,
64
+ raw: rawSubstring
65
+ }
66
+ end
67
+
68
+ def parse(rule)
69
+ depth = 0
70
+ inTag = false
71
+ results = {errors: [], sections: []}
72
+ escaped = false
73
+
74
+ errors = []
75
+ start = 0
76
+
77
+ escapedSubstring = ""
78
+ lastEscapedChar = nil
79
+
80
+ if(rule.nil?) then
81
+ sections = {errors: errors, sections: []}
82
+ return sections
83
+ end
84
+
85
+ rule.each_char.with_index do |c, i|
86
+ if(!escaped) then
87
+ case(c)
88
+ when '[' then
89
+ # Enter a deeper bracketed section
90
+ if(depth == 0 && !inTag) then
91
+ if(start < i) then
92
+ createSection(start, i, 0, results, lastEscapedChar, escapedSubstring, rule, errors)
93
+ lastEscapedChar = nil
94
+ escapedSubstring = ""
95
+ end
96
+ start = i + 1
97
+ end
98
+ depth += 1
99
+ when ']' then
100
+ depth -= 1
101
+ # End a bracketed section
102
+ if(depth == 0 && !inTag) then
103
+ createSection(start, i, 2, results, lastEscapedChar, escapedSubstring, rule, errors)
104
+ lastEscapedChar = nil
105
+ escapedSubstring = ""
106
+ start = i + 1
107
+ end
108
+ when '#' then
109
+ # Hashtag
110
+ # ignore if not at depth 0, that means we are in a bracket
111
+ if(depth == 0) then
112
+ if(inTag) then
113
+ createSection(start, i, 1, results, lastEscapedChar, escapedSubstring, rule, errors)
114
+ lastEscapedChar = nil
115
+ escapedSubstring = ""
116
+ start = i + 1
117
+ else
118
+ if(start < i) then
119
+ createSection(start, i, 0, results, lastEscapedChar, escapedSubstring, rule, errors)
120
+ lastEscapedChar = nil
121
+ escapedSubstring = ""
122
+ end
123
+ start = i + 1
124
+ end
125
+ inTag = !inTag
126
+ end
127
+ when '\\' then
128
+ escaped = true;
129
+ escapedSubstring = escapedSubstring + rule[start...i];
130
+ start = i + 1;
131
+ lastEscapedChar = i;
132
+ end
133
+ else
134
+ escaped = false
135
+ end
136
+ end #each character in rule
137
+
138
+ if(start < rule.length) then
139
+ createSection(start, rule.length, 0, results, lastEscapedChar, escapedSubstring, rule, errors)
140
+ lastEscapedChar = nil
141
+ escapedSubstring = ""
142
+ end
143
+
144
+ errors << ("Unclosed tag") if inTag
145
+ errors << ("Too many [") if depth > 0
146
+ errors << ("Too many ]") if depth < 0
147
+
148
+ # Strip out empty plaintext sections
149
+ results[:sections].select! {|section|
150
+ if(section[:type] == 0 && section[:raw].empty?) then
151
+ false
152
+ else
153
+ true
154
+ end
155
+ }
156
+ results[:errors] = errors;
157
+ return results
158
+ end
159
+ end
160
+
161
+ class TraceryNode
162
+ attr_accessor :grammar, :depth, :finishedText, :children, :errors
163
+
164
+ include Tracery
165
+
166
+ def initialize(parent, childIndex, settings)
167
+ @errors = []
168
+ @children = []
169
+
170
+ if(settings[:raw].nil?) then
171
+ @errors << "Empty input for node"
172
+ settings[:raw] = ""
173
+ end
174
+
175
+ # If the root node of an expansion, it will have the grammar passed as the 'parent'
176
+ # set the grammar from the 'parent', and set all other values for a root node
177
+ if(parent.is_a? Grammar)
178
+ @grammar = parent
179
+ @parent = nil
180
+ @depth = 0
181
+ @childIndex = 0
182
+ else
183
+ @grammar = parent.grammar
184
+ @parent = parent
185
+ @depth = parent.depth + 1
186
+ @childIndex = childIndex
187
+ end
188
+
189
+ @raw = settings[:raw]
190
+ @type = settings[:type]
191
+ @isExpanded = false
192
+
193
+ @errors << "No grammar specified for this node #{self}" if (@grammar.nil?)
194
+ end
195
+
196
+ def to_s
197
+ "Node('#{@raw}' #{@type} d:#{@depth})"
198
+ end
199
+
200
+ def expandChildren(childRule, preventRecursion)
201
+ @finishedText = ""
202
+ @childRule = childRule
203
+
204
+ if(!@childRule.nil?)
205
+ parsed = parse(childRule)
206
+ sections = parsed[:sections]
207
+
208
+ @errors.concat(parsed[:errors])
209
+
210
+ sections.each_with_index do |section, i|
211
+ child = TraceryNode.new(self, i, section)
212
+ if(!preventRecursion)
213
+ child.expand(preventRecursion)
214
+ end
215
+ @finishedText += child.finishedText
216
+ @children << child
217
+ end
218
+ else
219
+ # In normal operation, this shouldn't ever happen
220
+ @errors << "No child rule provided, can't expand children"
221
+ end
222
+ end
223
+
224
+ # Expand this rule (possibly creating children)
225
+ def expand(preventRecursion = false)
226
+ if(@isExpanded) then
227
+ @errors << "Already expanded #{self}"
228
+ return
229
+ end
230
+
231
+ @isExpanded = true
232
+ #this is no longer used
233
+ @expansionErrors = []
234
+
235
+ # Types of nodes
236
+ # -1: raw, needs parsing
237
+ # 0: Plaintext
238
+ # 1: Tag ("#symbol.mod.mod2.mod3#" or "#[pushTarget:pushRule]symbol.mod#")
239
+ # 2: Action ("[pushTarget:pushRule], [pushTarget:POP]", more in the future)
240
+
241
+ case(@type)
242
+ when -1 then
243
+ #raw rule
244
+ expandChildren(@raw, preventRecursion)
245
+ when 0 then
246
+ #plaintext, do nothing but copy text into finished text
247
+ @finishedText = @raw
248
+ when 1 then
249
+ #tag - Parse to find any actions, and figure out what the symbol is
250
+ @preactions = []
251
+ @postactions = []
252
+ parsed = parseTag(@raw)
253
+ @symbol = parsed[:symbol]
254
+ @modifiers = parsed[:modifiers]
255
+
256
+ # Create all the preactions from the raw syntax
257
+ @preactions = parsed[:preactions].map{|preaction|
258
+ NodeAction.new(self, preaction[:raw])
259
+ }
260
+
261
+ # @postactions = parsed[:preactions].map{|postaction|
262
+ # NodeAction.new(self, postaction.raw)
263
+ # }
264
+
265
+ # Make undo actions for all preactions (pops for each push)
266
+ @postactions = @preactions.
267
+ select{|preaction| preaction.type == 0 }.
268
+ map{|preaction| preaction.createUndo() }
269
+
270
+ @preactions.each { |preaction| preaction.activate }
271
+
272
+ @finishedText = @raw
273
+
274
+ # Expand (passing the node, this allows tracking of recursion depth)
275
+ selectedRule = @grammar.selectRule(@symbol, self, @errors)
276
+
277
+ expandChildren(selectedRule, preventRecursion)
278
+
279
+ # Apply modifiers
280
+ # TODO: Update parse function to not trigger on hashtags within parenthesis within tags,
281
+ # so that modifier parameters can contain tags "#story.replace(#protagonist#, #newCharacter#)#"
282
+ @modifiers.each{|modName|
283
+ modParams = [];
284
+ if (modName.include?("(")) then
285
+ #match something like `modifier(param, param)`, capture name and params separately
286
+ match = /([^\(]+)\(([^)]+)\)/.match(modName)
287
+ if(!match.nil?) then
288
+ modParams = match.captures[1].split(",")
289
+ modName = match.captures[0]
290
+ end
291
+ end
292
+
293
+ mod = @grammar.modifiers[modName]
294
+
295
+ # Missing modifier?
296
+ if(mod.nil?)
297
+ @errors << "Missing modifier #{modName}"
298
+ @finishedText += "((.#{modName}))"
299
+ else
300
+ @finishedText = mod.call(@finishedText, modParams)
301
+ end
302
+ }
303
+ # perform post-actions
304
+ @postactions.each{|postaction| postaction.activate()}
305
+ when 2 then
306
+ # Just a bare action? Expand it!
307
+ @action = NodeAction.new(self, @raw)
308
+ @action.activate()
309
+
310
+ # No visible text for an action
311
+ # TODO: some visible text for if there is a failure to perform the action?
312
+ @finishedText = ""
313
+ end
314
+ end
315
+
316
+ def allErrors
317
+ child_errors = @children.inject([]){|all, child| all.concat(child.allErrors)}
318
+ return child_errors.concat(@errors)
319
+ end
320
+
321
+ def clearEscapeCharacters
322
+ @finishedText = @finishedText.gsub(/\\\\/, "DOUBLEBACKSLASH").gsub(/\\/, "").gsub(/DOUBLEBACKSLASH/, "\\")
323
+ end
324
+ end
325
+
326
+ # Types of actions:
327
+ # 0 Push: [key:rule]
328
+ # 1 Pop: [key:POP]
329
+ # 2 function: [functionName(param0,param1)] (TODO!)
330
+ class NodeAction
331
+ attr_accessor :node, :target, :type, :ruleNode
332
+ def initialize(node, raw)
333
+ # puts("No node for NodeAction") if(node.nil?)
334
+ # puts("No raw commands for NodeAction") if(raw.empty?)
335
+
336
+ @node = node
337
+
338
+ sections = raw.split(":")
339
+ @target = sections.first
340
+ if(sections.size == 1) then
341
+ # No colon? A function!
342
+ @type = 2
343
+ else
344
+ # Colon? It's either a push or a pop
345
+ @rule = sections[1] || ""
346
+ if(@rule == "POP")
347
+ @type = 1;
348
+ else
349
+ @type = 0;
350
+ end
351
+ end
352
+ end
353
+
354
+ def activate
355
+ grammar = @node.grammar
356
+ case(@type)
357
+ when 0 then
358
+ # split into sections (the way to denote an array of rules)
359
+ ruleSections = @rule.split(",")
360
+ finishedRules = ruleSections.map{|ruleSection|
361
+ n = TraceryNode.new(grammar, 0, {
362
+ type: -1,
363
+ raw: ruleSection
364
+ })
365
+ n.expand()
366
+ n.finishedText
367
+ }
368
+
369
+ # TODO: escape commas properly
370
+ grammar.pushRules(@target, finishedRules, self)
371
+ # puts("Push rules: #{@target} #{@ruleText}")
372
+ when 1 then
373
+ grammar.popRules(@target);
374
+ when 2 then
375
+ grammar.flatten(@target, true);
376
+ end
377
+ end
378
+
379
+ def createUndo
380
+ if(@type == 0) then
381
+ return NodeAction.new(@node, "#{@target}:POP")
382
+ end
383
+ # TODO Not sure how to make Undo actions for functions or POPs
384
+ return nil
385
+ end
386
+
387
+ def toText
388
+ case(@type)
389
+ when 0 then
390
+ return "#{@target}:#{@rule}"
391
+ when 1 then
392
+ return "#{@target}:POP"
393
+ when 2 then
394
+ return "((some function))"
395
+ else
396
+ return "((Unknown Action))"
397
+ end
398
+ end
399
+ end
400
+
401
+ # Sets of rules
402
+ # (Can also contain conditional or fallback sets of rulesets)
403
+ class RuleSet
404
+ def initialize(grammar, raw)
405
+ @raw = raw
406
+ @grammar = grammar
407
+ @falloff = 1
408
+ @random = Random.new
409
+
410
+ @defaultUses = {}
411
+
412
+ if(raw.is_a? Array) then
413
+ @defaultRules = raw
414
+ else
415
+ if(raw.is_a? String) then
416
+ @defaultRules = [raw];
417
+ else
418
+ # TODO: support for conditional and hierarchical rule sets
419
+ end
420
+ end
421
+ end
422
+
423
+ def selectRule
424
+ # puts "Get rule #{@raw}"
425
+
426
+ #TODO_ : RuleSet.getRule @ conditionalRule
427
+ #TODO_ : RuleSet.getRule @ ranking
428
+
429
+ if(!@defaultRules.nil?) then
430
+ index = 0
431
+ # Select from this basic array of rules
432
+ # Get the distribution from the grammar if there is no other
433
+ distribution = @distribution || @grammar.distribution
434
+ case(distribution)
435
+ when "shuffle" then
436
+ #create a shuffled deck
437
+ if(@shuffledDeck.nil? || @shuffledDeck.empty?)
438
+ #TODO_ - use fyshuffle and falloff
439
+ @shuffledDeck = (0...@defaultRules.size).to_a.shuffle
440
+ end
441
+ index = @shuffledDeck.pop
442
+ when "weighted" then
443
+ @errors << "Weighted distribution not yet implemented"
444
+ when "falloff" then
445
+ @errors << "Falloff distribution not yet implemented"
446
+ else
447
+ index = ((@random.rand ** @falloff) * @defaultRules.size).floor
448
+ end
449
+
450
+ @defaultUses[index] = (@defaultUses[index] || 0) + 1
451
+ return @defaultRules[index]
452
+ end
453
+
454
+ @errors << "No default rules defined for #{self}"
455
+ return nil
456
+ end
457
+
458
+ def clearState
459
+ @defaultUses = {}
460
+ #TODO_ should clear shuffled deck too?
461
+ end
462
+ end
463
+
464
+ class TracerySymbol
465
+ attr_accessor :isDynamic
466
+
467
+ def initialize(grammar, key, rawRules)
468
+ # Symbols can be made with a single value, and array, or array of objects of (conditions/values)
469
+ @key = key
470
+ @grammar = grammar
471
+ @rawRules = rawRules
472
+
473
+ @baseRules = RuleSet.new(@grammar, @rawRules)
474
+ clearState
475
+ end
476
+
477
+ def clearState
478
+ # Clear the stack and clear all ruleset usages
479
+ @stack = [@baseRules]
480
+ @uses = []
481
+ @baseRules.clearState
482
+ end
483
+
484
+ def pushRules(rawRules)
485
+ rules = RuleSet.new(@grammar, rawRules)
486
+ @stack.push rules
487
+ end
488
+
489
+ def popRules
490
+ @stack.pop
491
+ end
492
+
493
+ def selectRule(node, errors)
494
+ @uses.push({ node: node })
495
+ if(@stack.empty?) then
496
+ errors << "The rule stack for '#{@key}' is empty, too many pops?"
497
+ return "((#{@key}))"
498
+ end
499
+ return @stack.last.selectRule
500
+ end
501
+
502
+ def getActiveRules
503
+ return nil if @stack.empty?
504
+ return @stack.last.selectRule
505
+ end
506
+
507
+ def rulesToJSON
508
+ return @rawRules.to_json
509
+ end
510
+ end
511
+
512
+ class Grammar
513
+ attr_accessor :distribution, :modifiers
514
+
515
+ def initialize(raw) #, settings
516
+ @modifiers = {}
517
+ loadFromRawObj(raw)
518
+ end
519
+
520
+ def clearState
521
+ @symbols.each{|k,v| v.clearState} # TODO_ check for nil keys
522
+ end
523
+
524
+ def addModifiers(mods)
525
+ # copy over the base modifiers
526
+ mods.each{|k,v| @modifiers[k] = v}
527
+ end
528
+
529
+ def loadFromRawObj(raw)
530
+ @raw = raw
531
+ @symbols = {}
532
+ @subgrammars = []
533
+ return if(@raw.nil?)
534
+ @raw.each{|k,v|
535
+ @symbols[k] = TracerySymbol.new(self, k, v)
536
+ }
537
+ end
538
+
539
+ def createRoot(rule)
540
+ # Create a node and subnodes
541
+ root = TraceryNode.new(self, 0, {
542
+ type: -1,
543
+ raw: rule
544
+ })
545
+ return root
546
+ end
547
+
548
+ def expand(rule, allowEscapeChars = false)
549
+ root = createRoot(rule)
550
+ root.expand
551
+ root.clearEscapeCharacters if(!allowEscapeChars)
552
+ return root
553
+ end
554
+
555
+ def flatten(rule, allowEscapeChars = false)
556
+ return expand(rule, allowEscapeChars).finishedText
557
+ end
558
+
559
+ def pushRules(key, rawRules, sourceAction)
560
+ # Create or push rules
561
+ if(@symbols[key].nil?) then
562
+ @symbols[key] = TracerySymbol.new(self, key, rawRules)
563
+ @symbols[key].isDynamic = true if(sourceAction)
564
+ else
565
+ @symbols[key].pushRules(rawRules)
566
+ end
567
+ end
568
+
569
+ def popRules(key)
570
+ errors << "No symbol for key #{key}" if(@symbols[key].nil?)
571
+ @symbols[key].popRules
572
+ end
573
+
574
+ def selectRule(key, node, errors)
575
+ if(@symbols.has_key? key) then
576
+ return @symbols[key].selectRule(node, errors)
577
+ end
578
+
579
+ # Failover to alternative subgrammars
580
+ @subgrammars.each do |subgrammar|
581
+ if(subgrammar.symbols.has_key? key) then
582
+ return subgrammar.symbols[key].selectRule
583
+ end
584
+ end
585
+
586
+ # No symbol?
587
+ errors << "No symbol for '#{key}'"
588
+ return "((#{key}))"
589
+ end
590
+
591
+ def toJSON
592
+ symbols = @symbols.each.collect{|symkey, symval| "\"#{symkey}\": #{symval.rulesToJSON}"}
593
+ return "{\n#{symbols.join("\n")}\n}"
594
+ end
595
+ end
596
+
597
+ class TraceryTests
598
+ include Tracery
599
+ require 'pp'
600
+
601
+ def test
602
+ tests = {
603
+ basic: ["", "a", "tracery"],
604
+ hashtag: ["#a#", "a#b#", "aaa#b##cccc#dd#eee##f#"],
605
+ hashtagWrong: ["##", "#", "a#a", "#aa#aa###"],
606
+ escape: ["\\#test\\#", "\\[#test#\\]"],
607
+ }
608
+
609
+ tests.each do |key, testSet|
610
+ puts "For #{key}:"
611
+ testSet.each do |t|
612
+ result = parse(t)
613
+ puts "\tTesting \"#{t}\": #{result}"
614
+ end
615
+ end
616
+
617
+ testGrammar = createGrammar({
618
+ "animal" => ["capybara", "unicorn", "university", "umbrella", "u-boat", "boa", "ocelot", "zebu", "finch", "fox", "hare", "fly"],
619
+ "color" => ["yellow", "maroon", "indigo", "ivory", "obsidian"],
620
+ "mood" => ["elated", "irritable", "morose", "enthusiastic"],
621
+ "story" => ["[mc:#animal#]Once there was #mc.a#, a very #mood# #mc#. In a pack of #color.ed# #mc.s#!"]
622
+ });
623
+
624
+ require "./mods-eng-basic"
625
+ testGrammar.addModifiers(Modifiers.baseEngModifiers);
626
+ puts testGrammar.flatten("#story#")
627
+
628
+ grammar = createGrammar({"origin" => "foo"});
629
+ grammar.addModifiers(Modifiers.baseEngModifiers);
630
+ puts grammar.flatten("#origin#")
631
+ end
632
+ end
633
+
634
+ if($PROGRAM_NAME == __FILE__) then
635
+ tests = TraceryTests.new
636
+ tests.test
637
+ end
@@ -0,0 +1,155 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib/')
2
+
3
+ require 'test/unit'
4
+ require 'tracery'
5
+ require "mods-eng-basic"
6
+
7
+ class TraceryTest < Test::Unit::TestCase
8
+ include Tracery
9
+ def setup
10
+ @grammar = createGrammar({
11
+ "deepHash" => ["\\#00FF00", "\\#FF00FF"],
12
+ "deeperHash" => ["#deepHash#"],
13
+ "animal" => ["bear", "cat", "dog", "fox", "giraffe", "hippopotamus"],
14
+ "mood" => ["quiet", "morose", "gleeful", "happy", "bemused", "clever", "jovial", "vexatious", "curious", "anxious", "obtuse", "serene", "demure"],
15
+
16
+ "nonrecursiveStory" => ["The #pet# went to the beach."],
17
+ # story : ["#recursiveStory#", "#recursiveStory#", "#nonrecursiveStory#"],
18
+ "recursiveStory" => ["The #pet# opened a book about[pet:#mood# #animal#] #pet.a#. #[#setPronouns#]story#[pet:POP] The #pet# closed the book."],
19
+
20
+ "svgColor" => ["rgb(120,180,120)", "rgb(240,140,40)", "rgb(150,45,55)", "rgb(150,145,125)", "rgb(220,215,195)", "rgb(120,250,190)"],
21
+ "svgStyle" => ['style="fill:#svgColor#;stroke-width:3;stroke:#svgColor#"'],
22
+
23
+ "name" => ["Cheri", "Fox", "Morgana", "Jedoo", "Brick", "Shadow", "Krox", "Urga", "Zelph"],
24
+ "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."],
25
+ "monster" => ["dragon", "ogre", "witch", "wizard", "goblin", "golem", "giant", "sphinx", "warlord"],
26
+ "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]"],
27
+ "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#]"],
28
+ "origin" => ["#[#setPronouns#][#setOccupation#][hero:#name#]story#"]
29
+ })
30
+ @grammar.addModifiers(Modifiers.baseEngModifiers)
31
+ end
32
+
33
+ def test_main_expansion_tests
34
+ tests = {
35
+ plaintextShort: {
36
+ src: "a"
37
+ },
38
+ plaintextLong: {
39
+ src: "Emma Woodhouse, handsome, clever, and rich, with a comfortable home and happy disposition, seemed to unite some of the best blessings of existence; and had lived nearly twenty-one years in the world with very little to distress or vex her."
40
+ },
41
+
42
+ # Escape chars
43
+ escapeCharacter: {
44
+ src: "\\#escape hash\\# and escape slash\\\\"
45
+ },
46
+
47
+ escapeDeep: {
48
+ src: "#deepHash# [myColor:#deeperHash#] #myColor#"
49
+ },
50
+
51
+ escapeQuotes: {
52
+ src: "\"test\" and \'test\'"
53
+ },
54
+ escapeBrackets: {
55
+ src: "\\[\\]"
56
+ },
57
+ escapeHash: {
58
+ src: "\\#"
59
+ },
60
+ unescapeCharSlash: {
61
+ src: "\\\\"
62
+ },
63
+ escapeMelange1: {
64
+ src: "An action can have inner tags: \[key:\#rule\#\]"
65
+ },
66
+ escapeMelange2: {
67
+ src: "A tag can have inner actions: \"\\#\\[myName:\\#name\\#\\]story\\[myName:POP\\]\\#\""
68
+ },
69
+ emoji: {
70
+ src: "💻🐋🌙🏄🍻"
71
+ },
72
+
73
+ unicode: {
74
+ src: "&\\#x2665; &\\#x2614; &\\#9749; &\\#x2665;"
75
+ },
76
+
77
+ svg: {
78
+ src: '<svg width="100" height="70"><rect x="0" y="0" width="100" height="100" #svgStyle#/> <rect x="20" y="10" width="40" height="30" #svgStyle#/></svg>'
79
+ },
80
+
81
+ pushBasic: {
82
+ src: "[pet:#animal#]You have a #pet#. Your #pet# is #mood#."
83
+ },
84
+
85
+ pushPop: {
86
+ src: "[pet:#animal#]You have a #pet#. [pet:#animal#]Pet:#pet# [pet:POP]Pet:#pet#"
87
+ },
88
+
89
+ tagAction: {
90
+ src: "#[pet:#animal#]nonrecursiveStory# post:#pet#"
91
+ },
92
+
93
+ testComplexGrammar: {
94
+ src: "#origin#"
95
+ },
96
+
97
+ modifierWithParams: {
98
+ src: "[pet:#animal#]#nonrecursiveStory# -> #nonrecursiveStory.replace(beach,mall)#"
99
+ },
100
+
101
+ recursivePush: {
102
+ src: "[pet:#animal#]#recursiveStory#"
103
+ },
104
+
105
+ missingModifier: {
106
+ src: "#animal.foo#",
107
+ error: true
108
+ },
109
+
110
+ unmatchedHash: {
111
+ src: "#unmatched",
112
+ error: true
113
+ },
114
+ missingSymbol: {
115
+ src: "#unicorns#",
116
+ error: true
117
+ },
118
+ missingRightBracket: {
119
+ src: "[pet:unicorn",
120
+ error: true
121
+ },
122
+ missingLeftBracket: {
123
+ src: "pet:unicorn]",
124
+ error: true
125
+ },
126
+ justALotOfBrackets: {
127
+ src: "[][]][][][[[]]][[]]]]",
128
+ error: true
129
+ },
130
+ bracketTagMelange: {
131
+ src: "[][#]][][##][[[##]]][#[]]]]",
132
+ error: true
133
+ }
134
+ }
135
+
136
+ puts
137
+ tests.each { |k,v|
138
+ puts "#{k}: "
139
+ @grammar.clearState
140
+ source = v[:src]
141
+ is_error = v[:error].nil? ? false : v[:error]
142
+ root = @grammar.expand(source)
143
+ all_errors = root.allErrors
144
+ puts "\t#{root.finishedText}"
145
+ if(is_error) then
146
+ puts "\tErrors: #{all_errors}"
147
+ refute(all_errors.empty?, "Expected errors.")
148
+ else
149
+ assert(all_errors.empty?, "Expected no errors.")
150
+ refute(root.finishedText.empty?, "Expected non-empty output.")
151
+ end
152
+ }
153
+ end
154
+
155
+ end
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tracery
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.7.0
5
+ platform: ruby
6
+ authors:
7
+ - Kate Compton
8
+ - Eli Brody
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-02-27 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: |
15
+ Tracery is a library for text generation.
16
+ The text is expanded by traversing a grammar.
17
+ See the main github repo for examples and documentation.
18
+ email: brodyeli@gmail.com
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - lib/mods-eng-basic.rb
24
+ - lib/tracery.rb
25
+ - test/tracery_test.rb
26
+ homepage: https://github.com/galaxykate/tracery
27
+ licenses:
28
+ - Apache-2.0
29
+ metadata: {}
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubyforge_project:
46
+ rubygems_version: 2.0.14
47
+ signing_key:
48
+ specification_version: 4
49
+ summary: A text expansion library
50
+ test_files:
51
+ - test/tracery_test.rb