tracery 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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