Module: Poefy::PoeticFormFromText

Included in:
PoefyGen
Defined in:
lib/poefy/poetic_form_from_text.rb

Instance Method Summary collapse

Instance Method Details

#poetic_form_from_text(lines) ⇒ Object

Read a song lyric file, output a poetic_form that matches its form.



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/poefy/poetic_form_from_text.rb', line 13

def poetic_form_from_text lines

  # If lines is not an array, assume string and split on newlines.
  lines = lines.respond_to?(:each) ? lines : lines.split("\n")

  # Remove duplicate '' elements that are neighbours in the array.
  # https://genius.com/The-monkees-im-a-believer-lyrics
  prev_line = ''
  lines.map! do |i|
    out = (i == '' && prev_line == '') ? nil : i
    prev_line = i
    out
  end
  lines.compact!

  # For refrains, we don't care about the lines exactly, just
  #   the structure. So we can delete punctuation and downcase.
  lines = lines.map do |line|
    hash = {}
    hash[:orig] = line
    hash[:strip] = line.strip
    hash[:downcase] = line.strip.gsub(/[[:punct:]]/, '').downcase
    hash
  end

  # Find all the lines that are duplicated.
  # These will be the refrain lines.
  refrains = lines.map { |i| i[:downcase] }
  refrains = refrains.inject(Hash.new(0)) { |h, e| h[e] += 1 ; h }
  refrains = refrains.select { |k, v| v > 1 && k != '' }.keys

  # Give each a unique refrain ID.
  buffer = {}
  refrains.each.with_index { |line, id| buffer[line] = id }
  refrains = buffer

  # Loop through and describe each line.
  lines = lines.map.with_index do |line, index|
    hash = {}

    # Text of the line.
    hash[:strip] = line[:strip]
    hash[:downcase] = line[:downcase]

    # Get the phrase info for the line.
    phrase = phrase_info line[:strip]

    # Misc details.
    hash[:num] = index + 1
    hash[:syllable] = phrase[:syllables]
    hash[:last_word] = phrase[:last_word]
    hash[:indent] = (line[:orig].length - line[:orig].lstrip.length) / 2

    # The rhyme tag array for the line.
    hash[:rhyme_tags] = phrase[:rhymes]

    # Map [:refrain] and [:exact].
    # (They are mutually exclusive)
    # If it needs to be an exact line, we don't need rhyme tokens.
    if bracketed?(line[:strip])
      hash[:exact] = line[:strip]
      hash[:rhyme_letter] = nil
      hash[:syllable] = 0
    elsif refrains.keys.include?(line[:downcase])
      hash[:refrain] = refrains[line[:downcase]]
    end

    hash
  end

  # [:rhyme_tags] may well contain more than one rhyme tag.
  # e.g. 'wind' rhymes with 'sinned' and 'find'.
  # So we will compare this array against the rhymes of each
  #   other line in the array, to find the correct one to use.
  # We will work from the closest lines, until we find a match.
  lines.each.with_index do |line, index|

    # Compare each other rhyme tag, order by closeness.
    found_rhyme = line[:rhyme_tags].first
    if line[:rhyme_tags].length > 1
      lines.by_distance(index).each do |i|
        i[:rhyme_tags].each do |tag|
          if line[:rhyme_tags].include?(tag)
            found_rhyme = tag
            break
          end
        end
      end
    end

    # If we haven't found the rhyme, then it doesn't matter,
    #   just use the first in the tag array.
    lines[index][:rhyme_tags]   = *found_rhyme
    lines[index][:rhyme_tag]    =  found_rhyme
    lines[index][:rhyme_letter] =  found_rhyme
  end

  # Split into separate sections, [:rhyme] and [:syllable].
  rhyme = lines.map do |line|
    hash = {}
    hash[:token] = line[:rhyme_letter] || ' '
    hash[:rhyme_letter] = hash[:token]
    hash[:refrain] = line[:refrain] if line[:refrain]
    hash[:exact] = line[:exact] if line[:exact]
    hash
  end

  syllable = {}
  lines.map.with_index do |line, index|
    syllable[index+1] = line[:syllable] if line[:syllable] > 0
  end

  # Has to be a single character, so 9 is the maximum.
  indent = lines.map do |line|
    line[:indent] >= 9 ? 9 : line[:indent]
  end.join

  poetic_form = {
    rhyme: rhyme,
    syllable: syllable,
    indent: indent
  }
  poetic_form
end