docx 0.2.07 → 0.6.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.
@@ -23,6 +23,8 @@ module Docx
23
23
  def initialize(node, document_properties = {})
24
24
  @node = node
25
25
  @text_nodes = @node.xpath('w:t').map {|t_node| Elements::Text.new(t_node) }
26
+ @text_nodes = @node.xpath('w:t|w:r/w:t').map {|t_node| Elements::Text.new(t_node) }
27
+
26
28
  @properties_tag = 'rPr'
27
29
  @text = parse_text || ''
28
30
  @formatting = parse_formatting || DEFAULT_FORMATTING
@@ -45,6 +47,13 @@ module Docx
45
47
  @text_nodes.map(&:content).join('')
46
48
  end
47
49
 
50
+ # Substitute text in text @text_nodes
51
+ def substitute(match, replacement)
52
+ @text_nodes.each do |text_node|
53
+ text_node.content = text_node.content.gsub(match, replacement)
54
+ end
55
+ end
56
+
48
57
  def parse_formatting
49
58
  {
50
59
  italic: !@node.xpath('.//w:i').empty?,
@@ -67,6 +76,7 @@ module Docx
67
76
  # No need to be granular with font size down to the span level if it doesn't vary.
68
77
  styles['font-size'] = "#{font_size}pt" if font_size != @font_size
69
78
  html = html_tag(:span, content: html, styles: styles) unless styles.empty?
79
+ html = html_tag(:a, content: html, attributes: {href: href, target: "_blank"}) if hyperlink?
70
80
  return html
71
81
  end
72
82
 
@@ -82,6 +92,18 @@ module Docx
82
92
  @formatting[:underline]
83
93
  end
84
94
 
95
+ def hyperlink?
96
+ @node.name == 'hyperlink'
97
+ end
98
+
99
+ def href
100
+ @document_properties[:hyperlinks][hyperlink_id]
101
+ end
102
+
103
+ def hyperlink_id
104
+ @node.attributes['id'].value
105
+ end
106
+
85
107
  def font_size
86
108
  size_tag = @node.xpath('w:rPr//w:sz').first
87
109
  size_tag ? size_tag.attributes['val'].value.to_i / 2 : @font_size
@@ -1,172 +1,172 @@
1
- unless Object.const_defined?("ActiveSupport")
2
- class Module
3
- # Provides a delegate class method to easily expose contained objects' public methods
4
- # as your own. Pass one or more methods (specified as symbols or strings)
5
- # and the name of the target object via the <tt>:to</tt> option (also a symbol
6
- # or string). At least one method and the <tt>:to</tt> option are required.
7
- #
8
- # Delegation is particularly useful with Active Record associations:
9
- #
10
- # class Greeter < ActiveRecord::Base
11
- # def hello
12
- # 'hello'
13
- # end
14
- #
15
- # def goodbye
16
- # 'goodbye'
17
- # end
18
- # end
19
- #
20
- # class Foo < ActiveRecord::Base
21
- # belongs_to :greeter
22
- # delegate :hello, to: :greeter
23
- # end
24
- #
25
- # Foo.new.hello # => "hello"
26
- # Foo.new.goodbye # => NoMethodError: undefined method `goodbye' for #<Foo:0x1af30c>
27
- #
28
- # Multiple delegates to the same target are allowed:
29
- #
30
- # class Foo < ActiveRecord::Base
31
- # belongs_to :greeter
32
- # delegate :hello, :goodbye, to: :greeter
33
- # end
34
- #
35
- # Foo.new.goodbye # => "goodbye"
36
- #
37
- # Methods can be delegated to instance variables, class variables, or constants
38
- # by providing them as a symbols:
39
- #
40
- # class Foo
41
- # CONSTANT_ARRAY = [0,1,2,3]
42
- # @@class_array = [4,5,6,7]
43
- #
44
- # def initialize
45
- # @instance_array = [8,9,10,11]
46
- # end
47
- # delegate :sum, to: :CONSTANT_ARRAY
48
- # delegate :min, to: :@@class_array
49
- # delegate :max, to: :@instance_array
50
- # end
51
- #
52
- # Foo.new.sum # => 6
53
- # Foo.new.min # => 4
54
- # Foo.new.max # => 11
55
- #
56
- # It's also possible to delegate a method to the class by using +:class+:
57
- #
58
- # class Foo
59
- # def self.hello
60
- # "world"
61
- # end
62
- #
63
- # delegate :hello, to: :class
64
- # end
65
- #
66
- # Foo.new.hello # => "world"
67
- #
68
- # Delegates can optionally be prefixed using the <tt>:prefix</tt> option. If the value
69
- # is <tt>true</tt>, the delegate methods are prefixed with the name of the object being
70
- # delegated to.
71
- #
72
- # Person = Struct.new(:name, :address)
73
- #
74
- # class Invoice < Struct.new(:client)
75
- # delegate :name, :address, to: :client, prefix: true
76
- # end
77
- #
78
- # john_doe = Person.new('John Doe', 'Vimmersvej 13')
79
- # invoice = Invoice.new(john_doe)
80
- # invoice.client_name # => "John Doe"
81
- # invoice.client_address # => "Vimmersvej 13"
82
- #
83
- # It is also possible to supply a custom prefix.
84
- #
85
- # class Invoice < Struct.new(:client)
86
- # delegate :name, :address, to: :client, prefix: :customer
87
- # end
88
- #
89
- # invoice = Invoice.new(john_doe)
90
- # invoice.customer_name # => 'John Doe'
91
- # invoice.customer_address # => 'Vimmersvej 13'
92
- #
93
- # If the delegate object is +nil+ an exception is raised, and that happens
94
- # no matter whether +nil+ responds to the delegated method. You can get a
95
- # +nil+ instead with the +:allow_nil+ option.
96
- #
97
- # class Foo
98
- # attr_accessor :bar
99
- # def initialize(bar = nil)
100
- # @bar = bar
101
- # end
102
- # delegate :zoo, to: :bar
103
- # end
104
- #
105
- # Foo.new.zoo # raises NoMethodError exception (you called nil.zoo)
106
- #
107
- # class Foo
108
- # attr_accessor :bar
109
- # def initialize(bar = nil)
110
- # @bar = bar
111
- # end
112
- # delegate :zoo, to: :bar, allow_nil: true
113
- # end
114
- #
115
- # Foo.new.zoo # returns nil
116
- def delegate(*methods)
117
- options = methods.pop
118
- unless options.is_a?(Hash) && to = options[:to]
119
- raise ArgumentError, 'Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter).'
120
- end
121
-
122
- prefix, allow_nil = options.values_at(:prefix, :allow_nil)
123
-
124
- if prefix == true && to =~ /^[^a-z_]/
125
- raise ArgumentError, 'Can only automatically set the delegation prefix when delegating to a method.'
126
- end
127
-
128
- method_prefix = \
129
- if prefix
130
- "#{prefix == true ? to : prefix}_"
131
- else
132
- ''
133
- end
134
-
135
- file, line = caller.first.split(':', 2)
136
- line = line.to_i
137
-
138
- to = to.to_s
139
- to = 'self.class' if to == 'class'
140
-
141
- methods.each do |method|
142
- # Attribute writer methods only accept one argument. Makes sure []=
143
- # methods still accept two arguments.
144
- definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block'
145
-
146
- if allow_nil
147
- module_eval(<<-EOS, file, line - 2)
148
- def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
149
- if #{to} || #{to}.respond_to?(:#{method}) # if client || client.respond_to?(:name)
150
- #{to}.#{method}(#{definition}) # client.name(*args, &block)
151
- end # end
152
- end # end
153
- EOS
154
- else
155
- exception = %(raise "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
156
-
157
- module_eval(<<-EOS, file, line - 1)
158
- def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
159
- #{to}.#{method}(#{definition}) # client.name(*args, &block)
160
- rescue NoMethodError # rescue NoMethodError
161
- if #{to}.nil? # if client.nil?
162
- #{exception} # # add helpful message to the exception
163
- else # else
164
- raise # raise
165
- end # end
166
- end # end
167
- EOS
168
- end
169
- end
170
- end
171
- end
1
+ unless Object.const_defined?("ActiveSupport")
2
+ class Module
3
+ # Provides a delegate class method to easily expose contained objects' public methods
4
+ # as your own. Pass one or more methods (specified as symbols or strings)
5
+ # and the name of the target object via the <tt>:to</tt> option (also a symbol
6
+ # or string). At least one method and the <tt>:to</tt> option are required.
7
+ #
8
+ # Delegation is particularly useful with Active Record associations:
9
+ #
10
+ # class Greeter < ActiveRecord::Base
11
+ # def hello
12
+ # 'hello'
13
+ # end
14
+ #
15
+ # def goodbye
16
+ # 'goodbye'
17
+ # end
18
+ # end
19
+ #
20
+ # class Foo < ActiveRecord::Base
21
+ # belongs_to :greeter
22
+ # delegate :hello, to: :greeter
23
+ # end
24
+ #
25
+ # Foo.new.hello # => "hello"
26
+ # Foo.new.goodbye # => NoMethodError: undefined method `goodbye' for #<Foo:0x1af30c>
27
+ #
28
+ # Multiple delegates to the same target are allowed:
29
+ #
30
+ # class Foo < ActiveRecord::Base
31
+ # belongs_to :greeter
32
+ # delegate :hello, :goodbye, to: :greeter
33
+ # end
34
+ #
35
+ # Foo.new.goodbye # => "goodbye"
36
+ #
37
+ # Methods can be delegated to instance variables, class variables, or constants
38
+ # by providing them as a symbols:
39
+ #
40
+ # class Foo
41
+ # CONSTANT_ARRAY = [0,1,2,3]
42
+ # @@class_array = [4,5,6,7]
43
+ #
44
+ # def initialize
45
+ # @instance_array = [8,9,10,11]
46
+ # end
47
+ # delegate :sum, to: :CONSTANT_ARRAY
48
+ # delegate :min, to: :@@class_array
49
+ # delegate :max, to: :@instance_array
50
+ # end
51
+ #
52
+ # Foo.new.sum # => 6
53
+ # Foo.new.min # => 4
54
+ # Foo.new.max # => 11
55
+ #
56
+ # It's also possible to delegate a method to the class by using +:class+:
57
+ #
58
+ # class Foo
59
+ # def self.hello
60
+ # "world"
61
+ # end
62
+ #
63
+ # delegate :hello, to: :class
64
+ # end
65
+ #
66
+ # Foo.new.hello # => "world"
67
+ #
68
+ # Delegates can optionally be prefixed using the <tt>:prefix</tt> option. If the value
69
+ # is <tt>true</tt>, the delegate methods are prefixed with the name of the object being
70
+ # delegated to.
71
+ #
72
+ # Person = Struct.new(:name, :address)
73
+ #
74
+ # class Invoice < Struct.new(:client)
75
+ # delegate :name, :address, to: :client, prefix: true
76
+ # end
77
+ #
78
+ # john_doe = Person.new('John Doe', 'Vimmersvej 13')
79
+ # invoice = Invoice.new(john_doe)
80
+ # invoice.client_name # => "John Doe"
81
+ # invoice.client_address # => "Vimmersvej 13"
82
+ #
83
+ # It is also possible to supply a custom prefix.
84
+ #
85
+ # class Invoice < Struct.new(:client)
86
+ # delegate :name, :address, to: :client, prefix: :customer
87
+ # end
88
+ #
89
+ # invoice = Invoice.new(john_doe)
90
+ # invoice.customer_name # => 'John Doe'
91
+ # invoice.customer_address # => 'Vimmersvej 13'
92
+ #
93
+ # If the delegate object is +nil+ an exception is raised, and that happens
94
+ # no matter whether +nil+ responds to the delegated method. You can get a
95
+ # +nil+ instead with the +:allow_nil+ option.
96
+ #
97
+ # class Foo
98
+ # attr_accessor :bar
99
+ # def initialize(bar = nil)
100
+ # @bar = bar
101
+ # end
102
+ # delegate :zoo, to: :bar
103
+ # end
104
+ #
105
+ # Foo.new.zoo # raises NoMethodError exception (you called nil.zoo)
106
+ #
107
+ # class Foo
108
+ # attr_accessor :bar
109
+ # def initialize(bar = nil)
110
+ # @bar = bar
111
+ # end
112
+ # delegate :zoo, to: :bar, allow_nil: true
113
+ # end
114
+ #
115
+ # Foo.new.zoo # returns nil
116
+ def delegate(*methods)
117
+ options = methods.pop
118
+ unless options.is_a?(Hash) && to = options[:to]
119
+ raise ArgumentError, 'Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter).'
120
+ end
121
+
122
+ prefix, allow_nil = options.values_at(:prefix, :allow_nil)
123
+
124
+ if prefix == true && to =~ /^[^a-z_]/
125
+ raise ArgumentError, 'Can only automatically set the delegation prefix when delegating to a method.'
126
+ end
127
+
128
+ method_prefix = \
129
+ if prefix
130
+ "#{prefix == true ? to : prefix}_"
131
+ else
132
+ ''
133
+ end
134
+
135
+ file, line = caller.first.split(':', 2)
136
+ line = line.to_i
137
+
138
+ to = to.to_s
139
+ to = 'self.class' if to == 'class'
140
+
141
+ methods.each do |method|
142
+ # Attribute writer methods only accept one argument. Makes sure []=
143
+ # methods still accept two arguments.
144
+ definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block'
145
+
146
+ if allow_nil
147
+ module_eval(<<-EOS, file, line - 2)
148
+ def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
149
+ if #{to} || #{to}.respond_to?(:#{method}) # if client || client.respond_to?(:name)
150
+ #{to}.#{method}(#{definition}) # client.name(*args, &block)
151
+ end # end
152
+ end # end
153
+ EOS
154
+ else
155
+ exception = %(raise "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
156
+
157
+ module_eval(<<-EOS, file, line - 1)
158
+ def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
159
+ #{to}.#{method}(#{definition}) # client.name(*args, &block)
160
+ rescue NoMethodError # rescue NoMethodError
161
+ if #{to}.nil? # if client.nil?
162
+ #{exception} # # add helpful message to the exception
163
+ else # else
164
+ raise # raise
165
+ end # end
166
+ end # end
167
+ EOS
168
+ end
169
+ end
170
+ end
171
+ end
172
172
  end
data/lib/docx/document.rb CHANGED
@@ -1,148 +1,201 @@
1
- require 'docx/containers'
2
- require 'docx/elements'
3
- require 'nokogiri'
4
- require 'zip'
5
-
6
- module Docx
7
- # The Document class wraps around a docx file and provides methods to
8
- # interface with it.
9
- #
10
- # # get a Docx::Document for a docx file in the local directory
11
- # doc = Docx::Document.open("test.docx")
12
- #
13
- # # get the text from the document
14
- # puts doc.text
15
- #
16
- # # do the same thing in a block
17
- # Docx::Document.open("test.docx") do |d|
18
- # puts d.text
19
- # end
20
- class Document
21
- attr_reader :xml, :doc, :zip, :styles
22
-
23
- def initialize(path, &block)
24
- @replace = {}
25
- @zip = Zip::File.open(path)
26
- @document_xml = @zip.read('word/document.xml')
27
- @doc = Nokogiri::XML(@document_xml)
28
- @styles_xml = @zip.read('word/styles.xml')
29
- @styles = Nokogiri::XML(@styles_xml)
30
- if block_given?
31
- yield self
32
- @zip.close
33
- end
34
- end
35
-
36
-
37
- # This stores the current global document properties, for now
38
- def document_properties
39
- {
40
- font_size: font_size
41
- }
42
- end
43
-
44
-
45
- # 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.
46
- # call-seq:
47
- # open(filepath) => file
48
- # open(filepath) {|file| block } => obj
49
- def self.open(path, &block)
50
- self.new(path, &block)
51
- end
52
-
53
- def paragraphs
54
- @doc.xpath('//w:document//w:body//w:p').map { |p_node| parse_paragraph_from p_node }
55
- end
56
-
57
- def bookmarks
58
- bkmrks_hsh = Hash.new
59
- bkmrks_ary = @doc.xpath('//w:bookmarkStart').map { |b_node| parse_bookmark_from b_node }
60
- # auto-generated by office 2010
61
- bkmrks_ary.reject! {|b| b.name == "_GoBack" }
62
- bkmrks_ary.each {|b| bkmrks_hsh[b.name] = b }
63
- bkmrks_hsh
64
- end
65
-
66
- def tables
67
- @doc.xpath('//w:document//w:body//w:tbl').map { |t_node| parse_table_from t_node }
68
- end
69
-
70
- # Some documents have this set, others don't.
71
- # Values are returned as half-points, so to get points, that's why it's divided by 2.
72
- def font_size
73
- size_tag = @styles.xpath('//w:docDefaults//w:rPrDefault//w:rPr//w:sz').first
74
- size_tag ? size_tag.attributes['val'].value.to_i / 2 : nil
75
- end
76
-
77
- ##
78
- # *Deprecated*
79
- #
80
- # Iterates over paragraphs within document
81
- # call-seq:
82
- # each_paragraph => Enumerator
83
- def each_paragraph
84
- paragraphs.each { |p| yield(p) }
85
- end
86
-
87
- # call-seq:
88
- # to_s -> string
89
- def to_s
90
- paragraphs.map(&:to_s).join("\n")
91
- end
92
-
93
- # Output entire document as a String HTML fragment
94
- def to_html
95
- paragraphs.map(&:to_html).join('\n')
96
- end
97
-
98
- # Save document to provided path
99
- # call-seq:
100
- # save(filepath) => void
101
- def save(path)
102
- update
103
- Zip::OutputStream.open(path) do |out|
104
- zip.each do |entry|
105
- out.put_next_entry(entry.name)
106
-
107
- if @replace[entry.name]
108
- out.write(@replace[entry.name])
109
- else
110
- out.write(zip.read(entry.name))
111
- end
112
- end
113
- end
114
- zip.close
115
- end
116
-
117
- alias_method :text, :to_s
118
-
119
- def replace_entry(entry_path, file_contents)
120
- @replace[entry_path] = file_contents
121
- end
122
-
123
- private
124
-
125
- #--
126
- # TODO: Flesh this out to be compatible with other files
127
- # TODO: Method to set flag on files that have been edited, probably by inserting something at the
128
- # end of methods that make edits?
129
- #++
130
- def update
131
- replace_entry "word/document.xml", doc.serialize(:save_with => 0)
132
- end
133
-
134
- # generate Elements::Containers::Paragraph from paragraph XML node
135
- def parse_paragraph_from(p_node)
136
- Elements::Containers::Paragraph.new(p_node, document_properties)
137
- end
138
-
139
- # generate Elements::Bookmark from bookmark XML node
140
- def parse_bookmark_from(b_node)
141
- Elements::Bookmark.new(b_node)
142
- end
143
-
144
- def parse_table_from(t_node)
145
- Elements::Containers::Table.new(t_node)
146
- end
147
- end
148
- end
1
+ require 'docx/containers'
2
+ require 'docx/elements'
3
+ require 'nokogiri'
4
+ require 'zip'
5
+
6
+ module Docx
7
+ # The Document class wraps around a docx file and provides methods to
8
+ # interface with it.
9
+ #
10
+ # # get a Docx::Document for a docx file in the local directory
11
+ # doc = Docx::Document.open("test.docx")
12
+ #
13
+ # # get the text from the document
14
+ # puts doc.text
15
+ #
16
+ # # do the same thing in a block
17
+ # Docx::Document.open("test.docx") do |d|
18
+ # puts d.text
19
+ # end
20
+ class Document
21
+ attr_reader :xml, :doc, :zip, :styles
22
+
23
+ def initialize(path_or_io, options = {})
24
+ @replace = {}
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
+
33
+ document = @zip.glob('word/document*.xml').first
34
+ raise Errno::ENOENT if document.nil?
35
+
36
+ @document_xml = document.get_input_stream.read
37
+ @doc = Nokogiri::XML(@document_xml)
38
+ load_styles
39
+ yield(self) if block_given?
40
+ ensure
41
+ @zip.close
42
+ end
43
+
44
+ # This stores the current global document properties, for now
45
+ def document_properties
46
+ {
47
+ font_size: font_size,
48
+ hyperlinks: hyperlinks
49
+ }
50
+ end
51
+
52
+ # 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.
53
+ # call-seq:
54
+ # open(filepath) => file
55
+ # open(filepath) {|file| block } => obj
56
+ def self.open(path, &block)
57
+ new(path, &block)
58
+ end
59
+
60
+ def paragraphs
61
+ @doc.xpath('//w:document//w:body/w:p').map { |p_node| parse_paragraph_from p_node }
62
+ end
63
+
64
+ def bookmarks
65
+ bkmrks_hsh = {}
66
+ bkmrks_ary = @doc.xpath('//w:bookmarkStart').map { |b_node| parse_bookmark_from b_node }
67
+ # auto-generated by office 2010
68
+ bkmrks_ary.reject! { |b| b.name == '_GoBack' }
69
+ bkmrks_ary.each { |b| bkmrks_hsh[b.name] = b }
70
+ bkmrks_hsh
71
+ end
72
+
73
+ def tables
74
+ @doc.xpath('//w:document//w:body//w:tbl').map { |t_node| parse_table_from t_node }
75
+ end
76
+
77
+ # Some documents have this set, others don't.
78
+ # Values are returned as half-points, so to get points, that's why it's divided by 2.
79
+ def font_size
80
+ return nil unless @styles
81
+
82
+ size_tag = @styles.xpath('//w:docDefaults//w:rPrDefault//w:rPr//w:sz').first
83
+ size_tag ? size_tag.attributes['val'].value.to_i / 2 : nil
84
+ end
85
+
86
+ # Hyperlink targets are extracted from the document.xml.rels file
87
+ def hyperlinks
88
+ hyperlink_relationships.each_with_object({}) do |rel, hash|
89
+ hash[rel.attributes['Id'].value] = rel.attributes['Target'].value
90
+ end
91
+ end
92
+
93
+ def hyperlink_relationships
94
+ @rels.xpath("//xmlns:Relationship[contains(@Type,'hyperlink')]")
95
+ end
96
+
97
+ ##
98
+ # *Deprecated*
99
+ #
100
+ # Iterates over paragraphs within document
101
+ # call-seq:
102
+ # each_paragraph => Enumerator
103
+ def each_paragraph
104
+ paragraphs.each { |p| yield(p) }
105
+ end
106
+
107
+ # call-seq:
108
+ # to_s -> string
109
+ def to_s
110
+ paragraphs.map(&:to_s).join("\n")
111
+ end
112
+
113
+ # Output entire document as a String HTML fragment
114
+ def to_html
115
+ paragraphs.map(&:to_html).join("\n")
116
+ end
117
+
118
+ # Save document to provided path
119
+ # call-seq:
120
+ # save(filepath) => void
121
+ def save(path)
122
+ update
123
+ Zip::OutputStream.open(path) do |out|
124
+ zip.each do |entry|
125
+ next unless entry.file?
126
+
127
+ out.put_next_entry(entry.name)
128
+
129
+ if @replace[entry.name]
130
+ out.write(@replace[entry.name])
131
+ else
132
+ out.write(zip.read(entry.name))
133
+ end
134
+ end
135
+ end
136
+ zip.close
137
+ end
138
+
139
+ # Output entire document as a StringIO object
140
+ def stream
141
+ update
142
+ stream = Zip::OutputStream.write_buffer do |out|
143
+ zip.each do |entry|
144
+ next unless entry.file?
145
+
146
+ out.put_next_entry(entry.name)
147
+
148
+ if @replace[entry.name]
149
+ out.write(@replace[entry.name])
150
+ else
151
+ out.write(zip.read(entry.name))
152
+ end
153
+ end
154
+ end
155
+
156
+ stream.rewind
157
+ stream
158
+ end
159
+
160
+ alias text to_s
161
+
162
+ def replace_entry(entry_path, file_contents)
163
+ @replace[entry_path] = file_contents
164
+ end
165
+
166
+ private
167
+
168
+ def load_styles
169
+ @styles_xml = @zip.read('word/styles.xml')
170
+ @styles = Nokogiri::XML(@styles_xml)
171
+ @rels_xml = @zip.read('word/_rels/document.xml.rels')
172
+ @rels = Nokogiri::XML(@rels_xml)
173
+ rescue Errno::ENOENT => e
174
+ warn e.message
175
+ nil
176
+ end
177
+
178
+ #--
179
+ # TODO: Flesh this out to be compatible with other files
180
+ # TODO: Method to set flag on files that have been edited, probably by inserting something at the
181
+ # end of methods that make edits?
182
+ #++
183
+ def update
184
+ replace_entry 'word/document.xml', doc.serialize(save_with: 0)
185
+ end
186
+
187
+ # generate Elements::Containers::Paragraph from paragraph XML node
188
+ def parse_paragraph_from(p_node)
189
+ Elements::Containers::Paragraph.new(p_node, document_properties)
190
+ end
191
+
192
+ # generate Elements::Bookmark from bookmark XML node
193
+ def parse_bookmark_from(b_node)
194
+ Elements::Bookmark.new(b_node)
195
+ end
196
+
197
+ def parse_table_from(t_node)
198
+ Elements::Containers::Table.new(t_node)
199
+ end
200
+ end
201
+ end