poefy 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,330 @@
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',
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
+ }
111
+
112
+ # Create a regex specification for acrostics.
113
+ # acrostic('unintelligible')
114
+ # acrostic('unin tell igib le')
115
+ def acrostic word
116
+ output = {}
117
+ counter = 1
118
+ word.split('').each do |i|
119
+ output[counter] = /^[#{i.upcase}#{i.downcase}]/ if i != ' '
120
+ counter += 1
121
+ end
122
+ output
123
+ end
124
+
125
+ private
126
+
127
+ # Can the string be converted to integer?
128
+ def is_int? str
129
+ !(Integer(str) rescue nil).nil?
130
+ end
131
+
132
+ # Make sure the form name is in the list.
133
+ def get_valid_form form_name
134
+ return nil if form_name.nil?
135
+ POETIC_FORMS[form_name.to_sym] ? form_name.to_sym : nil
136
+ end
137
+
138
+ # Get full form, from either the user-specified options,
139
+ # or the default poetic form.
140
+ def poetic_form_full poetic_form = @poetic_form
141
+ rhyme = get_poetic_form_token :rhyme, poetic_form
142
+ indent = get_poetic_form_token :indent, poetic_form
143
+ syllable = get_poetic_form_token :syllable, poetic_form
144
+ regex = get_poetic_form_token :regex, poetic_form
145
+ poetic_form[:rhyme] = rhyme
146
+ poetic_form[:indent] = indent if indent != ''
147
+ poetic_form[:syllable] = syllable if syllable != ''
148
+ poetic_form[:regex] = regex if regex
149
+ poetic_form
150
+ end
151
+
152
+ # If the token is specified in the hash, return it,
153
+ # else get the token for the named form.
154
+ def get_poetic_form_rhyme poetic_form = @poetic_form
155
+ get_poetic_form_token :rhyme, poetic_form
156
+ end
157
+ def get_poetic_form_indent poetic_form = @poetic_form
158
+ get_poetic_form_token :indent, poetic_form
159
+ end
160
+ def get_poetic_form_token token, poetic_form = @poetic_form
161
+ if poetic_form.empty?
162
+ ' '
163
+ elsif poetic_form[token]
164
+ poetic_form[token]
165
+ elsif poetic_form[:form].nil?
166
+ ' '
167
+ elsif POETIC_FORMS[poetic_form[:form].to_sym].nil?
168
+ ' '
169
+ else
170
+ token = POETIC_FORMS[poetic_form[:form].to_sym][token]
171
+ token = token.is_a?(Array) ? token.sample : token
172
+ end
173
+ end
174
+
175
+ # Turn a rhyme format string into a usable array of tokens.
176
+ # Example formats:
177
+ # sonnet_form = 'abab cdcd efef gg'
178
+ # villanelle_form = 'A1bA2 abA1 abA2 abA1 abA2 abA1A2'
179
+ def tokenise_rhyme rhyme_string
180
+ return rhyme_string if rhyme_string.is_a? Array
181
+
182
+ tokens = []
183
+ buffer = ''
184
+ rhyme_string.split('').each do |char|
185
+ if !numeric?(char) and buffer != ''
186
+ tokens << buffer
187
+ buffer = ''
188
+ end
189
+ buffer += char
190
+ end
191
+ tokens << buffer
192
+
193
+ # Handle invalid tokens.
194
+ # ["a1"] ["1"] ["1122"] [" 1"] [" 11"] [":1"]
195
+ boolean_array = tokens.map do |i|
196
+ keep = i.gsub(/[^A-Z,0-9]/,'')
197
+ (keep == '' or !is_int?(keep) or !is_int?(keep))
198
+ end
199
+ valid = boolean_array.reduce{ |sum, i| sum && i }
200
+ if !valid
201
+ return handle_error 'ERROR: Rhyme string is not valid', []
202
+ end
203
+ tokens = [' '] if tokens == ['']
204
+ tokens
205
+ end
206
+
207
+ # Indent an array of lines using a string of numbers.
208
+ def do_indent lines, str
209
+ return lines if str.nil? or lines.nil? or lines.empty?
210
+
211
+ # Convert the indent string into an array.
212
+ indent_arr = (str + '0' * lines.length).split('')
213
+ indent_arr = indent_arr.each_slice(lines.length).to_a[0]
214
+
215
+ # Convert to integers. Spaces should be zero.
216
+ indent_arr.map! { |i| Integer(i) rescue 0 }
217
+
218
+ # Zip, iterate, and prepend indent.
219
+ indent_arr.zip(lines).map do |line|
220
+ ' ' * line[0] + (line[1] ? line[1] : '')
221
+ end
222
+ end
223
+
224
+ # Runs a block of code without warnings.
225
+ # Used for 'eval' calls.
226
+ def silence_warnings &block
227
+ warn_level = $VERBOSE
228
+ $VERBOSE = nil
229
+ result = block.call
230
+ $VERBOSE = warn_level
231
+ result
232
+ end
233
+
234
+ # Sort by keys, to make JSON more human-readable.
235
+ def sort_hash input
236
+ output = {}
237
+ input.keys.sort.each do |k|
238
+ output[k] = input[k]
239
+ end
240
+ output
241
+ end
242
+
243
+ # '10'
244
+ # '9,10,11'
245
+ # '[8,8,5,5,8]'
246
+ # '[[8,9],[8,9],[4,5,6],[4,5,6],[8,9]]'
247
+ # '{1:8,2:8,3:5,4:5,5:8}'
248
+ # '{1:[8,9],2:[8,9],3:[4,5,6],4:[4,5,6],5:[8,9]}'
249
+ # '{0:[8,9],3:[4,5,6],4:[4,5,6]}'
250
+ # '{1:8,5:8}'
251
+ # '{1:8,2:8,3:5,-2:5,-1:8}'
252
+ # Uses #eval, so pretty likely to mess up big time on error.
253
+ # Use the rhyme string as base for the number of lines in total.
254
+ def transform_string_syllable input, rhyme
255
+ return input if input.is_a? Hash
256
+ input = input.to_s
257
+ transform_string_to_hash :syllable, input.gsub(':','=>'), rhyme, 0
258
+ end
259
+
260
+ # Do the same for regular expression strings.
261
+ def transform_string_regex input, rhyme
262
+ transform_string_to_hash :regex, input, rhyme, nil
263
+ end
264
+
265
+ # This should work for both syllable and regex strings.
266
+ def transform_string_to_hash type, string, rhyme, default
267
+ return string if string.is_a? Hash
268
+ return {} if string == ' '
269
+
270
+ output = {}
271
+ line_count = tokenise_rhyme(rhyme).length
272
+
273
+ # Figure out datatype.
274
+ datatype = 'string'
275
+ datatype = 'array' if !string.is_a?(Regexp) and string[0] == '['
276
+ datatype = 'hash' if !string.is_a?(Regexp) and string[0] == '{'
277
+
278
+ # Convert string to array, and eval the others.
279
+ if datatype == 'string'
280
+
281
+ # Regex cannot be an array, but syllable can.
282
+ if type == :syllable
283
+ arr = each_to_int(string.split(','))
284
+ elsif type == :regex
285
+ arr = [Regexp.new(string)]
286
+ end
287
+
288
+ # Set this to be the default '0' hash value.
289
+ arr = arr.first if arr.count == 1
290
+ output = { 0 => arr }
291
+ datatype = 'hash'
292
+ else
293
+ output = silence_warnings { eval string }
294
+ end
295
+
296
+ # Convert array to positioned hash.
297
+ if datatype == 'array'
298
+ output = output.map.with_index do |e, i|
299
+ [i+1, e]
300
+ end.to_h
301
+ end
302
+
303
+ # Go through each line and make sure there is a value for each.
304
+ # Use default if there is no specific value.
305
+ default_value = output[0] ? output[0] : default
306
+ (1..line_count).each do |i|
307
+ output[i] = default_value if output[i].nil?
308
+ end
309
+
310
+ # Handle negative keys.
311
+ output.keys.each do |k|
312
+ if k < 0
313
+ line = line_count + 1 + k
314
+ output[line] = output[k]
315
+ end
316
+ end
317
+
318
+ # Remove keys less than or equal to zero.
319
+ output.reject!{ |k| k <= 0 }
320
+
321
+ # Return sorted hash.
322
+ # ToDo: Doesn't need to be sorted in final code.
323
+ sort_hash output
324
+ end
325
+
326
+ end
327
+
328
+ end
329
+
330
+ ################################################################################
data/lib/poefy/self.rb ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ ################################################################################
5
+ # Class methods for Poefy module.
6
+ ################################################################################
7
+
8
+ module Poefy
9
+
10
+ # Array of all '.db' files in /data/.
11
+ # Do not include databases used for testing.
12
+ def self.all_databases
13
+ path = File.expand_path('../../../data', __FILE__)
14
+ Dir["#{path}/*.db"].map do |i|
15
+ File.basename(i, '.db')
16
+ end.reject{ |i| i.start_with?('spec_') }
17
+ end
18
+
19
+ end
20
+
21
+ ################################################################################
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ ################################################################################
5
+ # A bunch of string manipulation.
6
+ ################################################################################
7
+
8
+ require 'ruby_rhymes'
9
+ require 'wordfilter'
10
+
11
+ ################################################################################
12
+
13
+ module Poefy
14
+
15
+ module StringManipulation
16
+
17
+ private
18
+
19
+ # True if the whole text string can be expressed as Float.
20
+ def numeric? text
21
+ Float(text) != nil rescue false
22
+ end
23
+
24
+ # The first word in a text string. Relies on space for whitespace.
25
+ # Discards any punctuation.
26
+ def first_word text
27
+ (text.gsub(/[[:punct:]]/,'').scan(/^[^ ]+/).first rescue '') || ''
28
+ end
29
+
30
+ # Uses 'ruby_rhymes' to find the rhyme key for a text.
31
+ def get_rhymes text
32
+ (numeric?(text[-1]) ? [] : text.to_phrase.rhymes.keys) rescue []
33
+ end
34
+
35
+ # The number of syllables in the text.
36
+ def syllables text
37
+ text.to_phrase.syllables rescue 0
38
+ end
39
+
40
+ # Final line must close with sentence-end punctuation.
41
+ def end_the_sentence text
42
+ if find = text.scan(/[[:punct:]]+$/).first
43
+ swap = find.tr(',:;-', '.').delete('—–-')
44
+ text.reverse.sub(find.reverse, swap.reverse).reverse
45
+ else
46
+ text += '.'
47
+ end
48
+ end
49
+
50
+ # Does the sentence end with a .!?
51
+ def has_stop_punctuation? text
52
+ return false if text.nil?
53
+ punct = text.scan(/[[:punct:]]+$/).first
54
+ !!(punct.match(/[\.!?]/) if punct)
55
+ end
56
+
57
+ # Capitalise the first character of a string
58
+ def capitalize_first text
59
+ text[0] = text[0].upcase
60
+ text
61
+ end
62
+
63
+ # Convert each element in an input array to Integer, and raise
64
+ # an error if the conversion is not possible for any element.
65
+ def each_to_int input_array, error_to_raise = TypeError
66
+ output_array = []
67
+ input_array.each do |elem|
68
+ begin
69
+ output_array << Integer(elem)
70
+ rescue ArgumentError => e
71
+ raise error_to_raise
72
+ end
73
+ end
74
+ output_array
75
+ end
76
+
77
+ end
78
+
79
+ end
80
+
81
+ ################################################################################
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ ################################################################################
5
+ # The current version number and date.
6
+ ################################################################################
7
+
8
+ module Poefy
9
+
10
+ def self.version_number
11
+ Gem::Version.new VERSION::STRING
12
+ end
13
+
14
+ def self.version_date
15
+ '2017-05-17'
16
+ end
17
+
18
+ module VERSION
19
+ MAJOR = 0
20
+ MINOR = 5
21
+ TINY = 1
22
+ PRE = nil
23
+
24
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
25
+ end
26
+
27
+ end
28
+
29
+ ################################################################################
data/lib/poefy.rb ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ ################################################################################
5
+ # Line-based, used only to rearrange lines of text, not create new lines.
6
+ # Also uses 'wordfilter' to get rid of yucky words.
7
+ #
8
+ # https://en.wikipedia.org/wiki/Category:Western_medieval_lyric_forms
9
+ # https://en.wikipedia.org/wiki/Virelai
10
+ # https://en.wikipedia.org/wiki/List_of_compositions_by_Guillaume_de_Machaut#Virelais
11
+ ################################################################################
12
+
13
+ require 'ruby_rhymes'
14
+ require 'wordfilter'
15
+ require 'tempfile'
16
+ require 'sqlite3'
17
+ require 'timeout'
18
+ require 'json'
19
+
20
+ require_relative 'poefy/version.rb'
21
+ require_relative 'poefy/self.rb'
22
+ require_relative 'poefy/poefy_gen_base.rb'
23
+ require_relative 'poefy/generation.rb'
24
+ require_relative 'poefy/poetic_forms.rb'
25
+ require_relative 'poefy/string_manipulation.rb'
26
+ require_relative 'poefy/handle_error.rb'
27
+ require_relative 'poefy/database.rb'
28
+ require_relative 'poefy/conditional_satisfaction.rb'
29
+
30
+ ################################################################################
31
+
32
+ # Create a database from text lines.
33
+ # Read the database to generate poetry.
34
+ module Poefy
35
+
36
+ class PoefyGen
37
+
38
+ include Poefy::PoefyGenBase
39
+ include Poefy::Generation
40
+ include Poefy::PoeticForms
41
+ include Poefy::StringManipulation
42
+ include Poefy::ConditionalSatisfaction
43
+ include Poefy::HandleError
44
+
45
+ end
46
+
47
+ end
48
+
49
+ ################################################################################
data/poefy.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # Encoding: UTF-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'poefy/version'
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = 'poefy'
9
+ s.authors = ['Paul Thompson']
10
+ s.email = ['nossidge@gmail.com']
11
+
12
+ s.summary = %q{Create rhyming poetry by rearranging lines of text}
13
+ s.description = %q{Create poems from an input text file, by generating and querying a SQLite database describing each line. Poems are created using a template to select lines from the database, according to closing rhyme, syllable count, and regex matching.}
14
+ s.homepage = 'https://github.com/nossidge/poefy'
15
+
16
+ s.version = Poefy.version_number
17
+ s.date = Poefy.version_date
18
+ s.license = 'GPL-3.0'
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
23
+ s.require_paths = ['lib']
24
+ s.bindir = 'bin'
25
+
26
+ s.add_development_dependency('bundler', '~> 1.13')
27
+ s.add_development_dependency('rake', '~> 10.0')
28
+ s.add_development_dependency('rspec', '~> 3.0')
29
+
30
+ s.add_runtime_dependency('sqlite3', '~> 1.3', '>= 1.3.13')
31
+ s.add_runtime_dependency('ruby_rhymes', '~> 0.1', '>= 0.1.2')
32
+ s.add_runtime_dependency('wordfilter', '~> 0.2', '>= 0.2.6')
33
+ end