poefy 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,367 +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
- module Poefy
49
-
50
- module PoeticForms
51
-
52
- # If the token is an array, then a random sample will be used.
53
- POETIC_FORMS = {
54
- default: {
55
- rhyme: 'a',
56
- indent: '0',
57
- syllable: ''
58
- },
59
- rondeau: {
60
- rhyme: 'aabba aabR aabbaR',
61
- indent: '',
62
- syllable: ''
63
- },
64
- villanelle: {
65
- rhyme: 'A1bA2 abA1 abA2 abA1 abA2 abA1A2',
66
- indent: '010 001 001 001 001 0011',
67
- syllable: ''
68
- },
69
- ballade: {
70
- rhyme: 'ababbcbC ababbcbC ababbcbC bcbC',
71
- indent: '',
72
- syllable: ''
73
- },
74
- ballata: {
75
- rhyme: ['AbbaA','AbbaAbbaA','AbbaAbbaAbbaA'],
76
- indent: '',
77
- syllable: ''
78
- },
79
- sonnet: {
80
- rhyme: 'ababcdcdefefgg',
81
- indent: '',
82
- syllable: ''
83
- },
84
- petrarchan: {
85
- rhyme: ['abbaabbacdecde','abbaabbacdccdc','abbaabbacdcddc',
86
- 'abbaabbacddcdd','abbaabbacddece','abbaabbacdcdcd'],
87
- indent: ['01100110010010','10001000100100'],
88
- syllable: ''
89
- },
90
- limerick: {
91
- rhyme: 'aabba',
92
- indent: '',
93
- syllable: '{1:[8],2:[8],3:[4,5],4:[4,5],5:[8]}'
94
- },
95
- haiku: {
96
- rhyme: 'abc',
97
- indent: '',
98
- syllable: '[5,7,5]'
99
- },
100
- common: {
101
- rhyme: 'abcb',
102
- indent: '0101',
103
- syllable: '[8,6,8,6]'
104
- },
105
- ballad: {
106
- rhyme: 'abab',
107
- indent: '0101',
108
- syllable: '[8,6,8,6]'
109
- },
110
- double_dactyl: {
111
- rhyme: 'abcd efgd',
112
- indent: '',
113
- syllable: '[6,6,6,4,0,6,6,6,4]',
114
- regex: '{7=>/^\S+$/}'
115
- }
116
- }
117
-
118
- # Create a regex specification for acrostics.
119
- # acrostic('unintelligible')
120
- # acrostic('unin tell igib le')
121
- def acrostic word
122
- output = {}
123
- word.split('').each.with_index do |char, i|
124
- output[i + 1] = /^[#{char.downcase}]/i if char != ' '
125
- end
126
- output
127
- end
128
-
129
- # Create a regex specification for acrostics.
130
- # Uses special logic for 'X'.
131
- # Match words starting 'ex' and then change case to 'eX'.
132
- def acrostic_x word
133
- regex = {}
134
- transform = {}
135
- word.split('').each.with_index do |char, i|
136
- if char.downcase == 'x'
137
- regex[i + 1] = /^ex/i
138
- transform[i + 1] = proc do |line|
139
- line[0..1] = 'eX'
140
- ' ' + line
141
- end
142
- elsif char != ' '
143
- regex[i + 1] = /^[#{char.downcase}]/i
144
- transform[i + 1] = proc do |line|
145
- ' ' + line
146
- end
147
- end
148
- end
149
- { regex: regex, transform: transform }
150
- end
151
-
152
- private
153
-
154
- # Can the string be converted to integer?
155
- def is_int? str
156
- !(Integer(str) rescue nil).nil?
157
- end
158
-
159
- # Make sure the form name is in the list.
160
- def get_valid_form form_name
161
- return nil if form_name.nil?
162
- POETIC_FORMS[form_name.to_sym] ? form_name.to_sym : nil
163
- end
164
-
165
- # Get full form, from either the user-specified options,
166
- # or the default poetic form.
167
- def poetic_form_full poetic_form = @poetic_form
168
- rhyme = get_poetic_form_token :rhyme, poetic_form
169
- indent = get_poetic_form_token :indent, poetic_form
170
- syllable = get_poetic_form_token :syllable, poetic_form
171
- regex = get_poetic_form_token :regex, poetic_form
172
- transform = get_poetic_form_token :transform, poetic_form
173
- poetic_form[:rhyme] = rhyme
174
- poetic_form[:indent] = indent if indent != ''
175
- poetic_form[:syllable] = syllable if syllable != ''
176
- poetic_form[:regex] = regex if regex
177
- poetic_form[:transform] = transform if transform != ' '
178
- poetic_form
179
- end
180
-
181
- # If the token is specified in the hash, return it,
182
- # else get the token for the named form.
183
- def get_poetic_form_rhyme poetic_form = @poetic_form
184
- get_poetic_form_token :rhyme, poetic_form
185
- end
186
- def get_poetic_form_indent poetic_form = @poetic_form
187
- get_poetic_form_token :indent, poetic_form
188
- end
189
- def get_poetic_form_token token, poetic_form = @poetic_form
190
- if poetic_form.empty?
191
- ' '
192
- elsif poetic_form[token]
193
- poetic_form[token]
194
- elsif poetic_form[:form].nil?
195
- ' '
196
- elsif POETIC_FORMS[poetic_form[:form].to_sym].nil?
197
- ' '
198
- else
199
- token = POETIC_FORMS[poetic_form[:form].to_sym][token]
200
- token = token.is_a?(Array) ? token.sample : token
201
- end
202
- end
203
-
204
- # Turn a rhyme format string into a usable array of tokens.
205
- # Example formats:
206
- # sonnet_form = 'abab cdcd efef gg'
207
- # villanelle_form = 'A1bA2 abA1 abA2 abA1 abA2 abA1A2'
208
- def tokenise_rhyme rhyme_string
209
- return rhyme_string if rhyme_string.is_a? Array
210
-
211
- tokens = []
212
- buffer = ''
213
- rhyme_string.split('').each do |char|
214
- if !numeric?(char) and buffer != ''
215
- tokens << buffer
216
- buffer = ''
217
- end
218
- buffer += char
219
- end
220
- tokens << buffer
221
-
222
- # Handle invalid tokens.
223
- # ["a1"] ["1"] ["1122"] [" 1"] [" 11"] [":1"]
224
- boolean_array = tokens.map do |i|
225
- keep = i.gsub(/[^A-Z,0-9]/,'')
226
- (keep == '' or !is_int?(keep) or !is_int?(keep))
227
- end
228
- valid = boolean_array.reduce{ |sum, i| sum && i }
229
- if !valid
230
- return handle_error 'ERROR: Rhyme string is not valid', []
231
- end
232
- tokens = [' '] if tokens == ['']
233
-
234
- # Output as a hash.
235
- tokens.map do |i|
236
- hash = {
237
- token: i,
238
- rhyme_letter: i[0].downcase
239
- }
240
- hash[:refrain] = i if i[0] == i[0].upcase
241
- hash
242
- end
243
- end
244
-
245
- # Indent an array of lines using a string of numbers.
246
- def do_indent lines, str
247
- return lines if str.nil? or lines.nil? or lines.empty?
248
-
249
- # Convert the indent string into an array.
250
- indent_arr = (str + '0' * lines.length).split('')
251
- indent_arr = indent_arr.each_slice(lines.length).to_a[0]
252
-
253
- # Convert to integers. Spaces should be zero.
254
- indent_arr.map! { |i| Integer(i) rescue 0 }
255
-
256
- # Zip, iterate, and prepend indent.
257
- indent_arr.zip(lines).map do |line|
258
- ' ' * line[0] + (line[1] ? line[1] : '')
259
- end
260
- end
261
-
262
- # Runs a block of code without warnings.
263
- # Used for 'eval' calls.
264
- def silence_warnings &block
265
- warn_level = $VERBOSE
266
- $VERBOSE = nil
267
- result = block.call
268
- $VERBOSE = warn_level
269
- result
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
- # '10'
282
- # '9,10,11'
283
- # '[8,8,5,5,8]'
284
- # '[[8,9],[8,9],[4,5,6],[4,5,6],[8,9]]'
285
- # '{1:8,2:8,3:5,4:5,5:8}'
286
- # '{1:[8,9],2:[8,9],3:[4,5,6],4:[4,5,6],5:[8,9]}'
287
- # '{0:[8,9],3:[4,5,6],4:[4,5,6]}'
288
- # '{1:8,5:8}'
289
- # '{1:8,2:8,3:5,-2:5,-1:8}'
290
- # Uses #eval, so pretty likely to mess up big time on error.
291
- # Use the rhyme string as base for the number of lines in total.
292
- def transform_string_syllable input, rhyme
293
- return input if input.is_a? Hash
294
- input = input.to_s
295
- transform_string_to_hash :syllable, input.gsub(':','=>'), rhyme, 0
296
- end
297
-
298
- # Do the same for regular expression strings.
299
- def transform_string_regex input, rhyme
300
- transform_string_to_hash :regex, input, rhyme, nil
301
- end
302
-
303
- # This should work for both syllable and regex strings.
304
- def transform_string_to_hash type, string, rhyme, default
305
- return string if string.is_a? Hash
306
- return {} if string == ' '
307
-
308
- output = {}
309
- line_count = tokenise_rhyme(rhyme).length
310
-
311
- # Figure out datatype.
312
- datatype = 'string'
313
- datatype = 'array' if !string.is_a?(Regexp) and string[0] == '['
314
- datatype = 'hash' if !string.is_a?(Regexp) and string[0] == '{'
315
-
316
- # Convert string to array, and eval the others.
317
- if datatype == 'string'
318
-
319
- # Regex cannot be an array, but syllable can.
320
- if type == :syllable
321
- arr = each_to_int(string.split(','))
322
- elsif type == :regex
323
- arr = [Regexp.new(string)]
324
- end
325
-
326
- # Set this to be the default '0' hash value.
327
- arr = arr.first if arr.count == 1
328
- output = { 0 => arr }
329
- datatype = 'hash'
330
- else
331
- output = silence_warnings { eval string }
332
- end
333
-
334
- # Convert array to positioned hash.
335
- if datatype == 'array'
336
- output = output.map.with_index do |e, i|
337
- [i+1, e]
338
- end.to_h
339
- end
340
-
341
- # Go through each line and make sure there is a value for each.
342
- # Use default if there is no specific value.
343
- default_value = output[0] ? output[0] : default
344
- (1..line_count).each do |i|
345
- output[i] = default_value if output[i].nil?
346
- end
347
-
348
- # Handle negative keys.
349
- output.keys.each do |k|
350
- if k < 0
351
- line = line_count + 1 + k
352
- output[line] = output[k]
353
- end
354
- end
355
-
356
- # Remove keys less than or equal to zero.
357
- output.reject!{ |k| k <= 0 }
358
-
359
- # Return sorted hash.
360
- sort_hash output
361
- end
362
-
363
- end
364
-
365
- end
366
-
367
- ################################################################################
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
+ ################################################################################