ruby-rtf 0.0.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.
@@ -0,0 +1,7 @@
1
+ module RubyRTF
2
+ module_function
3
+
4
+ def twips_to_points(twips)
5
+ twips / 20.0
6
+ end
7
+ end
@@ -0,0 +1,72 @@
1
+ module RubyRTF
2
+ class Table
3
+ attr_accessor :rows, :half_gap, :left_margin
4
+
5
+ def initialize
6
+ @left_margin = 0
7
+ @half_gap = 0
8
+
9
+ @rows = []
10
+ add_row
11
+ end
12
+
13
+ def current_row
14
+ @rows.last
15
+ end
16
+
17
+ def add_row
18
+ @rows << RubyRTF::Table::Row.new(self)
19
+ @rows.last
20
+ end
21
+
22
+ class Row
23
+ attr_accessor :table, :end_positions, :cells
24
+
25
+ def initialize(table)
26
+ @table = table
27
+ @end_positions = []
28
+
29
+ @cells = [RubyRTF::Table::Row::Cell.new(self, 0)]
30
+ end
31
+
32
+ def current_cell
33
+ @cells.last
34
+ end
35
+
36
+ def add_cell
37
+ return @cells.last if @cells.last.sections.empty?
38
+
39
+ @cells << RubyRTF::Table::Row::Cell.new(self, @cells.length)
40
+ @cells.last
41
+ end
42
+
43
+ class Cell
44
+ attr_accessor :sections, :row, :idx
45
+
46
+ def initialize(row, idx)
47
+ @row = row
48
+ @idx = idx
49
+ @sections = []
50
+ end
51
+
52
+ def <<(obj)
53
+ @sections << obj
54
+ end
55
+
56
+ def table
57
+ row.table
58
+ end
59
+
60
+ def width
61
+ gap = row.table.half_gap
62
+ left_margin = row.table.left_margin
63
+
64
+ end_pos = row.end_positions[idx]
65
+ prev_pos = idx == 0 ? 0 : row.end_positions[idx - 1]
66
+
67
+ ((end_pos - prev_pos - (2 * gap) - left_margin) / row.end_positions[-1]) * 100
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,5 @@
1
+ # Main namespace of the RTF parser
2
+ module RubyRTF
3
+ # Current library version
4
+ VERSION = '0.0.1'
5
+ end
data/ruby-rtf.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ $: << "./lib"
2
+ require 'ruby-rtf/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'ruby-rtf'
6
+
7
+ s.version = RubyRTF::VERSION
8
+
9
+ s.authors = 'dan sinclair'
10
+ s.email = 'dj2@everburning.com'
11
+
12
+ s.homepage = 'http://github.com/dj2/ruby-rtf'
13
+ s.summary = 'Library for working with RTF files'
14
+ s.description = s.summary
15
+
16
+ s.add_development_dependency 'infinity_test'
17
+ s.add_development_dependency 'rspec', '>2.0'
18
+ s.add_development_dependency 'metric_fu'
19
+
20
+ s.add_development_dependency 'yard'
21
+ s.add_development_dependency 'bluecloth'
22
+
23
+ s.bindir = 'bin'
24
+ s.executables << 'rtf_parse'
25
+
26
+ s.files = `git ls-files`.split("\n")
27
+ s.test_files = `git ls-files -- spec/*`.split("\n")
28
+
29
+ s.require_paths = ['lib']
30
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ describe RubyRTF::Colour do
4
+ it 'also responds to Color' do
5
+ lambda { RubyRTF::Color.new }.should_not raise_error
6
+ end
7
+
8
+ it 'returns the rgb when to_s is called' do
9
+ c = RubyRTF::Colour.new(255, 200, 199)
10
+ c.to_s.should == '[255, 200, 199]'
11
+ end
12
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe RubyRTF::Document do
4
+ it 'provides a font table' do
5
+ doc = RubyRTF::Document.new
6
+ table = nil
7
+ lambda { table = doc.font_table }.should_not raise_error
8
+ table.should_not be_nil
9
+ end
10
+
11
+ context 'colour table' do
12
+ it 'provides a colour table' do
13
+ doc = RubyRTF::Document.new
14
+ tbl = nil
15
+ lambda { tbl = doc.colour_table }.should_not raise_error
16
+ tbl.should_not be_nil
17
+ end
18
+
19
+ it 'provdies access as color table' do
20
+ doc = RubyRTF::Document.new
21
+ tbl = nil
22
+ lambda { tbl = doc.color_table }.should_not raise_error
23
+ tbl.should == doc.colour_table
24
+ end
25
+ end
26
+
27
+ it 'provides a stylesheet'
28
+
29
+ context 'defaults to' do
30
+ it 'character set ansi' do
31
+ RubyRTF::Document.new.character_set.should == :ansi
32
+ end
33
+
34
+ it 'font 0' do
35
+ RubyRTF::Document.new.default_font.should == 0
36
+ end
37
+ end
38
+ end
data/spec/font_spec.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe RubyRTF::Font do
4
+ let(:font) { RubyRTF::Font.new }
5
+
6
+ it 'has a name' do
7
+ font.name = 'Arial'
8
+ font.name.should == 'Arial'
9
+ end
10
+
11
+ it 'has a command' do
12
+ font.family_command = :swiss
13
+ font.family_command.should == :swiss
14
+ end
15
+ end
@@ -0,0 +1,926 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe RubyRTF::Parser do
6
+ let(:parser) { RubyRTF::Parser.new }
7
+ let(:doc) { parser.doc }
8
+
9
+ it 'parses hello world' do
10
+ src = '{\rtf1\ansi\deff0 {\fonttbl {\f0 Times New Roman;}}\f0 \fs60 Hello, World!}'
11
+ lambda { parser.parse(src) }.should_not raise_error
12
+ end
13
+
14
+ it 'returns a RTF::Document' do
15
+ src = '{\rtf1\ansi\deff0 {\fonttbl {\f0 Times New Roman;}}\f0 \fs60 Hello, World!}'
16
+ d = parser.parse(src)
17
+ d.is_a?(RubyRTF::Document).should be_true
18
+ end
19
+
20
+ it 'parses a default font (\deffN)' do
21
+ src = '{\rtf1\ansi\deff10 {\fonttbl {\f10 Times New Roman;}}\f0 \fs60 Hello, World!}'
22
+ d = parser.parse(src)
23
+ d.default_font.should == 10
24
+ end
25
+
26
+ context 'invalid document' do
27
+ it 'raises exception if \rtf is missing' do
28
+ src = '{\ansi\deff0 {\fonttbl {\f0 Times New Roman;}}\f0 \fs60 Hello, World!}'
29
+ lambda { parser.parse(src) }.should raise_error(RubyRTF::InvalidDocument)
30
+ end
31
+
32
+ it 'raises exception if the document does not start with \rtf' do
33
+ src = '{\ansi\deff0\rtf1 {\fonttbl {\f0 Times New Roman;}}\f0 \fs60 Hello, World!}'
34
+ lambda { parser.parse(src) }.should raise_error(RubyRTF::InvalidDocument)
35
+ end
36
+
37
+ it 'raises exception if the {}s are unbalanced' do
38
+ src = '{\rtf1\ansi\deff0 {\fonttbl {\f0 Times New Roman;}\f0 \fs60 Hello, World!}'
39
+ lambda { parser.parse(src) }.should raise_error(RubyRTF::InvalidDocument)
40
+ end
41
+ end
42
+
43
+ context '#parse' do
44
+ it 'parses text into the current section' do
45
+ src = '{\rtf1\ansi\deff10 {\fonttbl {\f10 Times New Roman;}}\f0 \fs60 Hello, World!}'
46
+ d = parser.parse(src)
47
+ d.sections.first[:text].should == 'Hello, World!'
48
+ end
49
+
50
+ it 'adds a new section on {' do
51
+ src = '{\rtf1 \fs60 Hello {\fs30 World}}'
52
+ d = parser.parse(src)
53
+ d.sections.first[:modifiers][:font_size].should == 30
54
+ d.sections.first[:text].should == 'Hello '
55
+
56
+ d.sections.last[:modifiers][:font_size].should == 15
57
+ d.sections.last[:text].should == 'World'
58
+ end
59
+
60
+ it 'adds a new section on }' do
61
+ src = '{\rtf1 \fs60 Hello {\fs30 World}\fs12 Goodbye, cruel world.}'
62
+
63
+ section = parser.parse(src).sections
64
+ section[0][:modifiers][:font_size].should == 30
65
+ section[0][:text].should == 'Hello '
66
+
67
+ section[1][:modifiers][:font_size].should == 15
68
+ section[1][:text].should == 'World'
69
+
70
+ section[2][:modifiers][:font_size].should == 6
71
+ section[2][:text].should == 'Goodbye, cruel world.'
72
+ end
73
+
74
+ it 'inherits properly over {} groups' do
75
+ src = '{\rtf1 \b\fs60 Hello {\i\fs30 World}\ul Goodbye, cruel world.}'
76
+
77
+ section = parser.parse(src).sections
78
+ section[0][:modifiers][:font_size].should == 30
79
+ section[0][:modifiers][:bold].should be_true
80
+ section[0][:modifiers].has_key?(:italic).should be_false
81
+ section[0][:modifiers].has_key?(:underline).should be_false
82
+ section[0][:text].should == 'Hello '
83
+
84
+ section[1][:modifiers][:font_size].should == 15
85
+ section[1][:modifiers][:italic].should be_true
86
+ section[1][:modifiers][:bold].should be_true
87
+ section[1][:modifiers].has_key?(:underline).should be_false
88
+ section[1][:text].should == 'World'
89
+
90
+ section[2][:modifiers][:font_size].should == 30
91
+ section[2][:modifiers][:bold].should be_true
92
+ section[2][:modifiers][:underline].should be_true
93
+ section[2][:modifiers].has_key?(:italic).should be_false
94
+ section[2][:text].should == 'Goodbye, cruel world.'
95
+ end
96
+ end
97
+
98
+ context '#parse_control' do
99
+ it 'parses a normal control' do
100
+ parser.parse_control("rtf")[0, 2].should == [:rtf, nil]
101
+ end
102
+
103
+ it 'parses a control with a value' do
104
+ parser.parse_control("f2")[0, 2].should == [:f, 2]
105
+ end
106
+
107
+ context 'unicode' do
108
+ %w(u21487* u21487).each do |code|
109
+ it "parses #{code}" do
110
+ parser.parse_control(code)[0, 2].should == [:u, 21487]
111
+ end
112
+ end
113
+
114
+ %w(u-21487* u-21487).each do |code|
115
+ it "parses #{code}" do
116
+ parser.parse_control(code)[0, 2].should == [:u, -21487]
117
+ end
118
+ end
119
+ end
120
+
121
+ it 'parses a hex control' do
122
+ parser.parse_control("'7e")[0, 2].should == [:hex, '~']
123
+ end
124
+
125
+ it 'parses a hex control with a string after it' do
126
+ ctrl, val, current_pos = parser.parse_control("'7e25")
127
+ ctrl.should == :hex
128
+ val.should == '~'
129
+ current_pos.should == 3
130
+ end
131
+
132
+ [' ', '{', '}', '\\', "\r", "\n"].each do |stop|
133
+ it "stops at a #{stop}" do
134
+ parser.parse_control("rtf#{stop}test")[0, 2].should == [:rtf, nil]
135
+ end
136
+ end
137
+
138
+ it 'handles a non-zero current position' do
139
+ parser.parse_control('Test ansi test', 5)[0, 2].should == [:ansi, nil]
140
+ end
141
+
142
+ it 'advances the current positon' do
143
+ parser.parse_control('Test ansi{test', 5).last.should == 9
144
+ end
145
+
146
+ it 'advances the current positon past the optional space' do
147
+ parser.parse_control('Test ansi test', 5).last.should == 10
148
+ end
149
+ end
150
+
151
+ context 'character set' do
152
+ %w(ansi mac pc pca).each do |type|
153
+ it "accepts #{type}" do
154
+ src = "{\\rtf1\\#{type}\\deff0 {\\fonttbl {\\f0 Times New Roman;}}\\f0 \\fs60 Hello, World!}"
155
+ doc = parser.parse(src)
156
+ doc.character_set.should == type.to_sym
157
+ end
158
+ end
159
+ end
160
+
161
+ context 'font table' do
162
+ it 'sets the font table into the document' do
163
+ src = '{\rtf1{\fonttbl{\f0\froman Times;}{\f1\fnil Arial;}}}'
164
+ doc = parser.parse(src)
165
+
166
+ font = doc.font_table[0]
167
+ font.family_command.should == :roman
168
+ font.name.should == 'Times'
169
+ end
170
+
171
+ context '#parse_font_table' do
172
+ it 'parses a font table' do
173
+ src = '{\f0\froman Times New Roman;}{\f1\fnil Arial;}}}'
174
+ parser.parse_font_table(src, 0)
175
+ tbl = doc.font_table
176
+
177
+ tbl.length.should == 2
178
+ tbl[0].family_command.should == :roman
179
+ tbl[0].name.should == 'Times New Roman'
180
+
181
+ tbl[1].family_command.should == :nil
182
+ tbl[1].name.should == 'Arial'
183
+ end
184
+
185
+ it 'parses a font table without braces' do
186
+ src = '\f0\froman\fcharset0 TimesNewRomanPSMT;}}'
187
+ parser.parse_font_table(src, 0)
188
+ tbl = doc.font_table
189
+ tbl[0].name.should == 'TimesNewRomanPSMT'
190
+ end
191
+
192
+ it 'handles \r and \n in the font table' do
193
+ src = "{\\f0\\froman Times New Roman;}\r{\\f1\\fnil Arial;}\n}}"
194
+ parser.parse_font_table(src, 0)
195
+ tbl = doc.font_table
196
+
197
+ tbl.length.should == 2
198
+ tbl[0].family_command.should == :roman
199
+ tbl[0].name.should == 'Times New Roman'
200
+
201
+ tbl[1].family_command.should == :nil
202
+ tbl[1].name.should == 'Arial'
203
+ end
204
+
205
+ it 'the family command is optional' do
206
+ src = '{\f0 Times New Roman;}}}'
207
+ parser.parse_font_table(src, 0)
208
+ tbl = doc.font_table
209
+ tbl[0].family_command.should == :nil
210
+ tbl[0].name.should == 'Times New Roman'
211
+ end
212
+
213
+ it 'does not require the numbering to be incremental' do
214
+ src = '{\f77\froman Times New Roman;}{\f3\fnil Arial;}}}'
215
+ parser.parse_font_table(src, 0)
216
+ tbl = doc.font_table
217
+
218
+ tbl[77].family_command.should == :roman
219
+ tbl[77].name.should == 'Times New Roman'
220
+
221
+ tbl[3].family_command.should == :nil
222
+ tbl[3].name.should == 'Arial'
223
+ end
224
+
225
+ it 'accepts the \falt command' do
226
+ src = '{\f0\froman Times New Roman{\*\falt Courier New};}}'
227
+ parser.parse_font_table(src, 0)
228
+ tbl = doc.font_table
229
+ tbl[0].name.should == 'Times New Roman'
230
+ tbl[0].alternate_name.should == 'Courier New'
231
+ end
232
+
233
+ it 'sets current pos to the closing }' do
234
+ src = '{\f0\froman Times New Roman{\*\falt Courier New};}}'
235
+ parser.parse_font_table(src, 0).should == (src.length - 1)
236
+ end
237
+
238
+ it 'accepts the panose command' do
239
+ src = '{\f0\froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman{\*\falt Courier New};}}'
240
+ parser.parse_font_table(src, 0)
241
+ tbl = doc.font_table
242
+ tbl[0].panose.should == '02020603050405020304'
243
+ tbl[0].name.should == 'Times New Roman'
244
+ tbl[0].alternate_name.should == 'Courier New'
245
+ end
246
+
247
+ %w(flomajor fhimajor fdbmajor fbimajor flominor fhiminor fdbminor fbiminor).each do |type|
248
+ it "handles theme font type: #{type}" do
249
+ src = "{\\f0\\#{type} Times New Roman;}}"
250
+ parser.parse_font_table(src, 0)
251
+ tbl = doc.font_table
252
+ tbl[0].name.should == 'Times New Roman'
253
+ tbl[0].theme.should == type[1..-1].to_sym
254
+ end
255
+ end
256
+
257
+ [[0, :default], [1, :fixed], [2, :variable]].each do |pitch|
258
+ it 'parses pitch information' do
259
+ src = "{\\f0\\fprq#{pitch.first} Times New Roman;}}"
260
+ parser.parse_font_table(src, 0)
261
+ tbl = doc.font_table
262
+ tbl[0].name.should == 'Times New Roman'
263
+ tbl[0].pitch.should == pitch.last
264
+ end
265
+ end
266
+
267
+ it 'parses the non-tagged font name' do
268
+ src = '{\f0{\*\fname Arial;}Times New Roman;}}'
269
+ parser.parse_font_table(src, 0)
270
+ tbl = doc.font_table
271
+ tbl[0].name.should == 'Times New Roman'
272
+ tbl[0].non_tagged_name.should == 'Arial'
273
+ end
274
+
275
+ it 'parses the charset' do
276
+ src = '{\f0\fcharset87 Times New Roman;}}'
277
+ parser.parse_font_table(src, 0)
278
+ tbl = doc.font_table
279
+ tbl[0].name.should == 'Times New Roman'
280
+ tbl[0].character_set.should == 87
281
+ end
282
+ end
283
+ end
284
+
285
+ context 'colour table' do
286
+ it 'sets the colour table into the document' do
287
+ src = '{\rtf1{\colortbl\red0\green0\blue0;\red127\green2\blue255;}}'
288
+ doc = parser.parse(src)
289
+
290
+ clr = doc.colour_table[0]
291
+ clr.red.should == 0
292
+ clr.green.should == 0
293
+ clr.blue.should == 0
294
+
295
+ clr = doc.colour_table[1]
296
+ clr.red.should == 127
297
+ clr.green.should == 2
298
+ clr.blue.should == 255
299
+ end
300
+
301
+ it 'sets the first colour if missing' do
302
+ src = '{\rtf1{\colortbl;\red255\green0\blue0;\red0\green0\blue255;}}'
303
+ doc = parser.parse(src)
304
+
305
+ clr = doc.colour_table[0]
306
+ clr.use_default?.should be_true
307
+
308
+ clr = doc.colour_table[1]
309
+ clr.red.should == 255
310
+ clr.green.should == 0
311
+ clr.blue.should == 0
312
+ end
313
+
314
+ context '#parse_colour_table' do
315
+ it 'parses \red \green \blue' do
316
+ src = '\red2\green55\blue23;}'
317
+ parser.parse_colour_table(src, 0)
318
+ tbl = doc.colour_table
319
+ tbl[0].red.should == 2
320
+ tbl[0].green.should == 55
321
+ tbl[0].blue.should == 23
322
+ end
323
+
324
+ it 'handles ctintN' do
325
+ src = '\ctint22\red2\green55\blue23;}'
326
+ parser.parse_colour_table(src, 0)
327
+ tbl = doc.colour_table
328
+ tbl[0].tint.should == 22
329
+ end
330
+
331
+ it 'handles cshadeN' do
332
+ src = '\cshade11\red2\green55\blue23;}'
333
+ parser.parse_colour_table(src, 0)
334
+ tbl = doc.colour_table
335
+ tbl[0].shade.should == 11
336
+ end
337
+
338
+ %w(cmaindarkone cmainlightone cmaindarktwo cmainlighttwo caccentone
339
+ caccenttwo caccentthree caccentfour caccentfive caccentsix
340
+ chyperlink cfollowedhyperlink cbackgroundone ctextone
341
+ cbackgroundtwo ctexttwo).each do |theme|
342
+ it "it allows theme item #{theme}" do
343
+ src = "\\#{theme}\\red11\\green22\\blue33;}"
344
+ parser.parse_colour_table(src, 0)
345
+ tbl = doc.colour_table
346
+ tbl[0].theme.should == theme[1..-1].to_sym
347
+ end
348
+ end
349
+
350
+ it 'handles \r and \n' do
351
+ src = "\\cshade11\\red2\\green55\r\n\\blue23;}"
352
+ parser.parse_colour_table(src, 0)
353
+ tbl = doc.colour_table
354
+ tbl[0].shade.should == 11
355
+ tbl[0].red.should == 2
356
+ tbl[0].green.should == 55
357
+ tbl[0].blue.should == 23
358
+ end
359
+ end
360
+ end
361
+
362
+ context 'stylesheet' do
363
+ it 'parses a stylesheet'
364
+ end
365
+
366
+ context 'document info' do
367
+ it 'parse the doocument info'
368
+ end
369
+
370
+ context '#handle_control' do
371
+ it 'sets the font' do
372
+ font = RubyRTF::Font.new('Times New Roman')
373
+ doc.font_table[0] = font
374
+
375
+ parser.handle_control(:f, 0, nil, 0)
376
+ parser.current_section[:modifiers][:font].should == font
377
+ end
378
+
379
+ it 'sets the font size' do
380
+ parser.handle_control(:fs, 61, nil, 0)
381
+ parser.current_section[:modifiers][:font_size].should == 30.5
382
+ end
383
+
384
+ it 'sets bold' do
385
+ parser.handle_control(:b, nil, nil, 0)
386
+ parser.current_section[:modifiers][:bold].should be_true
387
+ end
388
+
389
+ it 'sets underline' do
390
+ parser.handle_control(:ul, nil, nil, 0)
391
+ parser.current_section[:modifiers][:underline].should be_true
392
+ end
393
+
394
+ it 'sets italic' do
395
+ parser.handle_control(:i, nil, nil, 0)
396
+ parser.current_section[:modifiers][:italic].should be_true
397
+ end
398
+
399
+ %w(rquote lquote).each do |quote|
400
+ it "sets a #{quote}" do
401
+ parser.current_section[:text] = 'My code'
402
+ parser.handle_control(quote.to_sym, nil, nil, 0)
403
+ doc.sections.last[:text].should == "'"
404
+ doc.sections.last[:modifiers][quote.to_sym].should be_true
405
+ end
406
+ end
407
+
408
+ %w(rdblquote ldblquote).each do |quote|
409
+ it "sets a #{quote}" do
410
+ parser.current_section[:text] = 'My code'
411
+ parser.handle_control(quote.to_sym, nil, nil, 0)
412
+ doc.sections.last[:text].should == '"'
413
+ doc.sections.last[:modifiers][quote.to_sym].should be_true
414
+ end
415
+ end
416
+
417
+ it 'sets a hex character' do
418
+ parser.current_section[:text] = 'My code'
419
+ parser.handle_control(:hex, '~', nil, 0)
420
+ parser.current_section[:text].should == 'My code~'
421
+ end
422
+
423
+ it 'sets a unicode character < 1000 (char 643)' do
424
+ parser.current_section[:text] = 'My code'
425
+ parser.handle_control(:u, 643, nil, 0)
426
+ parser.current_section[:text].should == 'My codeك'
427
+ end
428
+
429
+ it 'sets a unicode character < 32768 (char 2603)' do
430
+ parser.current_section[:text] = 'My code'
431
+ parser.handle_control(:u, 2603, nil, 0)
432
+ parser.current_section[:text].should == 'My code☃'
433
+ end
434
+
435
+ it 'sets a unicode character < 32768 (char 21340)' do
436
+ parser.current_section[:text] = 'My code'
437
+ parser.handle_control(:u, 21340, nil, 0)
438
+ parser.current_section[:text].should == 'My code卜'
439
+ end
440
+
441
+
442
+ it 'sets a unicode character > 32767 (char 36,947)' do
443
+ parser.current_section[:text] = 'My code'
444
+ parser.handle_control(:u, -28589, nil, 0)
445
+ parser.current_section[:text].should == 'My code道'
446
+ end
447
+
448
+ context 'new line' do
449
+ ['line', "\n"].each do |type|
450
+ it "sets from #{type}" do
451
+ parser.current_section[:text] = "end."
452
+ parser.handle_control(type.to_sym, nil, nil, 0)
453
+ doc.sections.last[:modifiers][:newline].should be_true
454
+ doc.sections.last[:text].should == "\n"
455
+ end
456
+ end
457
+
458
+ it 'ignores \r' do
459
+ parser.current_section[:text] = "end."
460
+ parser.handle_control(:"\r", nil, nil, 0)
461
+ parser.current_section[:text].should == "end."
462
+ end
463
+ end
464
+
465
+ it 'inserts a \tab' do
466
+ parser.current_section[:text] = "end."
467
+ parser.handle_control(:tab, nil, nil, 0)
468
+ doc.sections.last[:modifiers][:tab].should be_true
469
+ doc.sections.last[:text].should == "\t"
470
+ end
471
+
472
+ it 'inserts a \super' do
473
+ parser.current_section[:text] = "end."
474
+ parser.handle_control(:super, nil, nil, 0)
475
+
476
+ parser.current_section[:modifiers][:superscript].should be_true
477
+ parser.current_section[:text].should == ""
478
+ end
479
+
480
+ it 'inserts a \sub' do
481
+ parser.current_section[:text] = "end."
482
+ parser.handle_control(:sub, nil, nil, 0)
483
+
484
+ parser.current_section[:modifiers][:subscript].should be_true
485
+ parser.current_section[:text].should == ""
486
+ end
487
+
488
+ it 'inserts a \strike' do
489
+ parser.current_section[:text] = "end."
490
+ parser.handle_control(:strike, nil, nil, 0)
491
+
492
+ parser.current_section[:modifiers][:strikethrough].should be_true
493
+ parser.current_section[:text].should == ""
494
+ end
495
+
496
+ it 'inserts a \scaps' do
497
+ parser.current_section[:text] = "end."
498
+ parser.handle_control(:scaps, nil, nil, 0)
499
+
500
+ parser.current_section[:modifiers][:smallcaps].should be_true
501
+ parser.current_section[:text].should == ""
502
+ end
503
+
504
+ it 'inserts an \emdash' do
505
+ parser.current_section[:text] = "end."
506
+ parser.handle_control(:emdash, nil, nil, 0)
507
+ doc.sections.last[:modifiers][:emdash].should be_true
508
+ doc.sections.last[:text].should == "--"
509
+ end
510
+
511
+ it 'inserts an \endash' do
512
+ parser.current_section[:text] = "end."
513
+ parser.handle_control(:endash, nil, nil, 0)
514
+ doc.sections.last[:modifiers][:endash].should be_true
515
+ doc.sections.last[:text].should == "-"
516
+ end
517
+
518
+ context 'escapes' do
519
+ ['{', '}', '\\'].each do |escape|
520
+ it "inserts an escaped #{escape}" do
521
+ parser.current_section[:text] = "end."
522
+ parser.handle_control(escape.to_sym, nil, nil, 0)
523
+ parser.current_section[:text].should == "end.#{escape}"
524
+ end
525
+ end
526
+ end
527
+
528
+ it 'adds a new section for a par command' do
529
+ parser.current_section[:text] = 'end.'
530
+ parser.handle_control(:par, nil, nil, 0)
531
+ parser.current_section[:text].should == ""
532
+ end
533
+
534
+ %w(pard plain).each do |type|
535
+ it "resets the current sections information to default for #{type}" do
536
+ parser.current_section[:modifiers][:bold] = true
537
+ parser.current_section[:modifiers][:italic] = true
538
+ parser.handle_control(type.to_sym, nil, nil, 0)
539
+
540
+ parser.current_section[:modifiers].has_key?(:bold).should be_false
541
+ parser.current_section[:modifiers].has_key?(:italic).should be_false
542
+ end
543
+ end
544
+
545
+ context 'colour' do
546
+ it 'sets the foreground colour' do
547
+ doc.colour_table << RubyRTF::Colour.new(255, 0, 255)
548
+ parser.handle_control(:cf, 0, nil, 0)
549
+ parser.current_section[:modifiers][:foreground_colour].to_s.should == "[255, 0, 255]"
550
+ end
551
+
552
+ it 'sets the background colour' do
553
+ doc.colour_table << RubyRTF::Colour.new(255, 0, 255)
554
+ parser.handle_control(:cb, 0, nil, 0)
555
+ parser.current_section[:modifiers][:background_colour].to_s.should == "[255, 0, 255]"
556
+ end
557
+ end
558
+
559
+ context 'justification' do
560
+ it 'handles left justify' do
561
+ parser.handle_control(:ql, nil, nil, 0)
562
+ parser.current_section[:modifiers][:justification].should == :left
563
+ end
564
+
565
+ it 'handles right justify' do
566
+ parser.handle_control(:qr, nil, nil, 0)
567
+ parser.current_section[:modifiers][:justification].should == :right
568
+ end
569
+
570
+ it 'handles full justify' do
571
+ parser.handle_control(:qj, nil, nil, 0)
572
+ parser.current_section[:modifiers][:justification].should == :full
573
+ end
574
+
575
+ it 'handles centered' do
576
+ parser.handle_control(:qc, nil, nil, 0)
577
+ parser.current_section[:modifiers][:justification].should == :center
578
+ end
579
+ end
580
+
581
+ context 'indenting' do
582
+ it 'handles first line indent' do
583
+ parser.handle_control(:fi, 1000, nil, 0)
584
+ parser.current_section[:modifiers][:first_line_indent].should == 50
585
+ end
586
+
587
+ it 'handles left indent' do
588
+ parser.handle_control(:li, 1000, nil, 0)
589
+ parser.current_section[:modifiers][:left_indent].should == 50
590
+ end
591
+
592
+ it 'handles right indent' do
593
+ parser.handle_control(:ri, 1000, nil, 0)
594
+ parser.current_section[:modifiers][:right_indent].should == 50
595
+ end
596
+ end
597
+
598
+ context 'margins' do
599
+ it 'handles left margin' do
600
+ parser.handle_control(:margl, 1000, nil, 0)
601
+ parser.current_section[:modifiers][:left_margin].should == 50
602
+ end
603
+
604
+ it 'handles right margin' do
605
+ parser.handle_control(:margr, 1000, nil, 0)
606
+ parser.current_section[:modifiers][:right_margin].should == 50
607
+ end
608
+
609
+ it 'handles top margin' do
610
+ parser.handle_control(:margt, 1000, nil, 0)
611
+ parser.current_section[:modifiers][:top_margin].should == 50
612
+ end
613
+
614
+ it 'handles bottom margin' do
615
+ parser.handle_control(:margb, 1000, nil, 0)
616
+ parser.current_section[:modifiers][:bottom_margin].should == 50
617
+ end
618
+ end
619
+
620
+ context 'paragraph spacing' do
621
+ it 'handles space before' do
622
+ parser.handle_control(:sb, 1000, nil, 0)
623
+ parser.current_section[:modifiers][:space_before].should == 50
624
+ end
625
+
626
+ it 'handles space after' do
627
+ parser.handle_control(:sa, 1000, nil, 0)
628
+ parser.current_section[:modifiers][:space_after].should == 50
629
+ end
630
+ end
631
+
632
+ context 'non breaking space' do
633
+ it 'handles :~' do
634
+ parser.current_section[:text] = "end."
635
+ parser.handle_control(:~, nil, nil, 0)
636
+ doc.sections.last[:modifiers][:nbsp].should be_true
637
+ doc.sections.last[:text].should == " "
638
+ end
639
+ end
640
+ end
641
+
642
+ context 'sections' do
643
+ it 'has sections' do
644
+ doc.sections.should_not be_nil
645
+ end
646
+
647
+ it 'sets an initial section' do
648
+ parser.current_section.should_not be_nil
649
+ end
650
+
651
+ context '#add_section!' do
652
+ it 'does not add a section if the current :text is empty' do
653
+ d = parser
654
+ d.add_section!
655
+ doc.sections.length.should == 0
656
+ end
657
+
658
+ it 'adds a section of the current section has text' do
659
+ d = parser
660
+ d.current_section[:text] = "Test"
661
+ d.add_section!
662
+ doc.sections.length.should == 1
663
+ end
664
+
665
+ it 'inherits the modifiers from the parent section' do
666
+ d = parser
667
+ d.current_section[:modifiers][:bold] = true
668
+ d.current_section[:modifiers][:italics] = true
669
+ d.current_section[:text] = "New text"
670
+
671
+ d.add_section!
672
+
673
+ d.current_section[:modifiers][:underline] = true
674
+
675
+ sections = doc.sections
676
+ sections.first[:modifiers].should == {:bold => true, :italics => true}
677
+ d.current_section[:modifiers].should == {:bold => true, :italics => true, :underline => true}
678
+ end
679
+ end
680
+
681
+ context '#reset_current_section!' do
682
+ it 'resets the current sections modifiers' do
683
+ d = parser
684
+ d.current_section[:modifiers] = {:bold => true, :italics => true}
685
+ d.current_section[:text] = "New text"
686
+
687
+ d.add_section!
688
+ d.reset_current_section!
689
+ d.current_section[:modifiers][:underline] = true
690
+
691
+ sections = doc.sections
692
+ sections.first[:modifiers].should == {:bold => true, :italics => true}
693
+ d.current_section[:modifiers].should == {:underline => true}
694
+ end
695
+ end
696
+
697
+ context '#remove_last_section!' do
698
+ it 'removes the last section' do
699
+ d = parser
700
+ d.current_section[:modifiers] = {:bold => true, :italics => true}
701
+ d.current_section[:text] = "New text"
702
+
703
+ d.add_section!
704
+
705
+ d.current_section[:modifiers][:underline] = true
706
+
707
+ doc.sections.length.should == 1
708
+ doc.sections.first[:text].should == 'New text'
709
+ end
710
+ end
711
+
712
+ context 'tables' do
713
+ def compare_table_results(table, data)
714
+ table.rows.length.should == data.length
715
+
716
+ data.each_with_index do |row, idx|
717
+ end_positions = table.rows[idx].end_positions
718
+ row[:end_positions].each_with_index do |size, cidx|
719
+ end_positions[cidx].should == size
720
+ end
721
+
722
+ cells = table.rows[idx].cells
723
+ cells.length.should == row[:values].length
724
+
725
+ row[:values].each_with_index do |items, vidx|
726
+ sects = cells[vidx].sections
727
+ items.each_with_index do |val, iidx|
728
+ sects[iidx][:text].should == val
729
+ end
730
+ end
731
+ end
732
+ end
733
+
734
+ it 'parses a single row/column table' do
735
+ src = '{\rtf1 Before Table' +
736
+ '\trowd\trgaph180\cellx1440' +
737
+ '\pard\intbl fee.\cell\row ' +
738
+ 'After table}'
739
+ d = parser.parse(src)
740
+
741
+ sect = d.sections
742
+ sect.length.should == 3
743
+ sect[0][:text].should == 'Before Table'
744
+ sect[2][:text].should == 'After table'
745
+
746
+ sect[1][:modifiers][:table].should_not be_nil
747
+ table = sect[1][:modifiers][:table]
748
+
749
+ compare_table_results(table, [{:end_positions => [72], :values => [['fee.']]}])
750
+ end
751
+
752
+ it 'parses a \trgaph180' do
753
+ src = '{\rtf1 Before Table' +
754
+ '\trowd\trgaph180\cellx1440' +
755
+ '\pard\intbl fee.\cell\row ' +
756
+ 'After table}'
757
+ d = parser.parse(src)
758
+
759
+ table = d.sections[1][:modifiers][:table]
760
+ table.half_gap.should == 9
761
+ end
762
+
763
+ it 'parses a \trleft240' do
764
+ src = '{\rtf1 Before Table' +
765
+ '\trowd\trgaph180\trleft240\cellx1440' +
766
+ '\pard\intbl fee.\cell\row ' +
767
+ 'After table}'
768
+ d = parser.parse(src)
769
+
770
+ table = d.sections[1][:modifiers][:table]
771
+ table.left_margin.should == 12
772
+ end
773
+
774
+ it 'parses a single row with multiple columns' do
775
+ src = '{\rtf1 Before Table' +
776
+ '\trowd\trgaph180\cellx1440\cellx2880\cellx1000' +
777
+ '\pard\intbl fee.\cell' +
778
+ '\pard\intbl fie.\cell' +
779
+ '\pard\intbl foe.\cell\row ' +
780
+ 'After table}'
781
+ d = parser.parse(src)
782
+
783
+ sect = d.sections
784
+
785
+ sect.length.should == 3
786
+ sect[0][:text].should == 'Before Table'
787
+ sect[2][:text].should == 'After table'
788
+
789
+ sect[1][:modifiers][:table].should_not be_nil
790
+ table = sect[1][:modifiers][:table]
791
+
792
+ compare_table_results(table, [{:end_positions => [72, 144, 50], :values => [['fee.'], ['fie.'], ['foe.']]}])
793
+ end
794
+
795
+ it 'parses multiple rows and multiple columns' do
796
+ src = '{\rtf1 \strike Before Table' +
797
+ '\trowd\trgaph180\cellx1440\cellx2880\cellx1000' +
798
+ '\pard\intbl\ul fee.\cell' +
799
+ '\pard\intbl\i fie.\cell' +
800
+ '\pard\intbl\b foe.\cell\row ' +
801
+ '\trowd\trgaph180\cellx1000\cellx1440\cellx2880' +
802
+ '\pard\intbl\i foo.\cell' +
803
+ '\pard\intbl\b bar.\cell' +
804
+ '\pard\intbl\ul baz.\cell\row ' +
805
+ 'After table}'
806
+ d = parser.parse(src)
807
+
808
+ sect = d.sections
809
+ sect.length.should == 3
810
+ sect[0][:text].should == 'Before Table'
811
+ sect[2][:text].should == 'After table'
812
+
813
+ sect[1][:modifiers][:table].should_not be_nil
814
+ table = sect[1][:modifiers][:table]
815
+
816
+ compare_table_results(table, [{:end_positions => [72, 144, 50], :values => [['fee.'], ['fie.'], ['foe.']]},
817
+ {:end_positions => [50, 72, 144], :values => [['foo.'], ['bar.'], ['baz.']]}])
818
+ end
819
+
820
+ it 'parses a grouped table' do
821
+ src = '{\rtf1 \strike Before Table' +
822
+ '{\trowd\trgaph180\cellx1440\cellx2880\cellx1000' +
823
+ '\pard\intbl\ul fee.\cell' +
824
+ '\pard\intbl\i fie.\cell' +
825
+ '\pard\intbl\b foe.\cell\row}' +
826
+ '{\trowd\trgaph180\cellx1000\cellx1440\cellx2880' +
827
+ '\pard\intbl\i foo.\cell' +
828
+ '\pard\intbl\b bar.\cell' +
829
+ '\pard\intbl\ul baz.\cell\row}' +
830
+ 'After table}'
831
+ d = parser.parse(src)
832
+
833
+ sect = d.sections
834
+ sect.length.should == 3
835
+ sect[0][:text].should == 'Before Table'
836
+ sect[2][:text].should == 'After table'
837
+
838
+ sect[1][:modifiers][:table].should_not be_nil
839
+ table = sect[1][:modifiers][:table]
840
+
841
+ compare_table_results(table, [{:end_positions => [72, 144, 50], :values => [['fee.'], ['fie.'], ['foe.']]},
842
+ {:end_positions => [50, 72, 144], :values => [['foo.'], ['bar.'], ['baz.']]}])
843
+ end
844
+
845
+ it 'parses a new line inside a table cell' do
846
+ src = '{\rtf1 Before Table' +
847
+ '\trowd\trgaph180\cellx1440' +
848
+ '\pard\intbl fee.\line fie.\cell\row ' +
849
+ 'After table}'
850
+ d = parser.parse(src)
851
+
852
+ sect = d.sections
853
+ sect.length.should == 3
854
+ sect[0][:text].should == 'Before Table'
855
+ sect[2][:text].should == 'After table'
856
+ table = sect[1][:modifiers][:table]
857
+
858
+ compare_table_results(table, [{:end_positions => [72], :values => [["fee.", "\n", "fie."]]}])
859
+ end
860
+
861
+ it 'parses a new line inside a table cell' do
862
+ src = '{\rtf1 Before Table' +
863
+ '\trowd\trgaph180\cellx1440\cellx2880\cellx1000' +
864
+ '\pard\intbl fee.\cell' +
865
+ '\pard\intbl\cell' +
866
+ '\pard\intbl fie.\cell\row ' +
867
+ 'After table}'
868
+ d = parser.parse(src)
869
+
870
+ sect = d.sections
871
+ sect.length.should == 3
872
+ sect[0][:text].should == 'Before Table'
873
+ sect[2][:text].should == 'After table'
874
+ table = sect[1][:modifiers][:table]
875
+
876
+ compare_table_results(table, [{:end_positions => [72, 144, 50], :values => [["fee."], [""], ["fie."]]}])
877
+ end
878
+
879
+ it 'parses a grouped cell' do
880
+ src = '{\rtf1 Before Table\trowd\cellx1440\cellx2880\cellx1000 \pard ' +
881
+ '{\fs20 Familiar }{\cell }' +
882
+ '{\fs20 Alignment }{\cell }' +
883
+ '\pard \intbl {\fs20 Arcane Spellcaster Level}{\cell }' +
884
+ '\pard {\b\fs18 \trowd \trgaph108\trleft-108\cellx1000\row }After table}'
885
+ d = parser.parse(src)
886
+
887
+ sect = d.sections
888
+
889
+ sect.length.should == 3
890
+ sect[0][:text].should == 'Before Table'
891
+ sect[2][:text].should == 'After table'
892
+ table = sect[1][:modifiers][:table]
893
+
894
+ compare_table_results(table, [{:end_positions => [72, 144, 50],
895
+ :values => [["Familiar "], ["Alignment "], ['Arcane Spellcaster Level']]}])
896
+ end
897
+
898
+ it 'parses cells' do
899
+ src = '{\rtf1\trowd\trgaph108\trleft-108\cellx1440\cellx2880' +
900
+ '\intbl{\fs20 Familiar }{\cell }' +
901
+ '{\fs20 Alignment }{\cell }}'
902
+
903
+ d = parser.parse(src)
904
+ table = d.sections[0][:modifiers][:table]
905
+
906
+ compare_table_results(table, [{:end_positions => [72, 144], :values => [['Familiar '], ['Alignment ']]}])
907
+ end
908
+
909
+ it 'parses blank rows' do
910
+ src = '{\rtf1\trowd \trgaph108\trleft-108\cellx1440' +
911
+ '\intbl{\fs20 Familiar }{\cell }' +
912
+ '\pard\plain \intbl {\trowd \trgaph108\trleft-108\cellx1440\row } ' +
913
+ 'Improved animal}'
914
+ d = parser.parse(src)
915
+
916
+ sect = d.sections
917
+ sect.length.should == 2
918
+ sect[1][:text].should == ' Improved animal'
919
+ sect[1][:modifiers].should == {}
920
+
921
+ table = sect[0][:modifiers][:table]
922
+ compare_table_results(table, [{:end_positions => [72], :values => [['Familiar ']]}])
923
+ end
924
+ end
925
+ end
926
+ end