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