pslm 0.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.
- checksums.yaml +7 -0
- data/bin/pslm.rb +150 -0
- data/lib/pslm.rb +17 -0
- data/lib/pslm/consoleoutputter.rb +281 -0
- data/lib/pslm/joinedinput.rb +42 -0
- data/lib/pslm/latexoutputter.rb +449 -0
- data/lib/pslm/outputter.rb +47 -0
- data/lib/pslm/psalm.rb +149 -0
- data/lib/pslm/psalmpatterns.rb +97 -0
- data/lib/pslm/psalmpointer.rb +89 -0
- data/lib/pslm/psalmtones/solesmes196x.yml +22 -0
- data/lib/pslm/pslmoutputter.rb +71 -0
- data/lib/pslm/pslmreader.rb +225 -0
- data/lib/pslm/structuredsetup.rb +74 -0
- metadata +114 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module Pslm
|
4
|
+
|
5
|
+
# abstract superclass of all outputters
|
6
|
+
class Outputter
|
7
|
+
|
8
|
+
DEFAULT_SETUP = {
|
9
|
+
:no_formatting => false, # TODO
|
10
|
+
:title => {
|
11
|
+
:template => :semantic,
|
12
|
+
},
|
13
|
+
:pointing => {
|
14
|
+
:accents => [2,2],
|
15
|
+
:preparatory => [0,0],
|
16
|
+
:accent_style => :underline, # :underline|:bold|:semantic
|
17
|
+
},
|
18
|
+
:break_hints => true,
|
19
|
+
:parts => {
|
20
|
+
:marks_type => :semantic, # :simple|:semantic|:no
|
21
|
+
:novydvur_newlines => false,
|
22
|
+
},
|
23
|
+
:verses => {
|
24
|
+
:paragraphify => true
|
25
|
+
},
|
26
|
+
:skip_verses => 0,
|
27
|
+
:strophes => {
|
28
|
+
:end_marks => false,
|
29
|
+
:paragraph_space => true,
|
30
|
+
:mark_last_strophe => false,
|
31
|
+
},
|
32
|
+
:wrapper => {
|
33
|
+
:environment_name => 'psalmus'
|
34
|
+
},
|
35
|
+
:lettrine => nil,
|
36
|
+
:line_break_last_line => false, # TODO
|
37
|
+
:quote => false, # :single|:double|:guillemets|:czech|:delete
|
38
|
+
:mark_short_verses => false, # TODO
|
39
|
+
:final_add_content => false,
|
40
|
+
}
|
41
|
+
|
42
|
+
def process(psalm, options={})
|
43
|
+
raise RuntimeError.new "Abstract class. Method not implemented."
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
data/lib/pslm/psalm.rb
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
# parsed psalm data
|
6
|
+
module Pslm
|
7
|
+
class Psalm
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@header = OpenStruct.new
|
11
|
+
@header.title = ''
|
12
|
+
@strophes = [ Strophe.new ]
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :header, :strophes
|
16
|
+
|
17
|
+
# accesses all verses of the psalm regardless of the strophes
|
18
|
+
def verses
|
19
|
+
@strophes.collect {|s| s.verses }.flatten
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_strophe(s=nil)
|
23
|
+
if s != nil then
|
24
|
+
@strophes << s
|
25
|
+
else
|
26
|
+
@strophes << Strophe.new
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_verse(v)
|
31
|
+
@strophes.last.verses << v
|
32
|
+
end
|
33
|
+
|
34
|
+
def ==(ps2)
|
35
|
+
ps2.is_a?(Psalm) && self.header == ps2.header && self.strophes == ps2.strophes
|
36
|
+
end
|
37
|
+
|
38
|
+
# returns a new Psalm containing verses of the second appended to the verses
|
39
|
+
# of the first; everything else is copied from the first
|
40
|
+
# (title etc. of the second Psalm get lost)
|
41
|
+
def +(ps2)
|
42
|
+
ps_res = self.dup
|
43
|
+
ps_res.strophes.concat ps2.strophes
|
44
|
+
return ps_res
|
45
|
+
end
|
46
|
+
|
47
|
+
class Strophe
|
48
|
+
def initialize
|
49
|
+
@verses = []
|
50
|
+
end
|
51
|
+
|
52
|
+
attr_reader :verses
|
53
|
+
|
54
|
+
def ==(s2)
|
55
|
+
s2.is_a?(Strophe) && self.verses == s2.verses
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Verse
|
60
|
+
def initialize(flex=nil, first=nil, second=nil)
|
61
|
+
@flex = flex
|
62
|
+
@first = first
|
63
|
+
@second = second
|
64
|
+
if second.nil? then
|
65
|
+
yield self
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
attr_accessor :flex, :first, :second
|
70
|
+
|
71
|
+
def parts
|
72
|
+
r = [@first, @second]
|
73
|
+
r.unshift @flex if @flex
|
74
|
+
return r
|
75
|
+
end
|
76
|
+
|
77
|
+
def ==(v2)
|
78
|
+
unless v2.is_a? Verse
|
79
|
+
return false
|
80
|
+
end
|
81
|
+
|
82
|
+
[:flex, :first, :second].each do |part|
|
83
|
+
if self.send(part) != v2.send(part) then
|
84
|
+
return false
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
return true
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class VersePart
|
93
|
+
def initialize(words, src, pos)
|
94
|
+
@words = words
|
95
|
+
@src = src
|
96
|
+
@pos = pos
|
97
|
+
end
|
98
|
+
|
99
|
+
# Array of Words
|
100
|
+
attr_reader :words
|
101
|
+
|
102
|
+
# the source text before parsing
|
103
|
+
attr_reader :src
|
104
|
+
|
105
|
+
# position in the verse - one of :flex, :first, :second
|
106
|
+
attr_reader :pos
|
107
|
+
|
108
|
+
def ==(p2)
|
109
|
+
p2.is_a?(VersePart) && self.words == p2.words
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class Word
|
114
|
+
def initialize(syllables)
|
115
|
+
@syllables = syllables
|
116
|
+
end
|
117
|
+
|
118
|
+
attr_reader :syllables
|
119
|
+
|
120
|
+
def ==(w2)
|
121
|
+
w2.is_a?(Word) && self.syllables == w2.syllables
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class Syllable < String
|
126
|
+
def initialize(chars, accent=false)
|
127
|
+
super(chars)
|
128
|
+
@accent = accent
|
129
|
+
end
|
130
|
+
|
131
|
+
def accent?
|
132
|
+
@accent
|
133
|
+
end
|
134
|
+
|
135
|
+
def ==(s2)
|
136
|
+
s2.is_a? Syllable and (self.to_s == s2.to_s) and (self.accent? == s2.accent?)
|
137
|
+
end
|
138
|
+
|
139
|
+
def to_s
|
140
|
+
String.new self
|
141
|
+
end
|
142
|
+
|
143
|
+
def inspect
|
144
|
+
accent = @accent ? ' accent' : ''
|
145
|
+
return '#' + "<#{self.class}:#{self.object_id} \"#{self}\"#{accent}>"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Pslm
|
6
|
+
|
7
|
+
# holds and provides information on a set of psalm tones:
|
8
|
+
# how many accents and preparatory syllables does each tone
|
9
|
+
# need in first and second half-verse.
|
10
|
+
class PsalmPatterns
|
11
|
+
|
12
|
+
# expects a well-formed data structure -
|
13
|
+
# as an example see the yaml files in directory psalmtones
|
14
|
+
def initialize(data)
|
15
|
+
@data = {}
|
16
|
+
|
17
|
+
# downcase all tone and difference identifiers
|
18
|
+
data.each_pair do |tone, tdata|
|
19
|
+
if tdata[1][1].is_a? Hash then
|
20
|
+
dfrs = {}
|
21
|
+
tdata[1][1].each_pair do |df, num|
|
22
|
+
dfrs[df.downcase] = num
|
23
|
+
end
|
24
|
+
tdata[1][1] = dfrs
|
25
|
+
end
|
26
|
+
@data[tone.downcase] = tdata
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# alternative constructors
|
31
|
+
class << self
|
32
|
+
|
33
|
+
def from_yaml(str)
|
34
|
+
new(YAML::load(str))
|
35
|
+
end
|
36
|
+
|
37
|
+
def from_file(fname)
|
38
|
+
from_yaml(File.open(fname).read())
|
39
|
+
end
|
40
|
+
|
41
|
+
# loads and returns data of a default tone-set
|
42
|
+
def default
|
43
|
+
from_file(File.expand_path('psalmtones/solesmes196x.yml', File.dirname(__FILE__)))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# returns an Array like [[1,2], [2,0]]
|
48
|
+
# specifying number of accents and
|
49
|
+
def tone_data(tone, difference='')
|
50
|
+
tone.downcase!
|
51
|
+
difference.downcase!
|
52
|
+
|
53
|
+
unless @data.has_key? tone
|
54
|
+
raise ToneError.new "Unknown tone '#{tone}'"
|
55
|
+
end
|
56
|
+
|
57
|
+
if @data[tone][1][1].is_a? Hash then
|
58
|
+
unless @data[tone][1][1].has_key? difference
|
59
|
+
raise DifferenceError.new "Unknown difference '#{difference}' for tone '#{tone}'"
|
60
|
+
end
|
61
|
+
|
62
|
+
return [ @data[tone][0].dup , [ @data[tone][1][0], @data[tone][1][1][difference] ] ]
|
63
|
+
else
|
64
|
+
return @data[tone]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# returns tone data for a tone identified by a String like 'I.D'
|
69
|
+
def tone_data_str(tonestr)
|
70
|
+
tone_data(*normalize_tone_str(tonestr))
|
71
|
+
end
|
72
|
+
|
73
|
+
# returns [tone, difference]
|
74
|
+
def normalize_tone_str(str)
|
75
|
+
tone, sep, difference = str.rpartition(/[\.\s]/)
|
76
|
+
return [tone, difference]
|
77
|
+
end
|
78
|
+
|
79
|
+
# returns tone data in a data structure containing Hashes
|
80
|
+
# with descriptive keys
|
81
|
+
def tone_data_verbose(tone, difference='')
|
82
|
+
describe_tone_data(tone, difference)
|
83
|
+
end
|
84
|
+
|
85
|
+
def describe_tone_data(data)
|
86
|
+
return data[0..1].collect {|part| {:accents => part[0], :preparatory => part[1] } }
|
87
|
+
end
|
88
|
+
|
89
|
+
# tone unknown
|
90
|
+
class ToneError < KeyError
|
91
|
+
end
|
92
|
+
|
93
|
+
# difference unknown
|
94
|
+
class DifferenceError < KeyError
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module Pslm
|
4
|
+
|
5
|
+
# Workhorse of the pslm CLI utility.
|
6
|
+
class PsalmPointer
|
7
|
+
|
8
|
+
def initialize(options)
|
9
|
+
@options = options
|
10
|
+
|
11
|
+
# make @options a StructuredSetup - because of deep #dup and recursive #update
|
12
|
+
unless @options.is_a? StructuredSetup
|
13
|
+
@options = StructuredSetup.new @options
|
14
|
+
end
|
15
|
+
|
16
|
+
@reader = PslmReader.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def process(psalm_files, output_file=nil, overwr_options={})
|
20
|
+
options = @options.dup
|
21
|
+
options.update overwr_options
|
22
|
+
|
23
|
+
if output_file != nil then
|
24
|
+
outf = File.open output_file, 'w'
|
25
|
+
else
|
26
|
+
outf = STDOUT
|
27
|
+
end
|
28
|
+
|
29
|
+
unless psalm_files.is_a? Array
|
30
|
+
psalm_files = [ psalm_files ]
|
31
|
+
end
|
32
|
+
|
33
|
+
psalms = psalm_files.collect do |pf|
|
34
|
+
inf = open_psalm_file(pf)
|
35
|
+
if options[:input][:append] then
|
36
|
+
inf = JoinedInput.new inf, StringIO.new(options[:input][:append])
|
37
|
+
end
|
38
|
+
@reader.load_psalm(inf)
|
39
|
+
end
|
40
|
+
|
41
|
+
if options[:input][:join] then
|
42
|
+
while p = psalms.slice!(1) do
|
43
|
+
psalms[0] += p
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
if options[:input][:title] then
|
48
|
+
psalms.first.header.title = options[:input][:title] # should it be set for all instead of for the first one only?
|
49
|
+
end
|
50
|
+
|
51
|
+
outputter = get_outputter options[:general][:format]
|
52
|
+
psalms.each do |ps|
|
53
|
+
# yield the psalm before producing output to allow modification
|
54
|
+
modified = yield ps if block_given?
|
55
|
+
if modified != ps and modified.is_a?(Psalm) then
|
56
|
+
ps = modified
|
57
|
+
end
|
58
|
+
|
59
|
+
outf.puts outputter.process_psalm ps, options[:output]
|
60
|
+
end
|
61
|
+
|
62
|
+
if outf != STDOUT then
|
63
|
+
outf.close
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def open_psalm_file(fname)
|
70
|
+
if fname == '-' then
|
71
|
+
return STDIN
|
72
|
+
else
|
73
|
+
return File.open fname
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def get_outputter(format)
|
78
|
+
cls_name = format.to_s.gsub(/_(\w)/) {|m| m[1].upcase }
|
79
|
+
cls_name[0] = cls_name[0].upcase
|
80
|
+
cls_name += 'Outputter'
|
81
|
+
|
82
|
+
if Pslm.const_defined? cls_name then
|
83
|
+
return Pslm.const_get(cls_name).new
|
84
|
+
else
|
85
|
+
return nil
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# according to Liber Usualis, Solesmes cca 1962
|
2
|
+
---
|
3
|
+
I: [[2, 0], [1, 2]]
|
4
|
+
II: [[1, 0], [1, 1]]
|
5
|
+
III: [
|
6
|
+
[2, 0],
|
7
|
+
[1, {b: 1, a: 1, a2: 2, g: 2, g2: 3}]
|
8
|
+
]
|
9
|
+
IV: [
|
10
|
+
[1, 2],
|
11
|
+
[1, {g: 0, E: 3}]
|
12
|
+
]
|
13
|
+
IV alt: [
|
14
|
+
[1, 2],
|
15
|
+
[1, {c: 0, A: 3, 'A*': 3, d: 3}]
|
16
|
+
]
|
17
|
+
V: [[1, 0], [2, 0]]
|
18
|
+
VI: [[2, 0], [1, 2]]
|
19
|
+
VI alt: [[1, 1], [1, 2]]
|
20
|
+
VII: [[2, 0], [2, 0]]
|
21
|
+
VIII: [[1, 0], [1, 2]]
|
22
|
+
per: [[1, 3], [1, 1]]
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module Pslm
|
4
|
+
|
5
|
+
# reconstructs the input format
|
6
|
+
#
|
7
|
+
# an easier implementation would make use of VersePart#src, but
|
8
|
+
# this should be a simple example of full-depth output preparation
|
9
|
+
class PslmOutputter < Outputter
|
10
|
+
|
11
|
+
# doesn't respect any options; accepts them only because of a common
|
12
|
+
# outputter interface
|
13
|
+
def process(psalm, options={})
|
14
|
+
psalm_assembled = psalm.verses.collect do |verse|
|
15
|
+
|
16
|
+
verse_assembled = verse.parts.collect do |part|
|
17
|
+
|
18
|
+
part_assembled = part.words.collect do |word|
|
19
|
+
|
20
|
+
word_assembled = ''
|
21
|
+
word.syllables.each_with_index do |syll, si|
|
22
|
+
|
23
|
+
if word_assembled.size > 0 and
|
24
|
+
not word.syllables[si-1].accent? and
|
25
|
+
not syll.accent? then
|
26
|
+
word_assembled += '/'
|
27
|
+
end
|
28
|
+
word_assembled += syllable_format(syll)
|
29
|
+
end
|
30
|
+
|
31
|
+
word_format word_assembled, word
|
32
|
+
|
33
|
+
end.join ' '
|
34
|
+
part_format part_assembled, part
|
35
|
+
|
36
|
+
end.join "\n"
|
37
|
+
verse_format verse_assembled, verse
|
38
|
+
|
39
|
+
end.join "\n"
|
40
|
+
|
41
|
+
return psalm.header.title + "\n\n" + psalm_assembled
|
42
|
+
end
|
43
|
+
|
44
|
+
def syllable_format(syll)
|
45
|
+
if syll.accent? then
|
46
|
+
return '[' + syll + ']'
|
47
|
+
end
|
48
|
+
|
49
|
+
return syll
|
50
|
+
end
|
51
|
+
|
52
|
+
def word_format(text, word)
|
53
|
+
text
|
54
|
+
end
|
55
|
+
|
56
|
+
def part_format(text, part)
|
57
|
+
case part.pos
|
58
|
+
when :flex
|
59
|
+
return text + ' +'
|
60
|
+
when :first
|
61
|
+
return text + ' *'
|
62
|
+
else
|
63
|
+
return text
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def verse_format(text, verse)
|
68
|
+
text
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|