docx 0.13.1 → 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 +56 -32
- data/lib/docx/containers/text_run.rb +25 -6
- data/lib/docx/elements/bookmark.rb +34 -4
- data/lib/docx/elements/text.rb +2 -0
- 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
|
|
@@ -70,9 +70,13 @@ module Docx
|
|
|
70
70
|
# such as "{{fi", "rst_na", "me}}"). The per-run TextRun#substitute cannot
|
|
71
71
|
# match those, but this can, because it joins the runs first.
|
|
72
72
|
#
|
|
73
|
-
# The
|
|
74
|
-
#
|
|
75
|
-
#
|
|
73
|
+
# The substitution works on the paragraph's w:t nodes, so it also handles
|
|
74
|
+
# runs holding several w:t (LibreOffice writes "text<w:br/>text" in one
|
|
75
|
+
# run) and placeholders that start inside a hyperlink. The replacement
|
|
76
|
+
# goes into the w:t holding the first matched character, so that run's
|
|
77
|
+
# formatting is used; the rest of the match is cut from the following
|
|
78
|
+
# w:t, and line breaks and tabs inside the match are dropped. Text,
|
|
79
|
+
# breaks and tabs outside the match stay where they are.
|
|
76
80
|
#
|
|
77
81
|
# +pattern+ may be a String or a Regexp; +replacement+ follows String#sub
|
|
78
82
|
# semantics, so capture-group backreferences (e.g. '\1') work with a Regexp.
|
|
@@ -81,39 +85,15 @@ module Docx
|
|
|
81
85
|
# paragraph.substitute('{{first_name}}', 'Jane') # => "Hello Jane!"
|
|
82
86
|
# paragraph.substitute(/\{\{(\w+)\}\}/, 'value of \1')
|
|
83
87
|
#
|
|
84
|
-
# See https://github.com/ruby-docx/docx/issues/147
|
|
88
|
+
# See https://github.com/ruby-docx/docx/issues/147 and #187
|
|
85
89
|
def substitute(pattern, replacement)
|
|
86
90
|
search_from = 0
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
offsets = []
|
|
92
|
-
cursor = 0
|
|
93
|
-
runs.each do |run|
|
|
94
|
-
offsets << cursor
|
|
95
|
-
cursor += run.text.length
|
|
96
|
-
end
|
|
97
|
-
full_text = runs.map(&:text).join
|
|
98
|
-
|
|
99
|
-
match = full_text.match(pattern, search_from)
|
|
100
|
-
break unless match
|
|
101
|
-
break if match.end(0) == match.begin(0) # ignore empty matches
|
|
102
|
-
|
|
103
|
-
match_start = match.begin(0)
|
|
104
|
-
match_end = match.end(0) # exclusive
|
|
105
|
-
first = offsets.rindex { |offset| offset <= match_start }
|
|
106
|
-
last = offsets.rindex { |offset| offset < match_end }
|
|
107
|
-
|
|
108
|
-
combined = runs[first..last].map(&:text).join
|
|
109
|
-
local_start = match_start - offsets[first]
|
|
110
|
-
local_end = match_end - offsets[first]
|
|
111
|
-
replaced = combined[local_start...local_end].sub(pattern, replacement)
|
|
112
|
-
runs[first].text = combined[0...local_start] + replaced + combined[local_end..-1]
|
|
113
|
-
((first + 1)..last).each { |index| runs[index].text = '' }
|
|
91
|
+
while (match = find_match(pattern, search_from))
|
|
92
|
+
replaced = match[0].sub(pattern, replacement)
|
|
93
|
+
replace_text_range(match.begin(0)...match.end(0), replaced)
|
|
114
94
|
|
|
115
95
|
# advance past the inserted replacement so it is not re-matched
|
|
116
|
-
search_from =
|
|
96
|
+
search_from = match.begin(0) + replaced.length
|
|
117
97
|
end
|
|
118
98
|
self
|
|
119
99
|
end
|
|
@@ -165,6 +145,50 @@ module Docx
|
|
|
165
145
|
|
|
166
146
|
private
|
|
167
147
|
|
|
148
|
+
# The w:t read by #text (those of the runs and hyperlink runs), in
|
|
149
|
+
# document order.
|
|
150
|
+
TEXT_XPATH = 'w:r/w:t|w:hyperlink/w:r/w:t'
|
|
151
|
+
# Line breaks and tabs that sit between those w:t.
|
|
152
|
+
SEPARATOR_XPATH = %w[br tab cr].map { |tag| "w:r/w:#{tag}|w:hyperlink/w:r/w:#{tag}" }.join('|')
|
|
153
|
+
private_constant :TEXT_XPATH, :SEPARATOR_XPATH
|
|
154
|
+
|
|
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)
|
|
190
|
+
end
|
|
191
|
+
|
|
168
192
|
def style_property
|
|
169
193
|
properties&.at_xpath('w:pStyle') || properties&.add_child('<w:pStyle/>').first
|
|
170
194
|
end
|
|
@@ -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/elements/text.rb
CHANGED
data/lib/docx/version.rb
CHANGED