poefy 1.1.0 → 2.0.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.
@@ -1,529 +1,529 @@
1
- #!/usr/bin/env ruby
2
- # Encoding: UTF-8
3
-
4
- ################################################################################
5
- # Description of various poetic forms.
6
- # Also holds methods for parsing the form strings.
7
- #
8
- # All of this is better explained in the README.
9
- #
10
- ### Rhyme strings:
11
- # This is the most important argument.
12
- # All other form strings are based on this.
13
- # Each token represents a line.
14
- # (Token examples: 'a', 'b', 'A1', ' ')
15
- # Letters indicate rhymes, so all 'a' or 'A' lines have the same rhyme.
16
- # (Example, limerick: 'aabba')
17
- # Uppercase letter lines will be duplicated exactly.
18
- # This is used to create refrain lines.
19
- # (Example, rondeau: 'aabba aabR aabbaR')
20
- # Numbers after a capital letter indicate which specific line to repeat.
21
- # Letters indicate the same rhyme, uppercase or down.
22
- # (Example, villanelle: 'A1bA2 abA1 abA2 abA1 abA2 abA1A2'
23
- #
24
- ### Indent strings:
25
- # Each character represents a line.
26
- # The numbers show how many times to repeat ' ' before each line.
27
- # Any character that doesn't map to an integer defaults to 0.
28
- # So '0011000101' and '0011 001 1' are the same.
29
- #
30
- ### Syllable strings:
31
- # '10'
32
- # '9,10,11'
33
- # '[8,8,5,5,8]'
34
- # '[[8,9],[8,9],[4,5,6],[4,5,6],[8,9]]'
35
- # '{1:8,2:8,3:5,4:5,5:8}'
36
- # '{1:[8,9],2:[8,9],3:[4,5,6],4:[4,5,6],5:[8,9]}'
37
- # '{0:[8,9],3:[4,5,6],4:[4,5,6]}'
38
- # '{1:8,5:8}'
39
- # '{1:8,2:8,3:5,-2:5,-1:8}'
40
- #
41
- ### Regex strings:
42
- # '^[A-Z].*$'
43
- # '^[^e]*$'
44
- # '{1=>/^[A-Z].*$/}'
45
- #
46
- ################################################################################
47
-
48
- require 'yaml'
49
-
50
- ################################################################################
51
-
52
- module Poefy
53
-
54
- module PoeticForms
55
-
56
- # If the token is an array, then a random sample will be used.
57
- POETIC_FORMS = {
58
- default: {
59
- rhyme: 'a',
60
- indent: '0',
61
- syllable: ''
62
- },
63
- rondeau: {
64
- rhyme: 'aabba aabR aabbaR',
65
- indent: '',
66
- syllable: ''
67
- },
68
- villanelle: {
69
- rhyme: 'A1bA2 abA1 abA2 abA1 abA2 abA1A2',
70
- indent: '010 001 001 001 001 0011',
71
- syllable: ''
72
- },
73
- ballade: {
74
- rhyme: 'ababbcbC ababbcbC ababbcbC bcbC',
75
- indent: '',
76
- syllable: ''
77
- },
78
- ballata: {
79
- rhyme: ['AbbaA','AbbaAbbaA','AbbaAbbaAbbaA'],
80
- indent: '',
81
- syllable: ''
82
- },
83
- sonnet: {
84
- rhyme: 'ababcdcdefefgg',
85
- indent: '',
86
- syllable: ''
87
- },
88
- petrarchan: {
89
- rhyme: ['abbaabbacdecde','abbaabbacdccdc','abbaabbacdcddc',
90
- 'abbaabbacddcdd','abbaabbacddece','abbaabbacdcdcd'],
91
- indent: ['01100110010010','10001000100100'],
92
- syllable: ''
93
- },
94
- limerick: {
95
- rhyme: 'aabba',
96
- indent: '',
97
- syllable: '{1:[8],2:[8],3:[4,5],4:[4,5],5:[8]}'
98
- },
99
- haiku: {
100
- rhyme: 'abc',
101
- indent: '',
102
- syllable: '[5,7,5]'
103
- },
104
- common: {
105
- rhyme: 'abcb',
106
- indent: '0101',
107
- syllable: '{o:8,e:6}'
108
- },
109
- ballad: {
110
- rhyme: 'abab',
111
- indent: '0101',
112
- syllable: '{o:8,e:6}'
113
- },
114
- double_dactyl: {
115
- rhyme: 'abcd efgd',
116
- indent: '',
117
- syllable: '{0:6, 4m0:4}',
118
- regex: '{7: ^\S+$}'
119
- }
120
- }
121
-
122
- # Create a regex specification for acrostics.
123
- # acrostic('unintelligible')
124
- # acrostic('unin tell igib le')
125
- def acrostic word
126
- output = {}
127
- word.split('').each.with_index do |char, i|
128
- output[i + 1] = /^[#{char.downcase}]/i if char != ' '
129
- end
130
- output
131
- end
132
-
133
- # Create a regex specification for acrostics.
134
- # Uses special logic for 'X'.
135
- # Match words starting 'ex' and then change case to 'eX'.
136
- def acrostic_x word
137
- regex = {}
138
- transform = {}
139
- word.split('').each.with_index do |char, i|
140
- if char.downcase == 'x'
141
- regex[i + 1] = /^ex/i
142
- transform[i + 1] = proc do |line|
143
- line[0..1] = 'eX'
144
- ' ' + line
145
- end
146
- elsif char != ' '
147
- regex[i + 1] = /^[#{char.downcase}]/i
148
- transform[i + 1] = proc do |line|
149
- ' ' + line
150
- end
151
- end
152
- end
153
- { regex: regex, transform: transform }
154
- end
155
-
156
- private
157
-
158
- # Can the string be converted to integer?
159
- def is_int? str
160
- !(Integer(str) rescue nil).nil?
161
- end
162
-
163
- # Make sure the form name is in the list.
164
- def get_valid_form form_name
165
- return nil if form_name.nil?
166
- POETIC_FORMS[form_name.to_sym] ? form_name.to_sym : nil
167
- end
168
-
169
- # Get full form, from either the user-specified options,
170
- # or the default poetic form.
171
- def poetic_form_full poetic_form = @poetic_form
172
- rhyme = get_poetic_form_token :rhyme, poetic_form
173
- indent = get_poetic_form_token :indent, poetic_form
174
- syllable = get_poetic_form_token :syllable, poetic_form
175
- regex = get_poetic_form_token :regex, poetic_form
176
- transform = get_poetic_form_token :transform, poetic_form
177
- poetic_form[:rhyme] = rhyme
178
- poetic_form[:indent] = indent if indent != ''
179
- poetic_form[:syllable] = syllable if syllable != ''
180
- poetic_form[:regex] = regex if regex
181
- poetic_form[:transform] = transform if transform != ' '
182
- poetic_form
183
- end
184
-
185
- # If the token is specified in the hash, return it,
186
- # else get the token for the named form.
187
- def get_poetic_form_rhyme_longest poetic_form = @poetic_form
188
- get_poetic_form_token :rhyme, poetic_form, true
189
- end
190
- def get_poetic_form_rhyme poetic_form = @poetic_form
191
- get_poetic_form_token :rhyme, poetic_form
192
- end
193
- def get_poetic_form_indent poetic_form = @poetic_form
194
- get_poetic_form_token :indent, poetic_form
195
- end
196
- def get_poetic_form_token token,
197
- poetic_form = @poetic_form,
198
- longest = false
199
- if poetic_form.empty?
200
- ' '
201
- elsif poetic_form[token]
202
- poetic_form[token]
203
- elsif poetic_form[:form].nil?
204
- ' '
205
- elsif POETIC_FORMS[poetic_form[:form].to_sym].nil?
206
- ' '
207
- else
208
- token = POETIC_FORMS[poetic_form[:form].to_sym][token]
209
- if token.is_a?(Array)
210
- token = longest ? token.max_by(&:length) : token.sample
211
- end
212
- token
213
- end
214
- end
215
-
216
- # Turn a rhyme format string into a usable array of tokens.
217
- # Example formats:
218
- # sonnet_form = 'abab cdcd efef gg'
219
- # villanelle_form = 'A1bA2 abA1 abA2 abA1 abA2 abA1A2'
220
- def tokenise_rhyme rhyme_string
221
- return rhyme_string if rhyme_string.is_a? Array
222
-
223
- tokens = []
224
- buffer = ''
225
- rhyme_string.split('').each do |char|
226
- if !numeric?(char) and buffer != ''
227
- tokens << buffer
228
- buffer = ''
229
- end
230
- buffer += char
231
- end
232
- tokens << buffer
233
-
234
- # Handle invalid tokens.
235
- # ["a1"] ["1"] ["1122"] [" 1"] [" 11"] [":1"]
236
- boolean_array = tokens.map do |i|
237
- keep = i.gsub(/[^A-Z,0-9]/,'')
238
- (keep == '' or !is_int?(keep))
239
- end
240
- valid = boolean_array.reduce{ |sum, i| sum && i }
241
- raise Poefy::RhymeError unless valid
242
- tokens = [' '] if tokens == ['']
243
-
244
- # Output as a hash.
245
- tokens.map do |i|
246
- hash = {
247
- token: i,
248
- rhyme_letter: i[0].downcase
249
- }
250
- hash[:refrain] = i if i[0] == i[0].upcase
251
- hash
252
- end
253
- end
254
-
255
- # Indent an array of lines using a string of numbers.
256
- def do_indent lines, str
257
- return lines if str.nil? or lines.nil? or lines.empty?
258
-
259
- # Convert the indent string into an array.
260
- indent_arr = (str + '0' * lines.length).split('')
261
- indent_arr = indent_arr.each_slice(lines.length).to_a[0]
262
-
263
- # Convert to integers. Spaces should be zero.
264
- indent_arr.map! { |i| Integer(i) rescue 0 }
265
-
266
- # Zip, iterate, and prepend indent.
267
- indent_arr.zip(lines).map do |line|
268
- ' ' * line[0] + (line[1] ? line[1] : '')
269
- end
270
- end
271
-
272
- # Sort by keys, to make it more human-readable.
273
- def sort_hash input
274
- output = {}
275
- input.keys.sort.each do |k|
276
- output[k] = input[k]
277
- end
278
- output
279
- end
280
-
281
- # Convert a range in the string form "1-6" to an array.
282
- # Assumes elements are integers.
283
- def range_to_array input
284
- return input if input.is_a?(Numeric) || !input.include?('-')
285
- vals = input.split('-').map(&:to_i).sort
286
- (vals.first..vals.last).to_a
287
- end
288
-
289
- # Convert an array in the string form "4,6,8-10,12" to an array.
290
- # Assumes elements are positive integers.
291
- def string_to_array input
292
- return [input.to_i.abs] if input.is_a?(Numeric)
293
- arr = input.is_a?(Array) ? input : input.split(',')
294
-
295
- # Convert to positive integers, and remove duplicates.
296
- output = arr.map do |i|
297
- range_to_array(i)
298
- end.flatten.map do |i|
299
- i.to_i.abs
300
- end.sort.uniq.select do |i|
301
- i != 0
302
- end
303
-
304
- # This cannot be an empty array []. It will fail anyway when we
305
- # come to do the poem generation, but it's better to fail now.
306
- raise Poefy::SyllableError.new if output.empty?
307
- output
308
- end
309
-
310
- # '10'
311
- # '9,10,11'
312
- # '[8,8,5,5,8]'
313
- # '[[8,9],[8,9],[4,5,6],[4,5,6],[8,9]]'
314
- # '{1:8,2:8,3:5,4:5,5:8}'
315
- # '{1:[8,9],2:[8,9],3:[4,5,6],4:[4,5,6],5:[8,9]}'
316
- # '{0:[8,9],3:[4,5,6],4:[4,5,6]}'
317
- # '{1:8,5:8}'
318
- # '{1:8,2:8,3:5,-2:5,-1:8}'
319
- # Use the rhyme string as base for the number of lines in total.
320
- def transform_input_syllable input, rhyme
321
- tokens = tokenise_rhyme rhyme
322
- hash = transform_input_to_hash :syllable, input
323
- hash = validate_hash_values :syllable, hash
324
- hash = expand_hash_keys :syllable, hash, tokens, 0
325
- end
326
-
327
- # Do the same for regular expression strings.
328
- def transform_input_regex input, rhyme
329
- tokens = tokenise_rhyme rhyme
330
- hash = transform_input_to_hash :regex, input
331
- hash = validate_hash_values :regex, hash
332
- hash = expand_hash_keys :regex, hash, tokens, //
333
- end
334
-
335
- # This should work for both syllable and regex strings.
336
- # It should also be fine for Integer and Regexp 'input' values.
337
- def transform_input_to_hash type, input
338
- return input if input.is_a? Hash
339
-
340
- # Don't go any further if we've got an invalid type.
341
- valid_non_string =
342
- input.is_a?(Array) ||
343
- (type == :syllable and input.is_a?(Numeric)) ||
344
- (type == :regex and input.is_a?(Regexp))
345
- valid_string_like = !valid_non_string && input.respond_to?(:to_s)
346
- raise TypeError unless valid_non_string || valid_string_like
347
-
348
- # Perform different tasks depending on type.
349
- input.strip! if input.is_a? String
350
- input = input.to_i if input.is_a? Numeric
351
- input = input.to_s if valid_string_like
352
- return {} if input == ''
353
-
354
- # This will be built up over the course of the method.
355
- output = {}
356
-
357
- # Figure out datatype.
358
- # Regex string input cannot be an array, but syllable can.
359
- datatype = :string
360
- if !input.is_a?(Regexp)
361
- if input.is_a?(Array)
362
- datatype = :array
363
- elsif type == :syllable and input[0] == '[' and input[-1] == ']'
364
- datatype = :array
365
- elsif input[0] == '{' and input[-1] == '}'
366
- datatype = :hash
367
- end
368
- end
369
-
370
- # If it's a basic string format, convert it to hash.
371
- if datatype == :string
372
-
373
- # Regex cannot be an array or range, but syllable can.
374
- if type == :regex
375
- arr = (input == []) ? [] : [Regexp.new(input)]
376
-
377
- # Special case for if a user explicitly states only '0'.
378
- elsif type == :syllable
379
- arr = input == '0' ? [0] : string_to_array(input)
380
- end
381
-
382
- # Set this to be the default '0' hash value.
383
- arr = arr.first if arr.count == 1
384
- output = { 0 => arr }
385
- datatype = :hash
386
-
387
- # If it's wrapped in [] or {}, then evaluate it using YAML.
388
- else
389
-
390
- # Don't need to evaluate if it's already an Array.
391
- if input.is_a?(Array)
392
- output = input
393
- else
394
- begin
395
- # If it's a regex, mandate the ': ' key separator.
396
- # (This is so the string substitutions don't mess up the regex.)
397
- # If it's a syllable, we can be more flexible with gsubs.
398
- as_yaml = input
399
- if type == :syllable
400
- as_yaml = input.gsub(':', ': ').gsub('=>', ': ')
401
- end
402
- output = YAML.load(as_yaml)
403
- rescue
404
- # Raise a SyllableError or RegexError.
405
- msg = "#{type.capitalize} hash is not valid YAML"
406
- e = Object.const_get("Poefy::#{type.capitalize}Error")
407
- raise e.new(msg)
408
- end
409
- end
410
- end
411
-
412
- # Convert array to positioned hash.
413
- if datatype == :array
414
- output = output.map.with_index do |e, i|
415
- [i+1, e]
416
- end.to_h
417
- end
418
-
419
- output
420
- end
421
-
422
- # Run different methods on each value depending on the type.
423
- # If it's a syllable, convert all values to int arrays.
424
- # If it's a regex, convert all values to regexp.
425
- def validate_hash_values type, input
426
- format_value = if type == :syllable
427
- Proc.new do |x|
428
- arr = string_to_array(x)
429
- arr.count == 1 ? arr.first : arr
430
- end
431
- elsif type == :regex
432
- Proc.new do |x|
433
- x.is_a?(Regexp) ? x : Regexp.new(x.to_s)
434
- end
435
- end
436
-
437
- # Validate values.
438
- if input.is_a?(Hash)
439
- input.each do |k, v|
440
- begin
441
- input[k] = format_value.call(v)
442
- rescue
443
- # Raise a SyllableError or RegexError.
444
- msg = "#{type.capitalize} hash invalid, key='#{k}' value='#{v}'"
445
- e = Object.const_get("Poefy::#{type.capitalize}Error")
446
- raise e.new(msg)
447
- end
448
- end
449
- elsif input.is_a?(Array)
450
- input.map! do |i|
451
- i = format_value.call(i)
452
- end
453
- end
454
- input
455
- end
456
-
457
- # Convert non-positive-integer keys into the correct position.
458
- def expand_hash_keys type, input, tokens, default
459
- output = input.dup
460
- line_count = tokens.length
461
-
462
- # Handle negative keys.
463
- output.keys.each do |k|
464
- if k.is_a?(Numeric) and k < 0
465
- line = line_count + 1 + k
466
- output[line] = output[k]
467
- end
468
- end
469
-
470
- # Find all lines that are not empty.
471
- content_lines = tokens.map.with_index do |v, i|
472
- i + 1 if (v[:token].strip != '')
473
- end.compact
474
-
475
- # Handle modulo lines.
476
- # Handle 'e' even and 'o' odd lines.
477
- modulo_lines = {}
478
- output.keys.each do |k|
479
- is_modulo = k.respond_to?(:include?) && k.include?('m')
480
- is_even_odd = %w[e o].include?(k)
481
- if is_modulo or is_even_odd
482
- if is_modulo
483
- vals = k.split('m').map(&:to_i)
484
- divider = vals.first.to_i.abs
485
- remainder = vals.last.to_i.abs
486
- if divider == 0
487
- # Raise a SyllableError or RegexError.
488
- msg = "#{type.capitalize} hash invalid,"
489
- msg += " key='#{k}', modulo='#{divider}m#{remainder}'"
490
- e = Object.const_get("Poefy::#{type.capitalize}Error")
491
- raise e.new(msg)
492
- end
493
- elsif is_even_odd
494
- divider = 2
495
- remainder = (k == 'e') ? 0 : 1
496
- end
497
- content_lines.modulo_index(divider, remainder, 1).each do |i|
498
- modulo_lines[i] = output[k]
499
- end
500
- end
501
- end
502
-
503
- # Take {modulo_lines} as the base and overwrite it with specified keys.
504
- if modulo_lines
505
- output.keys.each do |k|
506
- modulo_lines[k] = output[k]
507
- end
508
- output = modulo_lines
509
- end
510
-
511
- # Go through each line and make sure there is a value for each.
512
- # Use default if there is no specific value.
513
- default_value = output[0] ? output[0] : default
514
- (1..line_count).each do |i|
515
- output[i] = default_value if output[i].nil?
516
- end
517
-
518
- # Remove keys that are not numeric, or are less than or equal to zero.
519
- output.reject!{ |k| !k.is_a?(Numeric) or k <= 0 }
520
-
521
- # Return sorted hash.
522
- sort_hash output
523
- end
524
-
525
- end
526
-
527
- end
528
-
529
- ################################################################################
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ ################################################################################
5
+ # Description of various poetic forms.
6
+ # Also holds methods for parsing the form strings.
7
+ #
8
+ # All of this is better explained in the README.
9
+ #
10
+ ### Rhyme strings:
11
+ # This is the most important argument.
12
+ # All other form strings are based on this.
13
+ # Each token represents a line.
14
+ # (Token examples: 'a', 'b', 'A1', ' ')
15
+ # Letters indicate rhymes, so all 'a' or 'A' lines have the same rhyme.
16
+ # (Example, limerick: 'aabba')
17
+ # Uppercase letter lines will be duplicated exactly.
18
+ # This is used to create refrain lines.
19
+ # (Example, rondeau: 'aabba aabR aabbaR')
20
+ # Numbers after a capital letter indicate which specific line to repeat.
21
+ # Letters indicate the same rhyme, uppercase or down.
22
+ # (Example, villanelle: 'A1bA2 abA1 abA2 abA1 abA2 abA1A2'
23
+ #
24
+ ### Indent strings:
25
+ # Each character represents a line.
26
+ # The numbers show how many times to repeat ' ' before each line.
27
+ # Any character that doesn't map to an integer defaults to 0.
28
+ # So '0011000101' and '0011 001 1' are the same.
29
+ #
30
+ ### Syllable strings:
31
+ # '10'
32
+ # '9,10,11'
33
+ # '[8,8,5,5,8]'
34
+ # '[[8,9],[8,9],[4,5,6],[4,5,6],[8,9]]'
35
+ # '{1:8,2:8,3:5,4:5,5:8}'
36
+ # '{1:[8,9],2:[8,9],3:[4,5,6],4:[4,5,6],5:[8,9]}'
37
+ # '{0:[8,9],3:[4,5,6],4:[4,5,6]}'
38
+ # '{1:8,5:8}'
39
+ # '{1:8,2:8,3:5,-2:5,-1:8}'
40
+ #
41
+ ### Regex strings:
42
+ # '^[A-Z].*$'
43
+ # '^[^e]*$'
44
+ # '{1=>/^[A-Z].*$/}'
45
+ #
46
+ ################################################################################
47
+
48
+ require 'yaml'
49
+
50
+ ################################################################################
51
+
52
+ module Poefy
53
+
54
+ module PoeticForms
55
+
56
+ # If the token is an array, then a random sample will be used.
57
+ POETIC_FORMS = {
58
+ default: {
59
+ rhyme: 'a',
60
+ indent: '0',
61
+ syllable: ''
62
+ },
63
+ rondeau: {
64
+ rhyme: 'aabba aabR aabbaR',
65
+ indent: '',
66
+ syllable: ''
67
+ },
68
+ villanelle: {
69
+ rhyme: 'A1bA2 abA1 abA2 abA1 abA2 abA1A2',
70
+ indent: '010 001 001 001 001 0011',
71
+ syllable: ''
72
+ },
73
+ ballade: {
74
+ rhyme: 'ababbcbC ababbcbC ababbcbC bcbC',
75
+ indent: '',
76
+ syllable: ''
77
+ },
78
+ ballata: {
79
+ rhyme: ['AbbaA','AbbaAbbaA','AbbaAbbaAbbaA'],
80
+ indent: '',
81
+ syllable: ''
82
+ },
83
+ sonnet: {
84
+ rhyme: 'ababcdcdefefgg',
85
+ indent: '',
86
+ syllable: ''
87
+ },
88
+ petrarchan: {
89
+ rhyme: ['abbaabbacdecde','abbaabbacdccdc','abbaabbacdcddc',
90
+ 'abbaabbacddcdd','abbaabbacddece','abbaabbacdcdcd'],
91
+ indent: ['01100110010010','10001000100100'],
92
+ syllable: ''
93
+ },
94
+ limerick: {
95
+ rhyme: 'aabba',
96
+ indent: '',
97
+ syllable: '{1:[8],2:[8],3:[4,5],4:[4,5],5:[8]}'
98
+ },
99
+ haiku: {
100
+ rhyme: 'abc',
101
+ indent: '',
102
+ syllable: '[5,7,5]'
103
+ },
104
+ common: {
105
+ rhyme: 'abcb',
106
+ indent: '0101',
107
+ syllable: '{o:8,e:6}'
108
+ },
109
+ ballad: {
110
+ rhyme: 'abab',
111
+ indent: '0101',
112
+ syllable: '{o:8,e:6}'
113
+ },
114
+ double_dactyl: {
115
+ rhyme: 'abcd efgd',
116
+ indent: '',
117
+ syllable: '{0:6, 4m0:4}',
118
+ regex: '{7: ^\S+$}'
119
+ }
120
+ }
121
+
122
+ # Create a regex specification for acrostics.
123
+ # acrostic('unintelligible')
124
+ # acrostic('unin tell igib le')
125
+ def acrostic word
126
+ output = {}
127
+ word.split('').each.with_index do |char, i|
128
+ output[i + 1] = /^[#{char.downcase}]/i if char != ' '
129
+ end
130
+ output
131
+ end
132
+
133
+ # Create a regex specification for acrostics.
134
+ # Uses special logic for 'X'.
135
+ # Match words starting 'ex' and then change case to 'eX'.
136
+ def acrostic_x word
137
+ regex = {}
138
+ transform = {}
139
+ word.split('').each.with_index do |char, i|
140
+ if char.downcase == 'x'
141
+ regex[i + 1] = /^ex/i
142
+ transform[i + 1] = proc do |line|
143
+ line[0..1] = 'eX'
144
+ ' ' + line
145
+ end
146
+ elsif char != ' '
147
+ regex[i + 1] = /^[#{char.downcase}]/i
148
+ transform[i + 1] = proc do |line|
149
+ ' ' + line
150
+ end
151
+ end
152
+ end
153
+ { regex: regex, transform: transform }
154
+ end
155
+
156
+ private
157
+
158
+ # Can the string be converted to integer?
159
+ def is_int? str
160
+ !(Integer(str) rescue nil).nil?
161
+ end
162
+
163
+ # Make sure the form name is in the list.
164
+ def get_valid_form form_name
165
+ return nil if form_name.nil?
166
+ POETIC_FORMS[form_name.to_sym] ? form_name.to_sym : nil
167
+ end
168
+
169
+ # Get full form, from either the user-specified options,
170
+ # or the default poetic form.
171
+ def poetic_form_full poetic_form = @poetic_form
172
+ rhyme = get_poetic_form_token :rhyme, poetic_form
173
+ indent = get_poetic_form_token :indent, poetic_form
174
+ syllable = get_poetic_form_token :syllable, poetic_form
175
+ regex = get_poetic_form_token :regex, poetic_form
176
+ transform = get_poetic_form_token :transform, poetic_form
177
+ poetic_form[:rhyme] = rhyme
178
+ poetic_form[:indent] = indent if indent != ''
179
+ poetic_form[:syllable] = syllable if syllable != ''
180
+ poetic_form[:regex] = regex if regex
181
+ poetic_form[:transform] = transform if transform != ' '
182
+ poetic_form
183
+ end
184
+
185
+ # If the token is specified in the hash, return it,
186
+ # else get the token for the named form.
187
+ def get_poetic_form_rhyme_longest poetic_form = @poetic_form
188
+ get_poetic_form_token :rhyme, poetic_form, true
189
+ end
190
+ def get_poetic_form_rhyme poetic_form = @poetic_form
191
+ get_poetic_form_token :rhyme, poetic_form
192
+ end
193
+ def get_poetic_form_indent poetic_form = @poetic_form
194
+ get_poetic_form_token :indent, poetic_form
195
+ end
196
+ def get_poetic_form_token token,
197
+ poetic_form = @poetic_form,
198
+ longest = false
199
+ if poetic_form.empty?
200
+ ' '
201
+ elsif poetic_form[token]
202
+ poetic_form[token]
203
+ elsif poetic_form[:form].nil?
204
+ ' '
205
+ elsif POETIC_FORMS[poetic_form[:form].to_sym].nil?
206
+ ' '
207
+ else
208
+ token = POETIC_FORMS[poetic_form[:form].to_sym][token]
209
+ if token.is_a?(Array)
210
+ token = longest ? token.max_by(&:length) : token.sample
211
+ end
212
+ token
213
+ end
214
+ end
215
+
216
+ # Turn a rhyme format string into a usable array of tokens.
217
+ # Example formats:
218
+ # sonnet_form = 'abab cdcd efef gg'
219
+ # villanelle_form = 'A1bA2 abA1 abA2 abA1 abA2 abA1A2'
220
+ def tokenise_rhyme rhyme_string
221
+ return rhyme_string if rhyme_string.is_a? Array
222
+
223
+ tokens = []
224
+ buffer = ''
225
+ rhyme_string.split('').each do |char|
226
+ if !numeric?(char) and buffer != ''
227
+ tokens << buffer
228
+ buffer = ''
229
+ end
230
+ buffer += char
231
+ end
232
+ tokens << buffer
233
+
234
+ # Handle invalid tokens.
235
+ # ["a1"] ["1"] ["1122"] [" 1"] [" 11"] [":1"]
236
+ boolean_array = tokens.map do |i|
237
+ keep = i.gsub(/[^A-Z,0-9]/,'')
238
+ (keep == '' or !is_int?(keep))
239
+ end
240
+ valid = boolean_array.reduce{ |sum, i| sum && i }
241
+ raise Poefy::RhymeError unless valid
242
+ tokens = [' '] if tokens == ['']
243
+
244
+ # Output as a hash.
245
+ tokens.map do |i|
246
+ hash = {
247
+ token: i,
248
+ rhyme_letter: i[0].downcase
249
+ }
250
+ hash[:refrain] = i if i[0] == i[0].upcase
251
+ hash
252
+ end
253
+ end
254
+
255
+ # Indent an array of lines using a string of numbers.
256
+ def do_indent lines, str
257
+ return lines if str.nil? or lines.nil? or lines.empty?
258
+
259
+ # Convert the indent string into an array.
260
+ indent_arr = (str + '0' * lines.length).split('')
261
+ indent_arr = indent_arr.each_slice(lines.length).to_a[0]
262
+
263
+ # Convert to integers. Spaces should be zero.
264
+ indent_arr.map! { |i| Integer(i) rescue 0 }
265
+
266
+ # Zip, iterate, and prepend indent.
267
+ indent_arr.zip(lines).map do |line|
268
+ ' ' * line[0] + (line[1] ? line[1] : '')
269
+ end
270
+ end
271
+
272
+ # Sort by keys, to make it more human-readable.
273
+ def sort_hash input
274
+ output = {}
275
+ input.keys.sort.each do |k|
276
+ output[k] = input[k]
277
+ end
278
+ output
279
+ end
280
+
281
+ # Convert a range in the string form "1-6" to an array.
282
+ # Assumes elements are integers.
283
+ def range_to_array input
284
+ return input if input.is_a?(Numeric) || !input.include?('-')
285
+ vals = input.split('-').map(&:to_i).sort
286
+ (vals.first..vals.last).to_a
287
+ end
288
+
289
+ # Convert an array in the string form "4,6,8-10,12" to an array.
290
+ # Assumes elements are positive integers.
291
+ def string_to_array input
292
+ return [input.to_i.abs] if input.is_a?(Numeric)
293
+ arr = input.is_a?(Array) ? input : input.split(',')
294
+
295
+ # Convert to positive integers, and remove duplicates.
296
+ output = arr.map do |i|
297
+ range_to_array(i)
298
+ end.flatten.map do |i|
299
+ i.to_i.abs
300
+ end.sort.uniq.select do |i|
301
+ i != 0
302
+ end
303
+
304
+ # This cannot be an empty array []. It will fail anyway when we
305
+ # come to do the poem generation, but it's better to fail now.
306
+ raise Poefy::SyllableError.new if output.empty?
307
+ output
308
+ end
309
+
310
+ # '10'
311
+ # '9,10,11'
312
+ # '[8,8,5,5,8]'
313
+ # '[[8,9],[8,9],[4,5,6],[4,5,6],[8,9]]'
314
+ # '{1:8,2:8,3:5,4:5,5:8}'
315
+ # '{1:[8,9],2:[8,9],3:[4,5,6],4:[4,5,6],5:[8,9]}'
316
+ # '{0:[8,9],3:[4,5,6],4:[4,5,6]}'
317
+ # '{1:8,5:8}'
318
+ # '{1:8,2:8,3:5,-2:5,-1:8}'
319
+ # Use the rhyme string as base for the number of lines in total.
320
+ def transform_input_syllable input, rhyme
321
+ tokens = tokenise_rhyme rhyme
322
+ hash = transform_input_to_hash :syllable, input
323
+ hash = validate_hash_values :syllable, hash
324
+ hash = expand_hash_keys :syllable, hash, tokens, 0
325
+ end
326
+
327
+ # Do the same for regular expression strings.
328
+ def transform_input_regex input, rhyme
329
+ tokens = tokenise_rhyme rhyme
330
+ hash = transform_input_to_hash :regex, input
331
+ hash = validate_hash_values :regex, hash
332
+ hash = expand_hash_keys :regex, hash, tokens, //
333
+ end
334
+
335
+ # This should work for both syllable and regex strings.
336
+ # It should also be fine for Integer and Regexp 'input' values.
337
+ def transform_input_to_hash type, input
338
+ return input if input.is_a? Hash
339
+
340
+ # Don't go any further if we've got an invalid type.
341
+ valid_non_string =
342
+ input.is_a?(Array) ||
343
+ (type == :syllable and input.is_a?(Numeric)) ||
344
+ (type == :regex and input.is_a?(Regexp))
345
+ valid_string_like = !valid_non_string && input.respond_to?(:to_s)
346
+ raise TypeError unless valid_non_string || valid_string_like
347
+
348
+ # Perform different tasks depending on type.
349
+ input.strip! if input.is_a? String
350
+ input = input.to_i if input.is_a? Numeric
351
+ input = input.to_s if valid_string_like
352
+ return {} if input == ''
353
+
354
+ # This will be built up over the course of the method.
355
+ output = {}
356
+
357
+ # Figure out datatype.
358
+ # Regex string input cannot be an array, but syllable can.
359
+ datatype = :string
360
+ if !input.is_a?(Regexp)
361
+ if input.is_a?(Array)
362
+ datatype = :array
363
+ elsif type == :syllable and input[0] == '[' and input[-1] == ']'
364
+ datatype = :array
365
+ elsif input[0] == '{' and input[-1] == '}'
366
+ datatype = :hash
367
+ end
368
+ end
369
+
370
+ # If it's a basic string format, convert it to hash.
371
+ if datatype == :string
372
+
373
+ # Regex cannot be an array or range, but syllable can.
374
+ if type == :regex
375
+ arr = (input == []) ? [] : [Regexp.new(input)]
376
+
377
+ # Special case for if a user explicitly states only '0'.
378
+ elsif type == :syllable
379
+ arr = input == '0' ? [0] : string_to_array(input)
380
+ end
381
+
382
+ # Set this to be the default '0' hash value.
383
+ arr = arr.first if arr.count == 1
384
+ output = { 0 => arr }
385
+ datatype = :hash
386
+
387
+ # If it's wrapped in [] or {}, then evaluate it using YAML.
388
+ else
389
+
390
+ # Don't need to evaluate if it's already an Array.
391
+ if input.is_a?(Array)
392
+ output = input
393
+ else
394
+ begin
395
+ # If it's a regex, mandate the ': ' key separator.
396
+ # (This is so the string substitutions don't mess up the regex.)
397
+ # If it's a syllable, we can be more flexible with gsubs.
398
+ as_yaml = input
399
+ if type == :syllable
400
+ as_yaml = input.gsub(':', ': ').gsub('=>', ': ')
401
+ end
402
+ output = YAML.load(as_yaml)
403
+ rescue
404
+ # Raise a SyllableError or RegexError.
405
+ msg = "#{type.capitalize} hash is not valid YAML"
406
+ e = Object.const_get("Poefy::#{type.capitalize}Error")
407
+ raise e.new(msg)
408
+ end
409
+ end
410
+ end
411
+
412
+ # Convert array to positioned hash.
413
+ if datatype == :array
414
+ output = output.map.with_index do |e, i|
415
+ [i+1, e]
416
+ end.to_h
417
+ end
418
+
419
+ output
420
+ end
421
+
422
+ # Run different methods on each value depending on the type.
423
+ # If it's a syllable, convert all values to int arrays.
424
+ # If it's a regex, convert all values to regexp.
425
+ def validate_hash_values type, input
426
+ format_value = if type == :syllable
427
+ Proc.new do |x|
428
+ arr = string_to_array(x)
429
+ arr.count == 1 ? arr.first : arr
430
+ end
431
+ elsif type == :regex
432
+ Proc.new do |x|
433
+ x.is_a?(Regexp) ? x : Regexp.new(x.to_s)
434
+ end
435
+ end
436
+
437
+ # Validate values.
438
+ if input.is_a?(Hash)
439
+ input.each do |k, v|
440
+ begin
441
+ input[k] = format_value.call(v)
442
+ rescue
443
+ # Raise a SyllableError or RegexError.
444
+ msg = "#{type.capitalize} hash invalid, key='#{k}' value='#{v}'"
445
+ e = Object.const_get("Poefy::#{type.capitalize}Error")
446
+ raise e.new(msg)
447
+ end
448
+ end
449
+ elsif input.is_a?(Array)
450
+ input.map! do |i|
451
+ i = format_value.call(i)
452
+ end
453
+ end
454
+ input
455
+ end
456
+
457
+ # Convert non-positive-integer keys into the correct position.
458
+ def expand_hash_keys type, input, tokens, default
459
+ output = input.dup
460
+ line_count = tokens.length
461
+
462
+ # Handle negative keys.
463
+ output.keys.each do |k|
464
+ if k.is_a?(Numeric) and k < 0
465
+ line = line_count + 1 + k
466
+ output[line] = output[k]
467
+ end
468
+ end
469
+
470
+ # Find all lines that are not empty.
471
+ content_lines = tokens.map.with_index do |v, i|
472
+ i + 1 if (v[:token].strip != '')
473
+ end.compact
474
+
475
+ # Handle modulo lines.
476
+ # Handle 'e' even and 'o' odd lines.
477
+ modulo_lines = {}
478
+ output.keys.each do |k|
479
+ is_modulo = k.respond_to?(:include?) && k.include?('m')
480
+ is_even_odd = %w[e o].include?(k)
481
+ if is_modulo or is_even_odd
482
+ if is_modulo
483
+ vals = k.split('m').map(&:to_i)
484
+ divider = vals.first.to_i.abs
485
+ remainder = vals.last.to_i.abs
486
+ if divider == 0
487
+ # Raise a SyllableError or RegexError.
488
+ msg = "#{type.capitalize} hash invalid,"
489
+ msg += " key='#{k}', modulo='#{divider}m#{remainder}'"
490
+ e = Object.const_get("Poefy::#{type.capitalize}Error")
491
+ raise e.new(msg)
492
+ end
493
+ elsif is_even_odd
494
+ divider = 2
495
+ remainder = (k == 'e') ? 0 : 1
496
+ end
497
+ content_lines.modulo_index(divider, remainder, 1).each do |i|
498
+ modulo_lines[i] = output[k]
499
+ end
500
+ end
501
+ end
502
+
503
+ # Take {modulo_lines} as the base and overwrite it with specified keys.
504
+ if modulo_lines
505
+ output.keys.each do |k|
506
+ modulo_lines[k] = output[k]
507
+ end
508
+ output = modulo_lines
509
+ end
510
+
511
+ # Go through each line and make sure there is a value for each.
512
+ # Use default if there is no specific value.
513
+ default_value = output[0] ? output[0] : default
514
+ (1..line_count).each do |i|
515
+ output[i] = default_value if output[i].nil?
516
+ end
517
+
518
+ # Remove keys that are not numeric, or are less than or equal to zero.
519
+ output.reject!{ |k| !k.is_a?(Numeric) or k <= 0 }
520
+
521
+ # Return sorted hash.
522
+ sort_hash output
523
+ end
524
+
525
+ end
526
+
527
+ end
528
+
529
+ ################################################################################