docx 0.2.03 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -13,17 +13,23 @@ module Docx
13
13
  underline: false
14
14
  }
15
15
 
16
- TAG = 'r'
16
+ def self.tag
17
+ 'r'
18
+ end
17
19
 
18
20
  attr_reader :text
19
21
  attr_reader :formatting
20
22
 
21
- def initialize(node)
23
+ def initialize(node, document_properties = {})
22
24
  @node = node
23
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
+
24
28
  @properties_tag = 'rPr'
25
29
  @text = parse_text || ''
26
30
  @formatting = parse_formatting || DEFAULT_FORMATTING
31
+ @document_properties = document_properties
32
+ @font_size = @document_properties[:font_size]
27
33
  end
28
34
 
29
35
  # Set text of text run
@@ -41,6 +47,13 @@ module Docx
41
47
  @text_nodes.map(&:content).join('')
42
48
  end
43
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
+
44
57
  def parse_formatting
45
58
  {
46
59
  italic: !@node.xpath('.//w:i').empty?,
@@ -52,7 +65,21 @@ module Docx
52
65
  def to_s
53
66
  @text
54
67
  end
55
-
68
+
69
+ # Return text as a HTML fragment with formatting based on properties.
70
+ def to_html
71
+ html = @text
72
+ html = html_tag(:em, content: html) if italicized?
73
+ html = html_tag(:strong, content: html) if bolded?
74
+ styles = {}
75
+ styles['text-decoration'] = 'underline' if underlined?
76
+ # No need to be granular with font size down to the span level if it doesn't vary.
77
+ styles['font-size'] = "#{font_size}pt" if font_size != @font_size
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?
80
+ return html
81
+ end
82
+
56
83
  def italicized?
57
84
  @formatting[:italic]
58
85
  end
@@ -64,6 +91,23 @@ module Docx
64
91
  def underlined?
65
92
  @formatting[:underline]
66
93
  end
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
+
107
+ def font_size
108
+ size_tag = @node.xpath('w:rPr//w:sz').first
109
+ size_tag ? size_tag.attributes['val'].value.to_i / 2 : @font_size
110
+ end
67
111
  end
68
112
  end
69
113
  end
@@ -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
@@ -1,87 +1,202 @@
1
- require 'docx/parser'
2
- require 'zip/zip'
3
-
4
- module Docx
5
- # The Document class wraps around a docx file and provides methods to
6
- # interface with it.
7
- #
8
- # # get a Docx::Document for a docx file in the local directory
9
- # doc = Docx::Document.open("test.docx")
10
- #
11
- # # get the text from the document
12
- # puts doc.text
13
- #
14
- # # do the same thing in a block
15
- # Docx::Document.open("test.docx") do |d|
16
- # puts d.text
17
- # end
18
- class Document
19
- delegate :paragraphs, :bookmarks, :to => :@parser
20
- delegate :doc, :xml, :zip, :to => :@parser
21
- def initialize(path, &block)
22
- @replace = {}
23
- if block_given?
24
- @parser = Parser.new(File.expand_path(path), &block)
25
- else
26
- @parser = Parser.new(File.expand_path(path))
27
- end
28
- end
29
-
30
- # 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.
31
- # call-seq:
32
- # open(filepath) => file
33
- # open(filepath) {|file| block } => obj
34
- def self.open(path, &block)
35
- self.new(path, &block)
36
- end
37
-
38
- ##
39
- # *Deprecated*
40
- #
41
- # Iterates over paragraphs within document
42
- # call-seq:
43
- # each_paragraph => Enumerator
44
- def each_paragraph
45
- paragraphs.each { |p| yield(p) }
46
- end
47
-
48
- # call-seq:
49
- # to_s -> string
50
- def to_s
51
- paragraphs.map(&:to_s).join("\n")
52
- end
53
-
54
- # Save document to provided path
55
- # call-seq:
56
- # save(filepath) => void
57
- def save(path)
58
- update
59
- Zip::ZipOutputStream.open(path) do |out|
60
- zip.each do |entry|
61
- out.put_next_entry(entry.name)
62
-
63
- if @replace[entry.name]
64
- out.write(@replace[entry.name])
65
- else
66
- out.write(zip.read(entry.name))
67
- end
68
- end
69
- end
70
- zip.close
71
- end
72
-
73
- alias_method :text, :to_s
74
-
75
- private
76
-
77
- #--
78
- # TODO: Flesh this out to be compatible with other files
79
- # TODO: Method to set flag on files that have been edited, probably by inserting something at the
80
- # end of methods that make edits?
81
- #++
82
- def update
83
- @replace["word/document.xml"] = doc.serialize :save_with => 0
84
- end
85
-
86
- end
87
- 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.find_entry('word/document.xml')
34
+ document ||= @zip.find_entry('word/document2.xml')
35
+ raise Errno::ENOENT if document.nil?
36
+
37
+ @document_xml = document.get_input_stream.read
38
+ @doc = Nokogiri::XML(@document_xml)
39
+ load_styles
40
+ yield(self) if block_given?
41
+ ensure
42
+ @zip.close
43
+ end
44
+
45
+ # This stores the current global document properties, for now
46
+ def document_properties
47
+ {
48
+ font_size: font_size,
49
+ hyperlinks: hyperlinks
50
+ }
51
+ end
52
+
53
+ # 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.
54
+ # call-seq:
55
+ # open(filepath) => file
56
+ # open(filepath) {|file| block } => obj
57
+ def self.open(path, &block)
58
+ new(path, &block)
59
+ end
60
+
61
+ def paragraphs
62
+ @doc.xpath('//w:document//w:body/w:p').map { |p_node| parse_paragraph_from p_node }
63
+ end
64
+
65
+ def bookmarks
66
+ bkmrks_hsh = {}
67
+ bkmrks_ary = @doc.xpath('//w:bookmarkStart').map { |b_node| parse_bookmark_from b_node }
68
+ # auto-generated by office 2010
69
+ bkmrks_ary.reject! { |b| b.name == '_GoBack' }
70
+ bkmrks_ary.each { |b| bkmrks_hsh[b.name] = b }
71
+ bkmrks_hsh
72
+ end
73
+
74
+ def tables
75
+ @doc.xpath('//w:document//w:body//w:tbl').map { |t_node| parse_table_from t_node }
76
+ end
77
+
78
+ # Some documents have this set, others don't.
79
+ # Values are returned as half-points, so to get points, that's why it's divided by 2.
80
+ def font_size
81
+ return nil unless @styles
82
+
83
+ size_tag = @styles.xpath('//w:docDefaults//w:rPrDefault//w:rPr//w:sz').first
84
+ size_tag ? size_tag.attributes['val'].value.to_i / 2 : nil
85
+ end
86
+
87
+ # Hyperlink targets are extracted from the document.xml.rels file
88
+ def hyperlinks
89
+ hyperlink_relationships.each_with_object({}) do |rel, hash|
90
+ hash[rel.attributes['Id'].value] = rel.attributes['Target'].value
91
+ end
92
+ end
93
+
94
+ def hyperlink_relationships
95
+ @rels.xpath("//xmlns:Relationship[contains(@Type,'hyperlink')]")
96
+ end
97
+
98
+ ##
99
+ # *Deprecated*
100
+ #
101
+ # Iterates over paragraphs within document
102
+ # call-seq:
103
+ # each_paragraph => Enumerator
104
+ def each_paragraph
105
+ paragraphs.each { |p| yield(p) }
106
+ end
107
+
108
+ # call-seq:
109
+ # to_s -> string
110
+ def to_s
111
+ paragraphs.map(&:to_s).join("\n")
112
+ end
113
+
114
+ # Output entire document as a String HTML fragment
115
+ def to_html
116
+ paragraphs.map(&:to_html).join("\n")
117
+ end
118
+
119
+ # Save document to provided path
120
+ # call-seq:
121
+ # save(filepath) => void
122
+ def save(path)
123
+ update
124
+ Zip::OutputStream.open(path) do |out|
125
+ zip.each do |entry|
126
+ next unless entry.file?
127
+
128
+ out.put_next_entry(entry.name)
129
+
130
+ if @replace[entry.name]
131
+ out.write(@replace[entry.name])
132
+ else
133
+ out.write(zip.read(entry.name))
134
+ end
135
+ end
136
+ end
137
+ zip.close
138
+ end
139
+
140
+ # Output entire document as a StringIO object
141
+ def stream
142
+ update
143
+ stream = Zip::OutputStream.write_buffer do |out|
144
+ zip.each do |entry|
145
+ next unless entry.file?
146
+
147
+ out.put_next_entry(entry.name)
148
+
149
+ if @replace[entry.name]
150
+ out.write(@replace[entry.name])
151
+ else
152
+ out.write(zip.read(entry.name))
153
+ end
154
+ end
155
+ end
156
+
157
+ stream.rewind
158
+ stream
159
+ end
160
+
161
+ alias text to_s
162
+
163
+ def replace_entry(entry_path, file_contents)
164
+ @replace[entry_path] = file_contents
165
+ end
166
+
167
+ private
168
+
169
+ def load_styles
170
+ @styles_xml = @zip.read('word/styles.xml')
171
+ @styles = Nokogiri::XML(@styles_xml)
172
+ @rels_xml = @zip.read('word/_rels/document.xml.rels')
173
+ @rels = Nokogiri::XML(@rels_xml)
174
+ rescue Errno::ENOENT => e
175
+ warn e.message
176
+ nil
177
+ end
178
+
179
+ #--
180
+ # TODO: Flesh this out to be compatible with other files
181
+ # TODO: Method to set flag on files that have been edited, probably by inserting something at the
182
+ # end of methods that make edits?
183
+ #++
184
+ def update
185
+ replace_entry 'word/document.xml', doc.serialize(save_with: 0)
186
+ end
187
+
188
+ # generate Elements::Containers::Paragraph from paragraph XML node
189
+ def parse_paragraph_from(p_node)
190
+ Elements::Containers::Paragraph.new(p_node, document_properties)
191
+ end
192
+
193
+ # generate Elements::Bookmark from bookmark XML node
194
+ def parse_bookmark_from(b_node)
195
+ Elements::Bookmark.new(b_node)
196
+ end
197
+
198
+ def parse_table_from(t_node)
199
+ Elements::Containers::Table.new(t_node)
200
+ end
201
+ end
202
+ end