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 +4 -4
- data/lib/docx/containers/container.rb +13 -0
- data/lib/docx/containers/paragraph.rb +38 -39
- data/lib/docx/containers/text_run.rb +25 -6
- data/lib/docx/elements/bookmark.rb +34 -4
- data/lib/docx/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6559deefb9d0bfae3c8101274040ebf8968827360a3f871d2f1d23440e6bbcb
|
|
4
|
+
data.tar.gz: 4ad8c35b486f9fb9aeac833f78f9425dbac5364f4c1d0d16e168685434695210
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
#
|
|
185
|
-
def
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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