Skip to content

Commit 9d6b022

Browse files
authored
Merge pull request #66 from visoft/master
Added opening streams and outputting to a stream
2 parents fea81fd + f5dea61 commit 9d6b022

3 files changed

Lines changed: 145 additions & 68 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ doc.bookmarks.each_pair do |bookmark_name, bookmark_object|
5151
end
5252
```
5353

54+
Don't have a local file but a buffer? Docx handles those to:
55+
56+
```ruby
57+
require 'docx'
58+
59+
# Create a Docx::Document object from a remote file
60+
doc = Docx::Document.open(buffer)
61+
62+
# Everything about reading is the same as shown above
63+
```
64+
5465
### Rendering html
5566
``` ruby
5667
require 'docx'

lib/docx/document.rb

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,16 @@ module Docx
2020
class Document
2121
attr_reader :xml, :doc, :zip, :styles
2222

23-
def initialize(path, &block)
23+
def initialize(path_or_io, options = {})
2424
@replace = {}
25-
@zip = Zip::File.open(path)
25+
26+
# if path-or_io is string && does not contain a null byte
27+
if (path_or_io.instance_of?(String) && !/\u0000/.match?(path_or_io))
28+
@zip = Zip::File.open(path_or_io)
29+
else
30+
@zip = Zip::File.open_buffer(path_or_io)
31+
end
32+
2633
@document_xml = @zip.read('word/document.xml')
2734
@doc = Nokogiri::XML(@document_xml)
2835
load_styles
@@ -32,33 +39,31 @@ def initialize(path, &block)
3239
end
3340
end
3441

35-
3642
# This stores the current global document properties, for now
3743
def document_properties
3844
{
3945
font_size: font_size
4046
}
4147
end
4248

43-
4449
# With no associated block, Docx::Document.open is a synonym for Docx::Document.new. If the optional code block is given, it will be passed the opened +docx+ file as an argument and the Docx::Document oject will automatically be closed when the block terminates. The values of the block will be returned from Docx::Document.open.
4550
# call-seq:
4651
# open(filepath) => file
4752
# open(filepath) {|file| block } => obj
4853
def self.open(path, &block)
49-
self.new(path, &block)
54+
new(path, &block)
5055
end
5156

5257
def paragraphs
5358
@doc.xpath('//w:document//w:body/w:p').map { |p_node| parse_paragraph_from p_node }
5459
end
5560

5661
def bookmarks
57-
bkmrks_hsh = Hash.new
62+
bkmrks_hsh = {}
5863
bkmrks_ary = @doc.xpath('//w:bookmarkStart').map { |b_node| parse_bookmark_from b_node }
5964
# auto-generated by office 2010
60-
bkmrks_ary.reject! {|b| b.name == "_GoBack" }
61-
bkmrks_ary.each {|b| bkmrks_hsh[b.name] = b }
65+
bkmrks_ary.reject! { |b| b.name == '_GoBack' }
66+
bkmrks_ary.each { |b| bkmrks_hsh[b.name] = b }
6267
bkmrks_hsh
6368
end
6469

@@ -104,6 +109,7 @@ def save(path)
104109
Zip::OutputStream.open(path) do |out|
105110
zip.each do |entry|
106111
next unless entry.file?
112+
107113
out.put_next_entry(entry.name)
108114

109115
if @replace[entry.name]
@@ -116,7 +122,28 @@ def save(path)
116122
zip.close
117123
end
118124

119-
alias_method :text, :to_s
125+
# Output entire document as a StringIO object
126+
def stream
127+
update
128+
stream = Zip::OutputStream.write_buffer do |out|
129+
zip.each do |entry|
130+
next unless entry.file?
131+
132+
out.put_next_entry(entry.name)
133+
134+
if @replace[entry.name]
135+
out.write(@replace[entry.name])
136+
else
137+
out.write(zip.read(entry.name))
138+
end
139+
end
140+
end
141+
142+
stream.rewind
143+
stream
144+
end
145+
146+
alias text to_s
120147

121148
def replace_entry(entry_path, file_contents)
122149
@replace[entry_path] = file_contents
@@ -138,7 +165,7 @@ def load_styles
138165
# end of methods that make edits?
139166
#++
140167
def update
141-
replace_entry "word/document.xml", doc.serialize(:save_with => 0)
168+
replace_entry 'word/document.xml', doc.serialize(save_with: 0)
142169
end
143170

144171
# generate Elements::Containers::Paragraph from paragraph XML node

spec/docx/document_spec.rb

Lines changed: 97 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
1-
# coding: utf-8
21
require 'docx'
32
require 'tempfile'
43

54
describe Docx::Document do
65
before(:all) do
7-
@fixtures_path = "spec/fixtures"
6+
@fixtures_path = 'spec/fixtures'
87
@formatting_line_count = 12 # number of lines the formatting.docx file has
98
end
109

11-
describe 'reading' do
12-
before do
13-
@doc = Docx::Document.open(@fixtures_path + '/basic.docx')
14-
end
15-
10+
shared_examples_for 'reading' do
1611
it 'should read the document' do
1712
expect(@doc.paragraphs.size).to eq(2)
1813
expect(@doc.paragraphs.first.text).to eq('hello')
@@ -53,12 +48,57 @@
5348
end
5449
end
5550

51+
shared_examples_for 'saving to file' do
52+
it 'should save to a normal file path' do
53+
@new_doc_path = @fixtures_path + '/new_save.docx'
54+
@doc.save(@new_doc_path)
55+
@new_doc = Docx::Document.open(@new_doc_path)
56+
expect(@new_doc.paragraphs.size).to eq(@doc.paragraphs.size)
57+
end
58+
59+
it 'should save to a tempfile' do
60+
temp_file = Tempfile.new(['docx_gem', '.docx'])
61+
@new_doc_path = temp_file.path
62+
@doc.save(@new_doc_path)
63+
@new_doc = Docx::Document.open(@new_doc_path)
64+
expect(@new_doc.paragraphs.size).to eq(@doc.paragraphs.size)
65+
66+
temp_file.close
67+
temp_file.unlink
68+
# ensure temp file has been removed
69+
expect(File.exist?(@new_doc_path)).to eq(false)
70+
end
71+
72+
after do
73+
File.delete(@new_doc_path) if File.exist?(@new_doc_path)
74+
end
75+
end
76+
77+
describe 'reading' do
78+
context 'using normal file' do
79+
before do
80+
@doc = Docx::Document.open(@fixtures_path + '/basic.docx')
81+
end
82+
83+
it_behaves_like 'reading'
84+
end
85+
86+
context 'using stream' do
87+
before do
88+
stream = File.binread(@fixtures_path + '/basic.docx')
89+
@doc = Docx::Document.open(stream)
90+
end
91+
92+
it_behaves_like 'reading'
93+
end
94+
end
95+
5696
describe 'read tables' do
5797
before do
5898
@doc = Docx::Document.open(@fixtures_path + '/tables.docx')
5999
end
60100

61-
it "should have tables with rows and cells" do
101+
it 'should have tables with rows and cells' do
62102
expect(@doc.tables.count).to eq 2
63103
@doc.tables.each do |table|
64104
expect(table).to be_an_instance_of(Docx::Elements::Containers::Table)
@@ -71,7 +111,7 @@
71111
end
72112
end
73113

74-
it "should have tables with columns and cells" do
114+
it 'should have tables with columns and cells' do
75115
@doc.tables.each do |table|
76116
table.columns.each do |column|
77117
expect(column).to be_an_instance_of(Docx::Elements::Containers::TableColumn)
@@ -82,23 +122,23 @@
82122
end
83123
end
84124

85-
it "should have proper count" do
125+
it 'should have proper count' do
86126
expect(@doc.tables[0].row_count).to eq 171
87127
expect(@doc.tables[1].row_count).to eq 2
88128
expect(@doc.tables[0].column_count).to eq 2
89129
expect(@doc.tables[1].column_count).to eq 2
90130
end
91131

92-
it "should have tables with proper text" do
93-
expect(@doc.tables[0].rows[0].cells[0].text).to eq "ENGLISH"
94-
expect(@doc.tables[0].rows[0].cells[1].text).to eq "FRANÇAIS"
95-
expect(@doc.tables[1].rows[0].cells[0].text).to eq "Second table"
96-
expect(@doc.tables[1].rows[0].cells[1].text).to eq "Second tableau"
97-
expect(@doc.tables[0].columns[0].cells[5].text).to eq "aphids"
98-
expect(@doc.tables[0].columns[1].cells[5].text).to eq "puceron"
132+
it 'should have tables with proper text' do
133+
expect(@doc.tables[0].rows[0].cells[0].text).to eq 'ENGLISH'
134+
expect(@doc.tables[0].rows[0].cells[1].text).to eq 'FRANÇAIS'
135+
expect(@doc.tables[1].rows[0].cells[0].text).to eq 'Second table'
136+
expect(@doc.tables[1].rows[0].cells[1].text).to eq 'Second tableau'
137+
expect(@doc.tables[0].columns[0].cells[5].text).to eq 'aphids'
138+
expect(@doc.tables[0].columns[1].cells[5].text).to eq 'puceron'
99139
end
100140

101-
it "should read embedded links" do
141+
it 'should read embedded links' do
102142
expect(@doc.tables[0].columns[1].cells[1].text).to match(/^Directive/)
103143
end
104144

@@ -109,7 +149,7 @@
109149
end
110150
end
111151

112-
describe 'editing' do
152+
describe 'editing' do
113153
before do
114154
@doc = Docx::Document.open(@fixtures_path + '/editing.docx')
115155
end
@@ -173,8 +213,7 @@
173213
end
174214

175215
it 'should allow content deletion' do
176-
expect{@doc.paragraphs.first.remove!}.to change{@doc.paragraphs.size}.by(-1)
177-
216+
expect { @doc.paragraphs.first.remove! }.to change { @doc.paragraphs.size }.by(-1)
178217
end
179218
end
180219

@@ -223,7 +262,6 @@
223262
end
224263

225264
it 'should contain a paragraph with multiple text runs' do
226-
227265
end
228266

229267
it 'should detect normal formatting' do
@@ -316,34 +354,21 @@
316354
end
317355

318356
describe 'saving' do
319-
before do
320-
@doc = Docx::Document.open(@fixtures_path + '/saving.docx')
321-
end
322-
323-
it 'should save to a normal file path' do
324-
@new_doc_path = @fixtures_path + '/new_save.docx'
325-
@doc.save(@new_doc_path)
326-
@new_doc = Docx::Document.open(@new_doc_path)
327-
expect(@new_doc.paragraphs.size).to eq(@doc.paragraphs.size)
328-
end
329-
330-
it 'should save to a tempfile' do
331-
temp_file = Tempfile.new(['docx_gem', '.docx'])
332-
@new_doc_path = temp_file.path
333-
@doc.save(@new_doc_path)
334-
@new_doc = Docx::Document.open(@new_doc_path)
335-
expect(@new_doc.paragraphs.size).to eq(@doc.paragraphs.size)
357+
context 'from a normal file' do
358+
before do
359+
@doc = Docx::Document.open(@fixtures_path + '/saving.docx')
360+
end
336361

337-
temp_file.close
338-
temp_file.unlink
339-
# ensure temp file has been removed
340-
expect(File.exists?(@new_doc_path)).to eq(false)
362+
it_behaves_like 'saving to file'
341363
end
342364

343-
after do
344-
if File.exists?(@new_doc_path)
345-
File.delete(@new_doc_path)
365+
context 'from a stream' do
366+
before do
367+
stream = File.binread(@fixtures_path + '/saving.docx')
368+
@doc = Docx::Document.open(stream)
346369
end
370+
371+
it_behaves_like 'saving to file'
347372
end
348373

349374
context 'wps modified docx file' do
@@ -357,6 +382,24 @@
357382
end
358383
end
359384

385+
describe 'streaming' do
386+
it 'should return a StringIO to send over HTTP' do
387+
doc = Docx::Document.open(@fixtures_path + '/basic.docx')
388+
expect(doc.stream).to be_a(StringIO)
389+
end
390+
391+
context 'should return a valid docx stream' do
392+
before do
393+
doc = Docx::Document.open(@fixtures_path + '/basic.docx')
394+
result = doc.stream
395+
396+
@doc = Docx::Document.open(result)
397+
end
398+
399+
it_behaves_like 'reading'
400+
end
401+
end
402+
360403
describe 'outputting html' do
361404
before do
362405
@doc = Docx::Document.open(@fixtures_path + '/formatting.docx')
@@ -400,7 +443,7 @@
400443
expect(@doc.paragraphs[8].to_html.scan(regex).flatten.first.split(';').include?('text-align:right')).to eq(true)
401444
end
402445

403-
it "should set font size on styled paragraphs" do
446+
it 'should set font size on styled paragraphs' do
404447
regex = /(\<p{1})[^\>]+style\=\"([^\"]+).+(<\/p>)/
405448
scan = @doc.paragraphs[9].to_html.scan(regex).flatten
406449
expect(scan.first).to eq '<p'
@@ -441,32 +484,28 @@
441484
it 'should output styled html' do
442485
expect(@formatted_line.to_html.scan('<span style="text-decoration:underline;"><strong><em>all</em></strong></span>').size).to eq 1
443486
end
444-
445487
end
446488

447489
describe 'replacing contents' do
448490
let(:replacement_file_path) { @fixtures_path + '/replacement.png' }
449-
let(:temp_file_path){ Tempfile.new(['docx_gem', '.docx']).path }
450-
let(:entry_path){ 'word/media/image1.png' }
451-
let(:doc){ Docx::Document.open(@fixtures_path + '/replacement.docx') }
491+
let(:temp_file_path) { Tempfile.new(['docx_gem', '.docx']).path }
492+
let(:entry_path) { 'word/media/image1.png' }
493+
let(:doc) { Docx::Document.open(@fixtures_path + '/replacement.docx') }
452494

453495
it 'should replace existing file within the document' do
454-
File.open replacement_file_path, "rb" do |io|
496+
File.open replacement_file_path, 'rb' do |io|
455497
doc.replace_entry entry_path, io.read
456498
end
457499

458500
doc.save(temp_file_path)
459501

460-
File.open replacement_file_path, "rb" do |io|
461-
expect(Zip::File.open(temp_file_path).read entry_path).to eq io.read
502+
File.open replacement_file_path, 'rb' do |io|
503+
expect(Zip::File.open(temp_file_path).read(entry_path)).to eq io.read
462504
end
463505
end
464506

465507
after do
466-
if File.exists?(temp_file_path)
467-
File.delete(temp_file_path)
468-
end
508+
File.delete(temp_file_path) if File.exist?(temp_file_path)
469509
end
470510
end
471511
end
472-

0 commit comments

Comments
 (0)