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.
@@ -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
@@ -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