ruby-rtf 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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