docx 0.13.0 → 0.13.2
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/paragraph.rb +43 -18
- data/lib/docx/containers/text_run.rb +1 -0
- data/lib/docx/document.rb +3 -0
- data/lib/docx/elements/bookmark.rb +76 -12
- data/lib/docx/elements/element.rb +2 -1
- data/lib/docx/elements/text.rb +2 -0
- data/lib/docx/helpers.rb +1 -1
- 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: db1fd167dc6ddfecc81518f27cd78fc203d74738ffe52b2877251b2ed291c297
|
|
4
|
+
data.tar.gz: 7c2f230a44f18ceffffcbbfa55d212ebdd2a58651823aa78d303cc64493aa461
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6e1af62546207b59b6b2bcd193c83aaafc11258dcb50d7c52bcd2251b77552704b39d5667a709cdc9fdd005c37e00e3530acd9fb88df565ba10db361d6032d98
|
|
7
|
+
data.tar.gz: 7d753f33adc0795e12cea763d32e01d4cf76777d1cf7f5c8d815991efccfd0ede2b513a7a99c5558824e972e17ffd86460f661d87f7003dd916cca2cdbc06498
|
|
@@ -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,20 +85,19 @@ 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
91
|
loop do
|
|
88
|
-
|
|
89
|
-
break if runs.empty?
|
|
92
|
+
texts = @node.xpath(TEXT_XPATH).map { |t_node| Elements::Text.new(t_node) }
|
|
90
93
|
|
|
91
94
|
offsets = []
|
|
92
95
|
cursor = 0
|
|
93
|
-
|
|
96
|
+
texts.each do |text|
|
|
94
97
|
offsets << cursor
|
|
95
|
-
cursor +=
|
|
98
|
+
cursor += text.content.length
|
|
96
99
|
end
|
|
97
|
-
full_text =
|
|
100
|
+
full_text = texts.map(&:content).join
|
|
98
101
|
|
|
99
102
|
match = full_text.match(pattern, search_from)
|
|
100
103
|
break unless match
|
|
@@ -102,15 +105,21 @@ module Docx
|
|
|
102
105
|
|
|
103
106
|
match_start = match.begin(0)
|
|
104
107
|
match_end = match.end(0) # exclusive
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
+
|
|
112
|
+
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
|
|
114
123
|
|
|
115
124
|
# advance past the inserted replacement so it is not re-matched
|
|
116
125
|
search_from = match_start + replaced.length
|
|
@@ -165,6 +174,22 @@ module Docx
|
|
|
165
174
|
|
|
166
175
|
private
|
|
167
176
|
|
|
177
|
+
# The w:t read by #text (those of the runs and hyperlink runs), in
|
|
178
|
+
# document order.
|
|
179
|
+
TEXT_XPATH = 'w:r/w:t|w:hyperlink/w:r/w:t'
|
|
180
|
+
# Line breaks and tabs that sit between those w:t.
|
|
181
|
+
SEPARATOR_XPATH = %w[br tab cr].map { |tag| "w:r/w:#{tag}|w:hyperlink/w:r/w:#{tag}" }.join('|')
|
|
182
|
+
private_constant :TEXT_XPATH, :SEPARATOR_XPATH
|
|
183
|
+
|
|
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) }
|
|
191
|
+
end
|
|
192
|
+
|
|
168
193
|
def style_property
|
|
169
194
|
properties&.at_xpath('w:pStyle') || properties&.add_child('<w:pStyle/>').first
|
|
170
195
|
end
|
data/lib/docx/document.rb
CHANGED
|
@@ -113,6 +113,9 @@ module Docx
|
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
def hyperlink_relationships
|
|
116
|
+
# @rels is nil when the document has no relationships part (#192)
|
|
117
|
+
return Nokogiri::XML::NodeSet.new(Nokogiri::XML::Document.new) unless @rels
|
|
118
|
+
|
|
116
119
|
@rels.xpath("//xmlns:Relationship[contains(@Type,'hyperlink')]")
|
|
117
120
|
end
|
|
118
121
|
|
|
@@ -51,28 +51,92 @@ module Docx
|
|
|
51
51
|
|
|
52
52
|
# Get text run immediately prior to bookmark node
|
|
53
53
|
def get_run_before
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
if enclosing_paragraph
|
|
55
|
+
# at_xpath returns the first match found and preceding-sibling returns siblings in the
|
|
56
|
+
# order they appear in the document not the order as they appear when moving out from
|
|
57
|
+
# the starting node
|
|
58
|
+
unless (r_nodes = @node.xpath("./preceding-sibling::w:r")).empty?
|
|
59
|
+
return Containers::TextRun.new(r_nodes.last)
|
|
60
|
+
end
|
|
61
|
+
|
|
61
62
|
new_r = Containers::TextRun.create_with(self)
|
|
62
63
|
new_r.insert_before(self)
|
|
63
|
-
new_r
|
|
64
|
+
return new_r
|
|
64
65
|
end
|
|
66
|
+
|
|
67
|
+
# Block-level bookmark (e.g. Google Docs places bookmarkStart/End directly
|
|
68
|
+
# under w:body). Add a run to the preceding paragraph, or to a new
|
|
69
|
+
# paragraph before the bookmark, so the run lives inside a w:p.
|
|
70
|
+
run_at_paragraph_end(@node.xpath("./preceding-sibling::w:p").last) ||
|
|
71
|
+
run_in_new_paragraph { |paragraph| @node.add_previous_sibling(paragraph) }
|
|
65
72
|
end
|
|
66
73
|
|
|
67
74
|
# Get text run immediately after bookmark node
|
|
68
75
|
def get_run_after
|
|
69
|
-
if
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
if enclosing_paragraph
|
|
77
|
+
if (r_node = @node.at_xpath("./following-sibling::w:r"))
|
|
78
|
+
return Containers::TextRun.new(r_node)
|
|
79
|
+
end
|
|
80
|
+
|
|
72
81
|
new_r = Containers::TextRun.create_with(self)
|
|
73
82
|
new_r.insert_after(self)
|
|
74
|
-
new_r
|
|
83
|
+
return new_r
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Block-level bookmark: add a run to the following paragraph, or to a new
|
|
87
|
+
# paragraph after the bookmark.
|
|
88
|
+
run_at_paragraph_start(@node.at_xpath("./following-sibling::w:p")) ||
|
|
89
|
+
run_in_new_paragraph { |paragraph| @node.add_next_sibling(paragraph) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Override Element#parent_paragraph so insert_multiple_lines also works for
|
|
93
|
+
# block-level bookmarks. For those we fill a fresh paragraph inserted at the
|
|
94
|
+
# bookmark, rather than an adjacent existing paragraph, so we neither crash
|
|
95
|
+
# (no enclosing paragraph) nor overwrite unrelated content.
|
|
96
|
+
def parent_paragraph
|
|
97
|
+
return Containers::Paragraph.new(enclosing_paragraph) if enclosing_paragraph
|
|
98
|
+
|
|
99
|
+
paragraph = Nokogiri::XML::Node.new("w:p", @node.document)
|
|
100
|
+
@node.add_next_sibling(paragraph)
|
|
101
|
+
Containers::Paragraph.new(paragraph)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
# The w:p the bookmark sits inside, or nil when it is block-level.
|
|
107
|
+
def enclosing_paragraph
|
|
108
|
+
@node.at_xpath("./parent::w:p")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A new run inserted at the start of paragraph_node (after w:pPr if present).
|
|
112
|
+
# Returns nil when paragraph_node is nil.
|
|
113
|
+
def run_at_paragraph_start(paragraph_node)
|
|
114
|
+
return nil unless paragraph_node
|
|
115
|
+
|
|
116
|
+
new_r = Containers::TextRun.create_with(self)
|
|
117
|
+
if (props = paragraph_node.at_xpath("w:pPr"))
|
|
118
|
+
props.add_next_sibling(new_r.node)
|
|
119
|
+
else
|
|
120
|
+
paragraph_node.prepend_child(new_r.node)
|
|
75
121
|
end
|
|
122
|
+
new_r
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# A new run appended to the end of paragraph_node. nil when it is nil.
|
|
126
|
+
def run_at_paragraph_end(paragraph_node)
|
|
127
|
+
return nil unless paragraph_node
|
|
128
|
+
|
|
129
|
+
Containers::TextRun.create_within(Containers::Paragraph.new(paragraph_node))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Create a run wrapped in a fresh w:p; the block positions that paragraph
|
|
133
|
+
# relative to the bookmark.
|
|
134
|
+
def run_in_new_paragraph
|
|
135
|
+
new_r = Containers::TextRun.create_with(self)
|
|
136
|
+
paragraph = Nokogiri::XML::Node.new("w:p", @node.document)
|
|
137
|
+
paragraph.add_child(new_r.node)
|
|
138
|
+
yield paragraph
|
|
139
|
+
new_r
|
|
76
140
|
end
|
|
77
141
|
end
|
|
78
142
|
end
|
|
@@ -42,7 +42,8 @@ module Docx
|
|
|
42
42
|
|
|
43
43
|
# Insert node as first child (after properties)
|
|
44
44
|
def prepend_to(element)
|
|
45
|
-
|
|
45
|
+
properties = element.properties if element.respond_to?(:properties)
|
|
46
|
+
@node = properties ? properties.add_next_sibling(@node) : element.node.prepend_child(@node)
|
|
46
47
|
self
|
|
47
48
|
end
|
|
48
49
|
|
data/lib/docx/elements/text.rb
CHANGED
data/lib/docx/helpers.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Docx
|
|
|
4
4
|
# than the default inspect method. But you can still get the default inspect method by passing
|
|
5
5
|
# true as the first argument.
|
|
6
6
|
def inspect(full = false)
|
|
7
|
-
return(
|
|
7
|
+
return super() if full
|
|
8
8
|
|
|
9
9
|
variable_values =
|
|
10
10
|
instance_variables.map do |var|
|
data/lib/docx/version.rb
CHANGED