docx 0.13.2 → 0.13.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db1fd167dc6ddfecc81518f27cd78fc203d74738ffe52b2877251b2ed291c297
4
- data.tar.gz: 7c2f230a44f18ceffffcbbfa55d212ebdd2a58651823aa78d303cc64493aa461
3
+ metadata.gz: a6559deefb9d0bfae3c8101274040ebf8968827360a3f871d2f1d23440e6bbcb
4
+ data.tar.gz: 4ad8c35b486f9fb9aeac833f78f9425dbac5364f4c1d0d16e168685434695210
5
5
  SHA512:
6
- metadata.gz: 6e1af62546207b59b6b2bcd193c83aaafc11258dcb50d7c52bcd2251b77552704b39d5667a709cdc9fdd005c37e00e3530acd9fb88df565ba10db361d6032d98
7
- data.tar.gz: 7d753f33adc0795e12cea763d32e01d4cf76777d1cf7f5c8d815991efccfd0ede2b513a7a99c5558824e972e17ffd86460f661d87f7003dd916cca2cdbc06498
6
+ metadata.gz: bb062084c4bd71c5ca72fca4f7eae5fe17869eca2533c5d668a4cd8c2963544fff8737b836676ca22073c19ec6fca7f9a78bf0ad5789f7172d149ad75d87a455
7
+ data.tar.gz: bea34b169aa1c320cab0d9bd8de4a6a02e91e50a327d60192d9e9e5f53e8d9c977a7b713d0cd5be4d3ba05573f0850481cc69c707b96edf29609a4be8e08cd14
@@ -18,6 +18,19 @@ module Docx
18
18
  def remove!
19
19
  @node.remove
20
20
  end
21
+
22
+ private
23
+
24
+ # Remove the line breaks and tabs (nodes matched by +separator_xpath+)
25
+ # lying between the w:t nodes +first_t+ and +last_t+, which are matched
26
+ # by +text_xpath+.
27
+ def remove_separators_between(first_t, last_t, text_xpath, separator_xpath)
28
+ separators = @node.xpath(separator_xpath).to_a
29
+ nodes = @node.xpath("#{text_xpath}|#{separator_xpath}").to_a
30
+ from = nodes.index(first_t)
31
+ to = nodes.index(last_t)
32
+ nodes[(from + 1)...to].each { |node| node.remove if separators.include?(node) }
33
+ end
21
34
  end
22
35
  end
23
36
  end
@@ -88,41 +88,12 @@ module Docx
88
88
  # See https://github.com/ruby-docx/docx/issues/147 and #187
89
89
  def substitute(pattern, replacement)
90
90
  search_from = 0
91
- loop do
92
- texts = @node.xpath(TEXT_XPATH).map { |t_node| Elements::Text.new(t_node) }
93
-
94
- offsets = []
95
- cursor = 0
96
- texts.each do |text|
97
- offsets << cursor
98
- cursor += text.content.length
99
- end
100
- full_text = texts.map(&:content).join
101
-
102
- match = full_text.match(pattern, search_from)
103
- break unless match
104
- break if match.end(0) == match.begin(0) # ignore empty matches
105
-
106
- match_start = match.begin(0)
107
- match_end = match.end(0) # exclusive
108
- # the w:t holding the first and the last matched character
109
- first = texts.each_index.find { |i| offsets[i] + texts[i].content.length > match_start }
110
- last = texts.each_index.find { |i| offsets[i] + texts[i].content.length >= match_end }
111
-
91
+ while (match = find_match(pattern, search_from))
112
92
  replaced = match[0].sub(pattern, replacement)
113
- head = texts[first].content[0...(match_start - offsets[first])]
114
- tail = texts[last].content[(match_end - offsets[last])..]
115
- if first == last
116
- texts[first].content = head + replaced + tail
117
- else
118
- texts[first].content = head + replaced
119
- texts[(first + 1)...last].each { |text| text.content = '' }
120
- texts[last].content = tail
121
- remove_separators_between(texts[first].node, texts[last].node)
122
- end
93
+ replace_text_range(match.begin(0)...match.end(0), replaced)
123
94
 
124
95
  # advance past the inserted replacement so it is not re-matched
125
- search_from = match_start + replaced.length
96
+ search_from = match.begin(0) + replaced.length
126
97
  end
127
98
  self
128
99
  end
@@ -181,13 +152,41 @@ module Docx
181
152
  SEPARATOR_XPATH = %w[br tab cr].map { |tag| "w:r/w:#{tag}|w:hyperlink/w:r/w:#{tag}" }.join('|')
182
153
  private_constant :TEXT_XPATH, :SEPARATOR_XPATH
183
154
 
184
- # Remove the line breaks and tabs lying between two w:t of a match.
185
- def remove_separators_between(first_t, last_t)
186
- separators = @node.xpath(SEPARATOR_XPATH).to_a
187
- nodes = @node.xpath("#{TEXT_XPATH}|#{SEPARATOR_XPATH}").to_a
188
- from = nodes.index(first_t)
189
- to = nodes.index(last_t)
190
- nodes[(from + 1)...to].each { |node| node.remove if separators.include?(node) }
155
+ # The paragraph's w:t, in document order; their contents joined make up #text.
156
+ def text_nodes
157
+ @node.xpath(TEXT_XPATH).map { |t_node| Elements::Text.new(t_node) }
158
+ end
159
+
160
+ # The first match of +pattern+ in the paragraph text at or after +from+,
161
+ # or nil when there is none. An empty match also gives nil, so that
162
+ # #substitute stops rather than loop on it.
163
+ def find_match(pattern, from)
164
+ match = text_nodes.map(&:content).join.match(pattern, from)
165
+ match unless match.nil? || match[0].empty?
166
+ end
167
+
168
+ # Replace the characters in +range+ of the paragraph text with +string+.
169
+ # The string goes into the w:t holding the range's first character; the
170
+ # rest of the range is cut from the following w:t, and line breaks and
171
+ # tabs inside it are removed.
172
+ def replace_text_range(range, string)
173
+ texts = text_nodes
174
+ # boundaries[i]...boundaries[i + 1] is where texts[i] sits in the text
175
+ boundaries = texts.each_with_object([0]) { |text, acc| acc << acc.last + text.content.length }
176
+ first = texts.each_index.find { |i| boundaries[i + 1] > range.begin }
177
+ last = texts.each_index.find { |i| boundaries[i + 1] >= range.end }
178
+
179
+ head = texts[first].content[0...(range.begin - boundaries[first])]
180
+ tail = texts[last].content[(range.end - boundaries[last])..]
181
+ if first == last
182
+ texts[first].content = head + string + tail
183
+ return
184
+ end
185
+
186
+ texts[first].content = head + string
187
+ texts[(first + 1)...last].each { |text| text.content = '' }
188
+ texts[last].content = tail
189
+ remove_separators_between(texts[first].node, texts[last].node, TEXT_XPATH, SEPARATOR_XPATH)
191
190
  end
192
191
 
193
192
  def style_property
@@ -14,6 +14,12 @@ module Docx
14
14
  strike: false
15
15
  }
16
16
 
17
+ # The w:t of the run, or of the runs of a hyperlink, in document order.
18
+ TEXT_XPATH = 'w:t|w:r/w:t'
19
+ # Line breaks and tabs that sit between those w:t.
20
+ SEPARATOR_XPATH = %w[br tab cr].map { |tag| "w:#{tag}|w:r/w:#{tag}" }.join('|')
21
+ private_constant :TEXT_XPATH, :SEPARATOR_XPATH
22
+
17
23
  def self.tag
18
24
  'r'
19
25
  end
@@ -23,8 +29,7 @@ module Docx
23
29
 
24
30
  def initialize(node, document_properties = {})
25
31
  @node = node
26
- @text_nodes = @node.xpath('w:t').map {|t_node| Elements::Text.new(t_node) }
27
- @text_nodes = @node.xpath('w:t|w:r/w:t').map {|t_node| Elements::Text.new(t_node) }
32
+ @text_nodes = @node.xpath(TEXT_XPATH).map {|t_node| Elements::Text.new(t_node) }
28
33
 
29
34
  @properties_tag = 'rPr'
30
35
  @text = parse_text || ''
@@ -33,14 +38,28 @@ module Docx
33
38
  @font_size = @document_properties[:font_size]
34
39
  end
35
40
 
36
- # Set text of text run
41
+ # Set text of text run.
42
+ #
43
+ # The text from the first through the last w:t is replaced: the content
44
+ # goes into the first w:t, and the other w:t and the line breaks and
45
+ # tabs between them are removed (#text never includes breaks or tabs,
46
+ # so a plain string cannot say where they belong). Anything before the
47
+ # first or after the last w:t, such as a trailing page break, is kept.
48
+ # To replace part of a paragraph while keeping its line breaks in place,
49
+ # use Paragraph#substitute.
37
50
  def text=(content)
38
- if @text_nodes.size == 1
39
- @text_nodes.first.content = content
40
- elsif @text_nodes.empty?
51
+ if @text_nodes.empty?
41
52
  new_t = Elements::Text.create_within(self)
42
53
  new_t.content = content
43
54
  @text_nodes << new_t
55
+ else
56
+ first, *rest = @text_nodes
57
+ first.content = content
58
+ unless rest.empty?
59
+ remove_separators_between(first.node, rest.last.node, TEXT_XPATH, SEPARATOR_XPATH)
60
+ rest.each { |text_node| text_node.node.remove }
61
+ @text_nodes = [first]
62
+ end
44
63
  end
45
64
  reset_text
46
65
  end
@@ -17,14 +17,12 @@ module Docx
17
17
 
18
18
  # Insert text before bookmarkStart node
19
19
  def insert_text_before(text)
20
- text_run = get_run_before
21
- text_run.text = "#{text_run.text}#{text}"
20
+ append_to_run(get_run_before, text)
22
21
  end
23
22
 
24
23
  # Insert text after bookmarkStart node
25
24
  def insert_text_after(text)
26
- text_run = get_run_after
27
- text_run.text = "#{text}#{text_run.text}"
25
+ prepend_to_run(get_run_after, text)
28
26
  end
29
27
 
30
28
  # insert multiple lines starting with paragraph containing bookmark node.
@@ -103,6 +101,38 @@ module Docx
103
101
 
104
102
  private
105
103
 
104
+ # A run's text, line breaks and tabs, in document order.
105
+ RUN_CONTENT_XPATH = 'w:t|w:br|w:tab|w:cr'
106
+ private_constant :RUN_CONTENT_XPATH
107
+
108
+ # Add text at the very end of the run, which sits just before the
109
+ # bookmark: to its last w:t, or to a new w:t when the run ends with a
110
+ # break or tab. The rest of the run is left as it is (#197).
111
+ def append_to_run(run, text)
112
+ last = run.node.xpath(RUN_CONTENT_XPATH).last
113
+ if last && run.node.xpath('w:t').include?(last)
114
+ Text.new(last).content = last.content + text
115
+ else
116
+ Text.create_within(run).content = text
117
+ end
118
+ end
119
+
120
+ # Add text at the very start of the run, which sits just after the
121
+ # bookmark: to its first w:t, or to a new w:t when the run starts with
122
+ # a break or tab. The rest of the run is left as it is (#197).
123
+ def prepend_to_run(run, text)
124
+ first = run.node.xpath(RUN_CONTENT_XPATH).first
125
+ if first && run.node.xpath('w:t').include?(first)
126
+ Text.new(first).content = text + first.content
127
+ elsif first
128
+ new_t = Text.create_with(run)
129
+ new_t.node = first.add_previous_sibling(new_t.node)
130
+ new_t.content = text
131
+ else
132
+ Text.create_within(run).content = text
133
+ end
134
+ end
135
+
106
136
  # The w:p the bookmark sits inside, or nil when it is block-level.
107
137
  def enclosing_paragraph
108
138
  @node.at_xpath("./parent::w:p")
data/lib/docx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docx #:nodoc:
4
- VERSION = '0.13.2'
4
+ VERSION = '0.13.3'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.2
4
+ version: 0.13.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christopher Hunt