rrtf 0.1.1
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/.byebug_history +4 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +45 -0
- data/README.md +110 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/01.rtf +45 -0
- data/examples/01_mac_libreoffice5_2_3_3.png +0 -0
- data/examples/01_mac_pages6_2.png +0 -0
- data/examples/01_mac_textedit1_12.png +0 -0
- data/examples/01_mac_word15_36.png +0 -0
- data/examples/01_styles_and_paragraphs.rb +33 -0
- data/examples/resources/json/redshirt_styles.json +49 -0
- data/lib/rrtf/colour.rb +182 -0
- data/lib/rrtf/converters/html.rb +123 -0
- data/lib/rrtf/converters.rb +5 -0
- data/lib/rrtf/font.rb +182 -0
- data/lib/rrtf/information.rb +110 -0
- data/lib/rrtf/list.rb +219 -0
- data/lib/rrtf/node.rb +1932 -0
- data/lib/rrtf/paper.rb +53 -0
- data/lib/rrtf/style/character_style.rb +68 -0
- data/lib/rrtf/style/document_style.rb +116 -0
- data/lib/rrtf/style/formatting.rb +276 -0
- data/lib/rrtf/style/paragraph_style.rb +79 -0
- data/lib/rrtf/style/style.rb +101 -0
- data/lib/rrtf/style.rb +8 -0
- data/lib/rrtf/stylesheet.rb +202 -0
- data/lib/rrtf/version.rb +3 -0
- data/lib/rrtf.rb +27 -0
- data/rrtf.gemspec +30 -0
- metadata +163 -0
data/lib/rrtf/font.rb
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
module RRTF
|
4
|
+
# This class represents a font for use with some RTF content.
|
5
|
+
class Font
|
6
|
+
# A declaration for a font family. This family is used for monospaced
|
7
|
+
# fonts (e.g. Courier New).
|
8
|
+
MODERN = :modern
|
9
|
+
|
10
|
+
# A declaration for a font family. This family is used for proportionally
|
11
|
+
# spaced serif fonts (e.g. Arial, Times New Roman).
|
12
|
+
ROMAN = :roman
|
13
|
+
|
14
|
+
# A declaration for a font family. This family is used for proportionally
|
15
|
+
# spaced sans serif fonts (e.g. Tahoma, Lucida Sans).
|
16
|
+
SWISS = :swiss
|
17
|
+
|
18
|
+
# A declaration for a font family. This family is used where none of the
|
19
|
+
# other families apply.
|
20
|
+
NIL = 'nil'.intern
|
21
|
+
|
22
|
+
|
23
|
+
# Attribute accessor.
|
24
|
+
attr_reader :family, :name
|
25
|
+
|
26
|
+
|
27
|
+
# Format: "<FAMILY_CONSTANT>:<Name>"
|
28
|
+
def self.from_string(str)
|
29
|
+
if str =~ /([a-z]+):(.+)/i
|
30
|
+
parts = str.split(':')
|
31
|
+
self.new(RRTF::Utilities.constantize("RRTF::Font::#{parts.first}"), parts.last)
|
32
|
+
else
|
33
|
+
RTFError.fire("Unreconized string font format '#{str}'.")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# This is the constructor for the Font class.
|
39
|
+
#
|
40
|
+
# ==== Parameters
|
41
|
+
# family:: The font family for the new font. This should be one of
|
42
|
+
# Font::MODERN, Font::ROMAN, Font::SWISS or
|
43
|
+
# Font::NIL.
|
44
|
+
# name:: A string containing the font name.
|
45
|
+
#
|
46
|
+
# ==== Exceptions
|
47
|
+
# RTFError:: Generated whenever an invalid font family is specified.
|
48
|
+
def initialize(family, name)
|
49
|
+
# Check that a valid family has been provided.
|
50
|
+
if ![MODERN, ROMAN, SWISS, NIL].include?(family)
|
51
|
+
RTFError::fire("Unknown font family specified for Font object.")
|
52
|
+
end
|
53
|
+
@family = family
|
54
|
+
@name = name
|
55
|
+
end
|
56
|
+
|
57
|
+
# This method overloads the equivalence test operator for the Font class
|
58
|
+
# to allow for Font comparisons.
|
59
|
+
#
|
60
|
+
# ==== Parameters
|
61
|
+
# object:: A reference to the object to be compared with.
|
62
|
+
def ==(object)
|
63
|
+
object.instance_of?(Font) &&
|
64
|
+
object.family == @family &&
|
65
|
+
object.name == @name
|
66
|
+
end
|
67
|
+
|
68
|
+
# This method fetches a textual description for a Font object.
|
69
|
+
#
|
70
|
+
# ==== Parameters
|
71
|
+
# indent:: The number of spaces to prefix to the lines generated by the
|
72
|
+
# method. Defaults to zero.
|
73
|
+
def to_s(indent=0)
|
74
|
+
prefix = indent > 0 ? ' ' * indent : ''
|
75
|
+
"#{prefix}Family: #{@family.id2name}, Name: #{@name}"
|
76
|
+
end
|
77
|
+
|
78
|
+
# This method generates the RTF representation for a Font object as it
|
79
|
+
# would appear within a document font table.
|
80
|
+
#
|
81
|
+
# ==== Parameters
|
82
|
+
# indent:: The number of spaces to prefix to the lines generated by the
|
83
|
+
# method. Defaults to zero.
|
84
|
+
def to_rtf(indent=0)
|
85
|
+
prefix = indent > 0 ? ' ' * indent : ''
|
86
|
+
"#{prefix}\\f#{@family.id2name} #{@name};"
|
87
|
+
end
|
88
|
+
end # End of the Font class.
|
89
|
+
|
90
|
+
|
91
|
+
# This class represents the font table for an RTF document. An instance of
|
92
|
+
# the class is used internally by the Document class and should not need to
|
93
|
+
# be explicitly instantiated (although it can be obtained from a Document
|
94
|
+
# object if needed).
|
95
|
+
class FontTable
|
96
|
+
# This is the constructor for the RTFTable class.
|
97
|
+
#
|
98
|
+
# ==== Parameters
|
99
|
+
# *fonts:: Zero or more font objects that are to be added to the font
|
100
|
+
# table. Objects that are not Fonts will be ignored.
|
101
|
+
def initialize(*fonts)
|
102
|
+
@fonts = []
|
103
|
+
fonts.each {|font| add(font)}
|
104
|
+
end
|
105
|
+
|
106
|
+
# This method is used to retrieve a count of the number of fonts held
|
107
|
+
# within an instance of the FontTable class.
|
108
|
+
def size
|
109
|
+
@fonts.size
|
110
|
+
end
|
111
|
+
|
112
|
+
# This method adds a font to a FontTable instance. This method returns
|
113
|
+
# a reference to the FontTable object updated.
|
114
|
+
#
|
115
|
+
# ==== Parameters
|
116
|
+
# font:: A reference to the font to be added. If this is not a Font
|
117
|
+
# object or already exists in the table it will be ignored.
|
118
|
+
def add(font)
|
119
|
+
if font.instance_of?(Font)
|
120
|
+
@fonts.push(font) if @fonts.index(font).nil?
|
121
|
+
end
|
122
|
+
self
|
123
|
+
end
|
124
|
+
|
125
|
+
# This method iterates over the contents of a FontTable object. This
|
126
|
+
# method expects a block that takes a single parameter (the next font
|
127
|
+
# from the table).
|
128
|
+
def each
|
129
|
+
@fonts.each {|font| yield font} if block_given?
|
130
|
+
end
|
131
|
+
|
132
|
+
# This method overloads the array dereference operator for the FontTable
|
133
|
+
# class.
|
134
|
+
#
|
135
|
+
# ==== Parameters
|
136
|
+
# index:: The index into the font table of the font to be retrieved. If
|
137
|
+
# the index is invalid then nil is returned.
|
138
|
+
def [](index)
|
139
|
+
@fonts[index]
|
140
|
+
end
|
141
|
+
|
142
|
+
# This method fetches the index of a font within a FontTable object. If
|
143
|
+
# the font does not exist in the table then nil is returned.
|
144
|
+
#
|
145
|
+
# ==== Parameters
|
146
|
+
# font:: A reference to the font to check for.
|
147
|
+
def index(font)
|
148
|
+
@fonts.index(font)
|
149
|
+
end
|
150
|
+
|
151
|
+
# This method generates a textual description for a FontTable object.
|
152
|
+
#
|
153
|
+
# ==== Parameters
|
154
|
+
# indent:: The number of spaces to prefix to the lines generated by the
|
155
|
+
# method. Defaults to zero.
|
156
|
+
def to_s(indent=0)
|
157
|
+
prefix = indent > 0 ? ' ' * indent : ''
|
158
|
+
text = StringIO.new
|
159
|
+
text << "#{prefix}Font Table (#{@fonts.size} fonts)"
|
160
|
+
@fonts.each {|font| text << "\n#{prefix} #{font}"}
|
161
|
+
text.string
|
162
|
+
end
|
163
|
+
|
164
|
+
# This method generates the RTF text for a FontTable object.
|
165
|
+
#
|
166
|
+
# ==== Parameters
|
167
|
+
# indent:: The number of spaces to prefix to the lines generated by the
|
168
|
+
# method. Defaults to zero.
|
169
|
+
def to_rtf(indent=0)
|
170
|
+
prefix = indent > 0 ? ' ' * indent : ''
|
171
|
+
text = StringIO.new
|
172
|
+
text << "#{prefix}{\\fonttbl"
|
173
|
+
@fonts.each_index do |index|
|
174
|
+
text << "\n#{prefix}{\\f#{index}#{@fonts[index].to_rtf}}"
|
175
|
+
end
|
176
|
+
text << "\n#{prefix}}"
|
177
|
+
text.string
|
178
|
+
end
|
179
|
+
|
180
|
+
alias << add
|
181
|
+
end # End of the FontTable class.
|
182
|
+
end # End of the RTF module.
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
module RRTF
|
5
|
+
# This class represents an information group for a RTF document.
|
6
|
+
class Information
|
7
|
+
# Attribute accessor.
|
8
|
+
attr_reader :title, :author, :company, :created, :comments
|
9
|
+
|
10
|
+
# Attribute mutator.
|
11
|
+
attr_writer :title, :author, :company, :comments
|
12
|
+
|
13
|
+
|
14
|
+
# This is the constructor for the Information class.
|
15
|
+
#
|
16
|
+
# ==== Parameters
|
17
|
+
# title:: A string containing the document title information. Defaults
|
18
|
+
# to nil.
|
19
|
+
# author:: A string containing the document author information.
|
20
|
+
# Defaults to nil.
|
21
|
+
# company:: A string containing the company name information. Defaults
|
22
|
+
# to nil.
|
23
|
+
# comments:: A string containing the information comments. Defaults to
|
24
|
+
# nil to indicate no comments.
|
25
|
+
# creation:: A Time object or a String that can be parsed into a Time
|
26
|
+
# object (using ParseDate) indicating the document creation
|
27
|
+
# date and time. Defaults to nil to indicate the current
|
28
|
+
# date and time.
|
29
|
+
#
|
30
|
+
# ==== Exceptions
|
31
|
+
# RTFError:: Generated whenever invalid creation date/time details are
|
32
|
+
# specified.
|
33
|
+
def initialize(title=nil, author=nil, company=nil, comments=nil, creation=nil)
|
34
|
+
@title = title
|
35
|
+
@author = author
|
36
|
+
@company = company
|
37
|
+
@comments = comments
|
38
|
+
self.created = (creation == nil ? Time.new : creation)
|
39
|
+
end
|
40
|
+
|
41
|
+
# This method provides the created attribute mutator for the Information
|
42
|
+
# class.
|
43
|
+
#
|
44
|
+
# ==== Parameters
|
45
|
+
# setting:: The new creation date/time setting for the object. This
|
46
|
+
# should be either a Time object or a string containing
|
47
|
+
# date/time details that can be parsed into a Time object
|
48
|
+
# (using the parsedate method).
|
49
|
+
#
|
50
|
+
# ==== Exceptions
|
51
|
+
# RTFError:: Generated whenever invalid creation date/time details are
|
52
|
+
# specified.
|
53
|
+
def created=(setting)
|
54
|
+
if setting.instance_of?(Time)
|
55
|
+
@created = setting
|
56
|
+
else
|
57
|
+
datetime = Date._parse(setting.to_s).values_at(:year, :mon, :mday, :hour, :min, :sec, :zone, :wday)
|
58
|
+
if datetime == nil
|
59
|
+
RTFError.fire("Invalid document creation date/time information "\
|
60
|
+
"specified.")
|
61
|
+
end
|
62
|
+
@created = Time.local(datetime[0], datetime[1], datetime[2],
|
63
|
+
datetime[3], datetime[4], datetime[5])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# This method creates a textual description for an Information object.
|
68
|
+
#
|
69
|
+
# ==== Parameters
|
70
|
+
# indent:: The number of spaces to prefix to the lines generated by the
|
71
|
+
# method. Defaults to zero.
|
72
|
+
def to_s(indent=0)
|
73
|
+
prefix = indent > 0 ? ' ' * indent : ''
|
74
|
+
text = StringIO.new
|
75
|
+
|
76
|
+
text << "#{prefix}Information"
|
77
|
+
text << "\n#{prefix} Title: #{@title}" unless @title.nil?
|
78
|
+
text << "\n#{prefix} Author: #{@author}" unless @author.nil?
|
79
|
+
text << "\n#{prefix} Company: #{@company}" unless @company.nil?
|
80
|
+
text << "\n#{prefix} Comments: #{@comments}" unless @comments.nil?
|
81
|
+
text << "\n#{prefix} Created: #{@created}" unless @created.nil?
|
82
|
+
|
83
|
+
text.string
|
84
|
+
end
|
85
|
+
|
86
|
+
# This method generates the RTF text for an Information object.
|
87
|
+
#
|
88
|
+
# ==== Parameters
|
89
|
+
# indent:: The number of spaces to prefix to the lines generated by the
|
90
|
+
# method. Defaults to zero.
|
91
|
+
def to_rtf(indent=0)
|
92
|
+
prefix = indent > 0 ? ' ' * indent : ''
|
93
|
+
text = StringIO.new
|
94
|
+
|
95
|
+
text << "#{prefix}{\\info"
|
96
|
+
text << "\n#{prefix}{\\title #{@title}}" unless @title.nil?
|
97
|
+
text << "\n#{prefix}{\\author #{@author}}" unless @author.nil?
|
98
|
+
text << "\n#{prefix}{\\company #{@company}}" unless @company.nil?
|
99
|
+
text << "\n#{prefix}{\\doccomm #{@comments}}" unless @comments.nil?
|
100
|
+
unless @created.nil?
|
101
|
+
text << "\n#{prefix}{\\createim\\yr#{@created.year}"
|
102
|
+
text << "\\mo#{@created.month}\\dy#{@created.day}"
|
103
|
+
text << "\\hr#{@created.hour}\\min#{@created.min}}"
|
104
|
+
end
|
105
|
+
text << "\n#{prefix}}"
|
106
|
+
|
107
|
+
text.string
|
108
|
+
end
|
109
|
+
end # End of the Information class.
|
110
|
+
end # End of the RTF module.
|
data/lib/rrtf/list.rb
ADDED
@@ -0,0 +1,219 @@
|
|
1
|
+
module RRTF
|
2
|
+
class ListTable
|
3
|
+
def initialize
|
4
|
+
@templates = []
|
5
|
+
end
|
6
|
+
|
7
|
+
def new_template
|
8
|
+
@templates.push ListTemplate.new(next_template_id)
|
9
|
+
@templates.last
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_rtf(indent=0)
|
13
|
+
return '' if @templates.empty?
|
14
|
+
|
15
|
+
prefix = indent > 0 ? ' ' * indent : ''
|
16
|
+
|
17
|
+
# List table
|
18
|
+
text = "#{prefix}{\\*\\listtable"
|
19
|
+
@templates.each {|tpl| text << tpl.to_rtf}
|
20
|
+
text << "}"
|
21
|
+
|
22
|
+
# List override table, a Cargo Cult.
|
23
|
+
text << "#{prefix}{\\*\\listoverridetable"
|
24
|
+
@templates.each do |tpl|
|
25
|
+
text << "{\\listoverride\\listid#{tpl.id}\\listoverridecount0\\ls#{tpl.id}}"
|
26
|
+
end
|
27
|
+
text << "}\n"
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
def next_template_id
|
32
|
+
@templates.size + 1
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
class ListMarker
|
38
|
+
def initialize(name, codepoint=nil)
|
39
|
+
@name = name
|
40
|
+
@codepoint = codepoint
|
41
|
+
end
|
42
|
+
|
43
|
+
def bullet?
|
44
|
+
!@codepoint.nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
def type
|
48
|
+
bullet? ? :bullet : :decimal
|
49
|
+
end
|
50
|
+
|
51
|
+
def number_type
|
52
|
+
# 23: bullet, 0: arabic
|
53
|
+
# applies to the \levelnfcN macro
|
54
|
+
#
|
55
|
+
bullet? ? 23 : 0
|
56
|
+
end
|
57
|
+
|
58
|
+
def name
|
59
|
+
name = "\\{#@name\\}"
|
60
|
+
name << '.' unless bullet?
|
61
|
+
name
|
62
|
+
end
|
63
|
+
|
64
|
+
def template_format
|
65
|
+
# The first char is the string size, the next ones are
|
66
|
+
# either placeholders (\'0X) or actual characters to
|
67
|
+
# include in the format. In the bullet case, \uc0 is
|
68
|
+
# used to get rid of the multibyte translation: we want
|
69
|
+
# an Unicode character.
|
70
|
+
#
|
71
|
+
# In the decimal case, we have a fixed format, with a
|
72
|
+
# dot following the actual number.
|
73
|
+
#
|
74
|
+
if bullet?
|
75
|
+
"\\'01\\uc0\\u#@codepoint"
|
76
|
+
else
|
77
|
+
"\\'02\\'00. "
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def text_format(n=nil)
|
82
|
+
text =
|
83
|
+
if bullet?
|
84
|
+
"\\uc0\\u#@codepoint"
|
85
|
+
else
|
86
|
+
"#{n}."
|
87
|
+
end
|
88
|
+
|
89
|
+
"\t#{text}\t"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class ListTemplate
|
94
|
+
attr_reader :id
|
95
|
+
|
96
|
+
Markers = {
|
97
|
+
:disc => ListMarker.new('disc', 0x2022),
|
98
|
+
:hyphen => ListMarker.new('hyphen', 0x2043),
|
99
|
+
:decimal => ListMarker.new('decimal' )
|
100
|
+
}
|
101
|
+
|
102
|
+
def initialize(id)
|
103
|
+
@levels = []
|
104
|
+
@id = id
|
105
|
+
end
|
106
|
+
|
107
|
+
def level_for(level, kind = :bullets)
|
108
|
+
@levels[level-1] ||= begin
|
109
|
+
# Only disc for now: we'll add support
|
110
|
+
# for more customization options later
|
111
|
+
marker = Markers[kind == :bullets ? :disc : :decimal]
|
112
|
+
ListLevel.new(self, marker, level)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def to_rtf(indent=0)
|
117
|
+
prefix = indent > 0 ? ' ' * indent : ''
|
118
|
+
|
119
|
+
text = "#{prefix}{\\list\\listtemplate#{id}\\listhybrid"
|
120
|
+
@levels.each {|lvl| text << lvl.to_rtf}
|
121
|
+
text << "{\\listname;}\\listid#{id}}\n"
|
122
|
+
|
123
|
+
text
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class ListLevel
|
128
|
+
ValidLevels = (1..9)
|
129
|
+
|
130
|
+
LevelTabs = [
|
131
|
+
220, 720, 1133, 1700, 2267,
|
132
|
+
2834, 3401, 3968, 4535, 5102,
|
133
|
+
5669, 6236, 6803
|
134
|
+
].freeze
|
135
|
+
|
136
|
+
ResetTabs = [560].concat(LevelTabs[2..-1]).freeze
|
137
|
+
|
138
|
+
attr_reader :level, :marker
|
139
|
+
|
140
|
+
def initialize(template, marker, level)
|
141
|
+
unless marker.kind_of? ListMarker
|
142
|
+
RTFError.fire("Invalid marker #{marker.inspect}")
|
143
|
+
end
|
144
|
+
|
145
|
+
unless ValidLevels.include? level
|
146
|
+
RTFError.fire("Invalid list level: #{level}")
|
147
|
+
end
|
148
|
+
|
149
|
+
@template = template
|
150
|
+
@level = level
|
151
|
+
@marker = marker
|
152
|
+
end
|
153
|
+
|
154
|
+
def type
|
155
|
+
@marker.type
|
156
|
+
end
|
157
|
+
|
158
|
+
def reset_tabs
|
159
|
+
ResetTabs
|
160
|
+
end
|
161
|
+
|
162
|
+
def tabs
|
163
|
+
@tabs ||= begin
|
164
|
+
tabs = LevelTabs.dup # Kernel#tap would be prettier here
|
165
|
+
|
166
|
+
(@level - 1).times do
|
167
|
+
# Reverse-engineered while looking at Textedit.app
|
168
|
+
# generated output: they already made sure that it
|
169
|
+
# would look good on every RTF editor :-p
|
170
|
+
#
|
171
|
+
a, = tabs.shift(3)
|
172
|
+
a,b = a + 720, a + 1220
|
173
|
+
tabs.shift while tabs.first < b
|
174
|
+
tabs.unshift a, b
|
175
|
+
end
|
176
|
+
|
177
|
+
tabs
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def id
|
182
|
+
@id ||= @template.id * 10 + level
|
183
|
+
end
|
184
|
+
|
185
|
+
def indent
|
186
|
+
@indent ||= level * 720
|
187
|
+
end
|
188
|
+
|
189
|
+
def to_rtf(indent=0)
|
190
|
+
prefix = indent > 0 ? ' ' * indent : ''
|
191
|
+
|
192
|
+
text = "#{prefix}{\\listlevel\\levelstartat1"
|
193
|
+
|
194
|
+
# Marker type. The first declaration is for Backward Compatibility (BC).
|
195
|
+
nfc = @marker.number_type
|
196
|
+
text << "\\levelnfc#{nfc}\\levelnfcn#{nfc}"
|
197
|
+
|
198
|
+
# Justification, currently only left justified (0). First decl for BC.
|
199
|
+
text << '\leveljc0\leveljcn0'
|
200
|
+
|
201
|
+
# Character that follows the level text, currently only TAB.
|
202
|
+
text << '\levelfollow0'
|
203
|
+
|
204
|
+
# BC: Minimum distance from the left & right edges.
|
205
|
+
text << '\levelindent0\levelspace360'
|
206
|
+
|
207
|
+
# Marker name
|
208
|
+
text << "{\\*\\levelmarker #{@marker.name}}"
|
209
|
+
|
210
|
+
# Marker text format
|
211
|
+
text << "{\\leveltext\\leveltemplateid#{id}#{@marker.template_format};}"
|
212
|
+
text << '{\levelnumbers;}'
|
213
|
+
|
214
|
+
# The actual spacing
|
215
|
+
text << "\\fi-360\\li#{self.indent}\\lin#{self.indent}}\n"
|
216
|
+
end
|
217
|
+
|
218
|
+
end
|
219
|
+
end
|