uniword 1.6.1 → 1.7.0
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/uniword/builder/chart_builder.rb +129 -134
- data/lib/uniword/diff/package_differ.rb +16 -15
- data/lib/uniword/docx/package_integrity_checker.rb +10 -11
- data/lib/uniword/docx/reconciler/body.rb +1 -0
- data/lib/uniword/docx/reconciler/helpers.rb +27 -12
- data/lib/uniword/docx/reconciler/parts.rb +5 -0
- data/lib/uniword/infrastructure/mime_packager.rb +3 -10
- data/lib/uniword/infrastructure/mime_parser.rb +23 -1
- data/lib/uniword/math/plurimath_adapter.rb +28 -24
- data/lib/uniword/math_equation.rb +1 -1
- data/lib/uniword/mhtml/html_part.rb +19 -13
- data/lib/uniword/mhtml/math_converter.rb +18 -12
- data/lib/uniword/mhtml/mime_part.rb +5 -16
- data/lib/uniword/ooxml/element_order.rb +21 -0
- data/lib/uniword/ooxml/schema/element_serializer.rb +27 -35
- data/lib/uniword/streaming_parser.rb +22 -20
- data/lib/uniword/theme/theme_xml_parser.rb +3 -3
- data/lib/uniword/transformation/html_element_builder.rb +62 -48
- data/lib/uniword/transformation/html_formatting_mapper.rb +3 -3
- data/lib/uniword/transformation/html_to_ooxml_converter.rb +11 -11
- data/lib/uniword/transformation/mhtml_element_renderer.rb +1 -2
- data/lib/uniword/validation/schema_registry.rb +9 -16
- data/lib/uniword/validation/validators/xml_schema_validator.rb +75 -26
- data/lib/uniword/validation/verify_orchestrator.rb +1 -1
- data/lib/uniword/version.rb +1 -1
- data/lib/uniword/wp_drawing/position_h.rb +1 -1
- data/lib/uniword/wp_drawing/position_v.rb +1 -1
- data/lib/uniword/wp_drawing/size_rel_h.rb +1 -1
- data/lib/uniword/wp_drawing/size_rel_v.rb +1 -1
- data/lib/uniword.rb +4 -8
- metadata +8 -9
- data/lib/uniword/infrastructure/xml_namespace_normalizer.rb +0 -107
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34ddbf784b5b85ba8aedc6b4df9b255763e2b8014bcdea0124563e261555b32b
|
|
4
|
+
data.tar.gz: e4ef3fce0529a6db2642cbf547c28628512834339ce636f15d90c30dccba7eb2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae06c93c3b13ffffc70b0bddc7137945e4a035f2a4f75e315406547166c4423cdfe8ff8835b47ae5df50be9c6a021f500b89bd17512ae646cb3dd4c890bf8d8b
|
|
7
|
+
data.tar.gz: 7a37bcf5e4cc1ece3a92ad962757a48613be7f960890bd52017dcf8343bb1989172c7b9bd931a384dfe4bcee58c24de27b80b76e9dd4d3eca09280b078f8e3f1
|
|
@@ -28,13 +28,16 @@ module Uniword
|
|
|
28
28
|
# end
|
|
29
29
|
class ChartBuilder
|
|
30
30
|
CHART_NS = "http://schemas.openxmlformats.org/drawingml/2006/chart"
|
|
31
|
+
DRAWINGML_MAIN_NS =
|
|
32
|
+
"http://schemas.openxmlformats.org/drawingml/2006/main"
|
|
33
|
+
OFFICE_DOCUMENT_RELS_NS =
|
|
34
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
31
35
|
CHART_REL_TYPE = Ooxml::PartRegistry.find_by_key(:chart).rel_type
|
|
32
36
|
CHART_CONTENT_TYPE =
|
|
33
37
|
Ooxml::PartRegistry.find_by_key(:chart).content_type
|
|
34
38
|
|
|
35
|
-
attr_reader :chart_type, :title_text, :series_list
|
|
36
|
-
|
|
37
|
-
attr_reader :show_legend, :width, :height, :series_list
|
|
39
|
+
attr_reader :chart_type, :title_text, :series_list, :show_legend, :width,
|
|
40
|
+
:height
|
|
38
41
|
|
|
39
42
|
def initialize(chart_type: :bar)
|
|
40
43
|
@chart_type = chart_type
|
|
@@ -101,16 +104,15 @@ module Uniword
|
|
|
101
104
|
#
|
|
102
105
|
# @return [String] Chart XML
|
|
103
106
|
def build_xml
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
builder.to_xml
|
|
107
|
+
@document = Moxml.new.create_document
|
|
108
|
+
@document.add_child(@document.create_declaration("1.0", "UTF-8"))
|
|
109
|
+
root = @document.create_element("c:chartSpace")
|
|
110
|
+
root.add_namespace("c", CHART_NS)
|
|
111
|
+
root.add_namespace("a", DRAWINGML_MAIN_NS)
|
|
112
|
+
root.add_namespace("r", OFFICE_DOCUMENT_RELS_NS)
|
|
113
|
+
@document.root = root
|
|
114
|
+
build_chart(root)
|
|
115
|
+
@document.to_xml
|
|
114
116
|
end
|
|
115
117
|
|
|
116
118
|
# Register chart on a document and create the Drawing element
|
|
@@ -147,166 +149,159 @@ module Uniword
|
|
|
147
149
|
include DeterministicId
|
|
148
150
|
|
|
149
151
|
# Build the inner chart XML
|
|
150
|
-
def
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
if @show_legend
|
|
181
|
-
xml["c"].legend do
|
|
182
|
-
xml["c"].legendPos("val" => @legend_position)
|
|
183
|
-
xml["c"].overlay("val" => "0")
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
xml["c"].plotVisOnly("val" => "1")
|
|
152
|
+
def build_chart(parent)
|
|
153
|
+
chart = add_element(parent, "c:chart")
|
|
154
|
+
if @title_text
|
|
155
|
+
title = add_element(chart, "c:title")
|
|
156
|
+
tx = add_element(title, "c:tx")
|
|
157
|
+
rich = add_element(tx, "c:rich")
|
|
158
|
+
add_element(rich, "a:bodyPr")
|
|
159
|
+
add_element(rich, "a:lstStyle")
|
|
160
|
+
p_el = add_element(rich, "a:p")
|
|
161
|
+
r_el = add_element(p_el, "a:r")
|
|
162
|
+
add_text(add_element(r_el, "a:t"), @title_text)
|
|
163
|
+
add_element(title, "c:overlay", "val" => "0")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
add_element(chart, "c:autoTitleDeleted", "val" => "0")
|
|
167
|
+
|
|
168
|
+
plot_area = add_element(chart, "c:plotArea")
|
|
169
|
+
build_chart_type(plot_area)
|
|
170
|
+
|
|
171
|
+
unless @chart_type == :pie
|
|
172
|
+
build_axis(plot_area, "catAx", "10", "b", "20")
|
|
173
|
+
build_axis(plot_area, "valAx", "20", "l", "10")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if @show_legend
|
|
177
|
+
legend = add_element(chart, "c:legend")
|
|
178
|
+
add_element(legend, "c:legendPos", "val" => @legend_position)
|
|
179
|
+
add_element(legend, "c:overlay", "val" => "0")
|
|
188
180
|
end
|
|
181
|
+
|
|
182
|
+
add_element(chart, "c:plotVisOnly", "val" => "1")
|
|
183
|
+
chart
|
|
189
184
|
end
|
|
190
185
|
|
|
191
186
|
# Build the specific chart type element
|
|
192
|
-
def build_chart_type(
|
|
187
|
+
def build_chart_type(parent)
|
|
193
188
|
case @chart_type
|
|
194
189
|
when :bar
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
end
|
|
190
|
+
chart = add_element(parent, "c:barChart")
|
|
191
|
+
add_element(chart, "c:barDir", "val" => "col")
|
|
192
|
+
add_element(chart, "c:grouping", "val" => "clustered")
|
|
193
|
+
add_element(chart, "c:varyColors", "val" => "0")
|
|
194
|
+
build_all_series(chart)
|
|
195
|
+
add_element(chart, "c:axId", "val" => "10")
|
|
196
|
+
add_element(chart, "c:axId", "val" => "20")
|
|
203
197
|
when :line
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
end
|
|
198
|
+
chart = add_element(parent, "c:lineChart")
|
|
199
|
+
add_element(chart, "c:grouping", "val" => "standard")
|
|
200
|
+
add_element(chart, "c:varyColors", "val" => "0")
|
|
201
|
+
build_all_series(chart)
|
|
202
|
+
add_element(chart, "c:axId", "val" => "10")
|
|
203
|
+
add_element(chart, "c:axId", "val" => "20")
|
|
211
204
|
when :pie
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
end
|
|
205
|
+
chart = add_element(parent, "c:pieChart")
|
|
206
|
+
add_element(chart, "c:varyColors", "val" => "1")
|
|
207
|
+
build_all_series(chart)
|
|
216
208
|
end
|
|
217
209
|
end
|
|
218
210
|
|
|
219
211
|
# Build all series XML blocks
|
|
220
|
-
def build_all_series(
|
|
212
|
+
def build_all_series(parent)
|
|
221
213
|
@series_list.each_with_index do |s, i|
|
|
222
|
-
build_series(
|
|
214
|
+
build_series(parent, s, i)
|
|
223
215
|
end
|
|
224
216
|
end
|
|
225
217
|
|
|
226
218
|
# Build a single series XML block
|
|
227
|
-
def build_series(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
build_values(xml, series[:data])
|
|
242
|
-
end
|
|
219
|
+
def build_series(parent, series, index)
|
|
220
|
+
ser = add_element(parent, "c:ser")
|
|
221
|
+
add_element(ser, "c:idx", "val" => index)
|
|
222
|
+
add_element(ser, "c:order", "val" => index)
|
|
223
|
+
tx = add_element(ser, "c:tx")
|
|
224
|
+
str_ref = add_element(tx, "c:strRef")
|
|
225
|
+
add_text(add_element(str_ref, "c:f"), "Sheet1!$B$#{index + 1}")
|
|
226
|
+
str_cache = add_element(str_ref, "c:strCache")
|
|
227
|
+
add_element(str_cache, "c:ptCount", "val" => 1)
|
|
228
|
+
pt = add_element(str_cache, "c:pt", "idx" => 0)
|
|
229
|
+
add_text(add_element(pt, "c:v"), series[:name])
|
|
230
|
+
build_categories(ser)
|
|
231
|
+
build_values(ser, series[:data])
|
|
232
|
+
ser
|
|
243
233
|
end
|
|
244
234
|
|
|
245
235
|
# Build category data XML
|
|
246
|
-
def build_categories(
|
|
236
|
+
def build_categories(parent)
|
|
247
237
|
return if @categories.empty?
|
|
248
238
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
end
|
|
258
|
-
end
|
|
239
|
+
cat = add_element(parent, "c:cat")
|
|
240
|
+
str_ref = add_element(cat, "c:strRef")
|
|
241
|
+
add_text(add_element(str_ref, "c:f"), "Sheet1!$A$2:$A$100")
|
|
242
|
+
str_cache = add_element(str_ref, "c:strCache")
|
|
243
|
+
add_element(str_cache, "c:ptCount", "val" => @categories.size)
|
|
244
|
+
@categories.each_with_index do |category, idx|
|
|
245
|
+
pt = add_element(str_cache, "c:pt", "idx" => idx)
|
|
246
|
+
add_text(add_element(pt, "c:v"), category)
|
|
259
247
|
end
|
|
260
248
|
end
|
|
261
249
|
|
|
262
250
|
# Build values data XML
|
|
263
|
-
def build_values(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
end
|
|
274
|
-
end
|
|
251
|
+
def build_values(parent, data)
|
|
252
|
+
val = add_element(parent, "c:val")
|
|
253
|
+
num_ref = add_element(val, "c:numRef")
|
|
254
|
+
add_text(add_element(num_ref, "c:f"), "Sheet1!$C$2:$C$100")
|
|
255
|
+
num_cache = add_element(num_ref, "c:numCache")
|
|
256
|
+
add_text(add_element(num_cache, "c:formatCode"), "General")
|
|
257
|
+
add_element(num_cache, "c:ptCount", "val" => data.size)
|
|
258
|
+
data.each_with_index do |value, idx|
|
|
259
|
+
pt = add_element(num_cache, "c:pt", "idx" => idx)
|
|
260
|
+
add_text(add_element(pt, "c:v"), value.to_s)
|
|
275
261
|
end
|
|
276
262
|
end
|
|
277
263
|
|
|
278
264
|
# Build a chart axis
|
|
279
|
-
def build_axis(
|
|
265
|
+
def build_axis(parent, tag, id, position, cross_id)
|
|
280
266
|
case tag
|
|
281
267
|
when "catAx"
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
end
|
|
268
|
+
write_axis_content(add_element(parent, "c:catAx"), id, position,
|
|
269
|
+
cross_id)
|
|
285
270
|
when "valAx"
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
end
|
|
271
|
+
write_axis_content(add_element(parent, "c:valAx"), id, position,
|
|
272
|
+
cross_id)
|
|
289
273
|
when "serAx"
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
end
|
|
274
|
+
write_axis_content(add_element(parent, "c:serAx"), id, position,
|
|
275
|
+
cross_id)
|
|
293
276
|
else
|
|
294
277
|
raise ArgumentError, "Unknown axis type: #{tag}"
|
|
295
278
|
end
|
|
296
279
|
end
|
|
297
280
|
|
|
298
|
-
def write_axis_content(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
281
|
+
def write_axis_content(axis, id, position, cross_id)
|
|
282
|
+
add_element(axis, "c:axId", "val" => id)
|
|
283
|
+
scaling = add_element(axis, "c:scaling")
|
|
284
|
+
add_element(scaling, "c:orientation", "val" => "minMax")
|
|
285
|
+
add_element(axis, "c:delete", "val" => "0")
|
|
286
|
+
add_element(axis, "c:axPos", "val" => position)
|
|
287
|
+
add_element(axis, "c:majorGridlines")
|
|
288
|
+
add_element(axis, "c:numFmt", "formatCode" => "General",
|
|
289
|
+
"sourceLinked" => "1")
|
|
290
|
+
add_element(axis, "c:tickLblPos", "val" => "nextTo")
|
|
291
|
+
add_element(axis, "c:crossAx", "val" => cross_id)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Append an element with attributes to a parent node.
|
|
295
|
+
def add_element(parent, name, attributes = {})
|
|
296
|
+
element = @document.create_element(name)
|
|
297
|
+
attributes.each { |key, value| element[key] = value }
|
|
298
|
+
parent.add_child(element)
|
|
299
|
+
element
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Append a text node to a parent node.
|
|
303
|
+
def add_text(parent, content)
|
|
304
|
+
parent.add_child(@document.create_text(content))
|
|
310
305
|
end
|
|
311
306
|
|
|
312
307
|
# Create a Drawing element with chart reference
|
|
@@ -200,8 +200,8 @@ canon_profile: :spec_friendly)
|
|
|
200
200
|
def compare_xml(part_name, old_xml, new_xml)
|
|
201
201
|
changes = []
|
|
202
202
|
|
|
203
|
-
old_doc =
|
|
204
|
-
new_doc =
|
|
203
|
+
old_doc = Moxml.parse(old_xml)
|
|
204
|
+
new_doc = Moxml.parse(new_xml)
|
|
205
205
|
|
|
206
206
|
# Compare namespace declarations
|
|
207
207
|
changes.concat(
|
|
@@ -219,7 +219,7 @@ canon_profile: :spec_friendly)
|
|
|
219
219
|
)
|
|
220
220
|
|
|
221
221
|
changes
|
|
222
|
-
rescue
|
|
222
|
+
rescue Moxml::ParseError
|
|
223
223
|
changes
|
|
224
224
|
end
|
|
225
225
|
|
|
@@ -243,8 +243,8 @@ canon_profile: :spec_friendly)
|
|
|
243
243
|
compression_name(new_e.compression_method)]
|
|
244
244
|
end
|
|
245
245
|
|
|
246
|
-
old_text = old_e.internal_file_attributes
|
|
247
|
-
new_text = new_e.internal_file_attributes
|
|
246
|
+
old_text = old_e.internal_file_attributes.anybits?(1)
|
|
247
|
+
new_text = new_e.internal_file_attributes.anybits?(1)
|
|
248
248
|
if old_text != new_text
|
|
249
249
|
diffs[:internal_attr] = [old_text ? "text" : "binary",
|
|
250
250
|
new_text ? "text" : "binary"]
|
|
@@ -361,7 +361,7 @@ canon_profile: :spec_friendly)
|
|
|
361
361
|
return issues
|
|
362
362
|
end
|
|
363
363
|
|
|
364
|
-
doc =
|
|
364
|
+
doc = Moxml.parse(rels_xml)
|
|
365
365
|
doc.xpath("//xmlns:Relationship").each do |rel|
|
|
366
366
|
target = rel["Target"]&.sub(%r{^/}, "")
|
|
367
367
|
next unless target
|
|
@@ -376,7 +376,7 @@ canon_profile: :spec_friendly)
|
|
|
376
376
|
end
|
|
377
377
|
|
|
378
378
|
issues
|
|
379
|
-
rescue
|
|
379
|
+
rescue Moxml::ParseError
|
|
380
380
|
issues
|
|
381
381
|
end
|
|
382
382
|
|
|
@@ -386,7 +386,7 @@ canon_profile: :spec_friendly)
|
|
|
386
386
|
# @return [Hash{String => String}, nil]
|
|
387
387
|
def parse_content_types(zip)
|
|
388
388
|
ct_xml = zip.read("[Content_Types].xml")
|
|
389
|
-
doc =
|
|
389
|
+
doc = Moxml.parse(ct_xml)
|
|
390
390
|
overrides = {}
|
|
391
391
|
doc.xpath("//xmlns:Override").each do |node|
|
|
392
392
|
overrides[node["PartName"]] = node["ContentType"]
|
|
@@ -397,7 +397,7 @@ canon_profile: :spec_friendly)
|
|
|
397
397
|
nil
|
|
398
398
|
end
|
|
399
399
|
|
|
400
|
-
# ---
|
|
400
|
+
# --- Structural comparison methods ---
|
|
401
401
|
|
|
402
402
|
# Compare namespace declarations between two XML documents.
|
|
403
403
|
#
|
|
@@ -532,28 +532,29 @@ canon_profile: :spec_friendly)
|
|
|
532
532
|
|
|
533
533
|
# Extract namespace prefix-to-URI mapping from an element.
|
|
534
534
|
#
|
|
535
|
-
# @param element [
|
|
535
|
+
# @param element [Moxml::Element]
|
|
536
536
|
# @return [Hash{String => String}]
|
|
537
537
|
def namespace_hash(element)
|
|
538
538
|
return {} unless element
|
|
539
539
|
|
|
540
540
|
result = {}
|
|
541
|
-
element.
|
|
541
|
+
element.namespaces.each do |ns|
|
|
542
542
|
prefix = ns.prefix || ""
|
|
543
|
-
result[prefix] = ns.
|
|
543
|
+
result[prefix] = ns.uri
|
|
544
544
|
end
|
|
545
545
|
result
|
|
546
546
|
end
|
|
547
547
|
|
|
548
548
|
# Extract attribute name-to-value mapping from an element.
|
|
549
549
|
#
|
|
550
|
-
# @param element [
|
|
550
|
+
# @param element [Moxml::Element]
|
|
551
551
|
# @return [Hash{String => String}]
|
|
552
552
|
def attribute_hash(element)
|
|
553
553
|
return {} unless element
|
|
554
554
|
|
|
555
555
|
result = {}
|
|
556
|
-
element.attributes.each do |
|
|
556
|
+
element.attributes.each do |attr|
|
|
557
|
+
name = attr.name
|
|
557
558
|
key = attr.namespace ? "#{attr.namespace.prefix}:#{name}" : name
|
|
558
559
|
result[key] = attr.value
|
|
559
560
|
end
|
|
@@ -562,7 +563,7 @@ canon_profile: :spec_friendly)
|
|
|
562
563
|
|
|
563
564
|
# Count elements by qualified name.
|
|
564
565
|
#
|
|
565
|
-
# @param doc [
|
|
566
|
+
# @param doc [Moxml::Document]
|
|
566
567
|
# @return [Hash{String => Integer}]
|
|
567
568
|
def element_counts(doc)
|
|
568
569
|
counts = Hash.new(0)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "nokogiri"
|
|
4
3
|
require "set"
|
|
5
4
|
|
|
6
5
|
module Uniword
|
|
@@ -83,15 +82,15 @@ module Uniword
|
|
|
83
82
|
# Memoized strict parse of one entry.
|
|
84
83
|
#
|
|
85
84
|
# @param name [String] Entry path
|
|
86
|
-
# @return [
|
|
85
|
+
# @return [Moxml::Document, nil] Parsed document, or nil when
|
|
87
86
|
# the entry is malformed (OPC-008 already recorded)
|
|
88
87
|
def parsed_part(name)
|
|
89
88
|
return @parsed_parts[name] if @parsed_parts.key?(name)
|
|
90
89
|
|
|
91
90
|
@parsed_parts[name] =
|
|
92
91
|
begin
|
|
93
|
-
|
|
94
|
-
rescue
|
|
92
|
+
Moxml.parse(@content[name])
|
|
93
|
+
rescue Moxml::ParseError => e
|
|
95
94
|
add_issue("OPC-008", name,
|
|
96
95
|
"Malformed XML in #{name}: #{e.message}")
|
|
97
96
|
nil
|
|
@@ -180,19 +179,19 @@ module Uniword
|
|
|
180
179
|
|
|
181
180
|
# Collect (attribute local name, value) pairs for r:id/r:embed/r:link.
|
|
182
181
|
#
|
|
183
|
-
# @param doc [
|
|
182
|
+
# @param doc [Moxml::Document] Parsed XML part
|
|
184
183
|
# @return [Array<Array(String, String)>] Reference pairs
|
|
185
184
|
def relationship_references(doc)
|
|
186
185
|
refs = []
|
|
187
186
|
doc.traverse do |node|
|
|
188
187
|
next unless node.element?
|
|
189
188
|
|
|
190
|
-
node.
|
|
191
|
-
next unless attr.namespace&.
|
|
192
|
-
next unless R_REF_ATTRIBUTES.include?(attr.
|
|
189
|
+
node.attributes.each do |attr|
|
|
190
|
+
next unless attr.namespace&.uri == OFFICE_RELS_NS
|
|
191
|
+
next unless R_REF_ATTRIBUTES.include?(attr.name)
|
|
193
192
|
|
|
194
193
|
value = attr.value
|
|
195
|
-
refs << [attr.
|
|
194
|
+
refs << [attr.name, value] unless value.empty?
|
|
196
195
|
end
|
|
197
196
|
end
|
|
198
197
|
refs
|
|
@@ -243,8 +242,8 @@ module Uniword
|
|
|
243
242
|
|
|
244
243
|
# Relationship elements of a parsed .rels document.
|
|
245
244
|
#
|
|
246
|
-
# @param doc [
|
|
247
|
-
# @return [
|
|
245
|
+
# @param doc [Moxml::Document] Parsed .rels part
|
|
246
|
+
# @return [Moxml::NodeSet] Relationship elements
|
|
248
247
|
def relationship_nodes(doc)
|
|
249
248
|
doc.xpath("//xmlns:Relationship", "xmlns" => RELS_NS)
|
|
250
249
|
end
|
|
@@ -15,7 +15,7 @@ module Uniword
|
|
|
15
15
|
# -- ID generation --
|
|
16
16
|
|
|
17
17
|
def generate_rsid
|
|
18
|
-
"00#{hex_derive(
|
|
18
|
+
"00#{hex_derive('rsid', 3)}"
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def generate_hex_id(seed = 0)
|
|
@@ -24,7 +24,7 @@ module Uniword
|
|
|
24
24
|
|
|
25
25
|
def hex_derive(seed, byte_count)
|
|
26
26
|
Digest::SHA256.hexdigest(
|
|
27
|
-
"#{document_fingerprint}:#{seed}"
|
|
27
|
+
"#{document_fingerprint}:#{seed}",
|
|
28
28
|
)[0...(byte_count * 2)].upcase
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -43,7 +43,7 @@ module Uniword
|
|
|
43
43
|
|
|
44
44
|
# -- Traversal --
|
|
45
45
|
|
|
46
|
-
def walk_body_paragraphs(body)
|
|
46
|
+
def walk_body_paragraphs(body, &block)
|
|
47
47
|
if body.element_order && !body.element_order.empty?
|
|
48
48
|
p_idx = 0
|
|
49
49
|
tbl_idx = 0
|
|
@@ -54,18 +54,20 @@ module Uniword
|
|
|
54
54
|
p_idx += 1
|
|
55
55
|
when "tbl"
|
|
56
56
|
if body.tables[tbl_idx]
|
|
57
|
-
walk_table_paragraphs(body.tables[tbl_idx])
|
|
57
|
+
walk_table_paragraphs(body.tables[tbl_idx], &block)
|
|
58
58
|
end
|
|
59
59
|
tbl_idx += 1
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
else
|
|
63
|
-
body.paragraphs.each
|
|
64
|
-
body.tables&.each
|
|
63
|
+
body.paragraphs.each(&block)
|
|
64
|
+
body.tables&.each do |tbl|
|
|
65
|
+
walk_table_paragraphs(tbl, &block)
|
|
66
|
+
end
|
|
65
67
|
end
|
|
66
68
|
end
|
|
67
69
|
|
|
68
|
-
def walk_body_tables(body)
|
|
70
|
+
def walk_body_tables(body, &block)
|
|
69
71
|
return unless body
|
|
70
72
|
|
|
71
73
|
if body.element_order && !body.element_order.empty?
|
|
@@ -77,7 +79,7 @@ module Uniword
|
|
|
77
79
|
tbl_idx += 1
|
|
78
80
|
end
|
|
79
81
|
else
|
|
80
|
-
body.tables&.each
|
|
82
|
+
body.tables&.each(&block)
|
|
81
83
|
end
|
|
82
84
|
end
|
|
83
85
|
|
|
@@ -89,11 +91,12 @@ module Uniword
|
|
|
89
91
|
end
|
|
90
92
|
end
|
|
91
93
|
|
|
92
|
-
def walk_table_paragraphs(table)
|
|
94
|
+
def walk_table_paragraphs(table, &block)
|
|
93
95
|
return unless table
|
|
96
|
+
|
|
94
97
|
table.rows&.each do |row|
|
|
95
98
|
row.cells&.each do |cell|
|
|
96
|
-
cell.paragraphs.each
|
|
99
|
+
cell.paragraphs.each(&block)
|
|
97
100
|
end
|
|
98
101
|
end
|
|
99
102
|
end
|
|
@@ -152,6 +155,18 @@ module Uniword
|
|
|
152
155
|
end
|
|
153
156
|
end
|
|
154
157
|
|
|
158
|
+
# The paragraph backfill above emits w14:paraId/textId; part
|
|
159
|
+
# roots that never declared w14 ignorable must list it or
|
|
160
|
+
# strict MCE consumers reject those attributes.
|
|
161
|
+
def ensure_w14_ignorable(root)
|
|
162
|
+
prefixes = root.mc_ignorable.to_s.split(/\s+/)
|
|
163
|
+
return if prefixes.include?("w14")
|
|
164
|
+
|
|
165
|
+
root.mc_ignorable = Ooxml::Types::McIgnorable.new(
|
|
166
|
+
(prefixes + ["w14"]).join(" "),
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
155
170
|
# Merge adjacent runs with identical formatting and consolidate
|
|
156
171
|
# standalone tab/br runs into following text runs (F4 + F5).
|
|
157
172
|
# Respects element_order: only merges runs that are truly adjacent
|
|
@@ -165,7 +180,7 @@ module Uniword
|
|
|
165
180
|
runs[1..].each_with_index do |run, idx|
|
|
166
181
|
prev = merged.last
|
|
167
182
|
if run_indices.include?(idx) &&
|
|
168
|
-
|
|
183
|
+
run_properties_match?(prev, run) && can_merge?(prev, run)
|
|
169
184
|
merge_run_into(prev, run)
|
|
170
185
|
else
|
|
171
186
|
merged << run
|
|
@@ -180,7 +195,7 @@ module Uniword
|
|
|
180
195
|
# in element_order (no non-run elements between them).
|
|
181
196
|
def adjacent_run_indices(paragraph)
|
|
182
197
|
eo = paragraph.element_order
|
|
183
|
-
return (0...paragraph.runs.size - 1).to_a unless eo && !eo.empty?
|
|
198
|
+
return (0...(paragraph.runs.size - 1)).to_a unless eo && !eo.empty?
|
|
184
199
|
|
|
185
200
|
indices = Set.new
|
|
186
201
|
run_count = 0
|
|
@@ -50,6 +50,11 @@ module Uniword
|
|
|
50
50
|
val: profile.list_separator,
|
|
51
51
|
)
|
|
52
52
|
|
|
53
|
+
# CT_Settings places w:rsids between w:compat and m:mathPr;
|
|
54
|
+
# fixtures carrying rsids last serialize it after the
|
|
55
|
+
# appended siblings and fail strict sequence validation.
|
|
56
|
+
Ooxml::ElementOrder.move_once(settings, "rsids", after: "compat")
|
|
57
|
+
|
|
53
58
|
# w:updateFields set by the user/API: ensure it serializes —
|
|
54
59
|
# parsed settings carry a stored element_order that lacks it.
|
|
55
60
|
if settings.update_fields
|