uniword 1.3.0 → 1.4.1
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/CHANGELOG.md +90 -0
- data/lib/uniword/builder/chart_builder.rb +3 -5
- data/lib/uniword/builder/image_builder.rb +5 -5
- data/lib/uniword/document_factory.rb +10 -24
- data/lib/uniword/docx/custom_xml_item.rb +8 -0
- data/lib/uniword/docx/image_part.rb +83 -0
- data/lib/uniword/docx/package.rb +22 -402
- data/lib/uniword/docx/package_defaults.rb +36 -32
- data/lib/uniword/docx/package_serialization.rb +46 -7
- data/lib/uniword/docx/part.rb +19 -0
- data/lib/uniword/docx/part_collection.rb +15 -7
- data/lib/uniword/docx/part_loader/chart_loader.rb +50 -0
- data/lib/uniword/docx/part_loader/custom_xml_loader.rb +53 -0
- data/lib/uniword/docx/part_loader/embedding_loader.rb +26 -0
- data/lib/uniword/docx/part_loader/header_footer_loader.rb +101 -0
- data/lib/uniword/docx/part_loader/image_loader.rb +105 -0
- data/lib/uniword/docx/part_loader/load_context.rb +85 -0
- data/lib/uniword/docx/part_loader/raw_part_loader.rb +183 -0
- data/lib/uniword/docx/part_loader/theme_media_loader.rb +39 -0
- data/lib/uniword/docx/part_loader/xml_model_loader.rb +56 -0
- data/lib/uniword/docx/part_loader.rb +92 -0
- data/lib/uniword/docx/raw_part.rb +75 -0
- data/lib/uniword/docx/reconciler/helpers.rb +9 -30
- data/lib/uniword/docx/reconciler/package_structure.rb +4 -5
- data/lib/uniword/docx/reconciler/referential_integrity.rb +4 -79
- data/lib/uniword/docx.rb +3 -0
- data/lib/uniword/images/image_manager.rb +13 -13
- data/lib/uniword/ooxml/element_order.rb +94 -0
- data/lib/uniword/ooxml/part_definition.rb +164 -6
- data/lib/uniword/ooxml/part_registry.rb +238 -45
- data/lib/uniword/ooxml/relationships/image_relationship.rb +5 -2
- data/lib/uniword/ooxml.rb +1 -0
- data/lib/uniword/toc/toc_generator.rb +6 -7
- data/lib/uniword/transformation/mhtml_element_renderer.rb +1 -5
- data/lib/uniword/transformation/mhtml_metadata_builder.rb +6 -6
- data/lib/uniword/validation/opc_validator.rb +16 -1
- data/lib/uniword/validation/validators/xml_schema_validator.rb +46 -0
- data/lib/uniword/version.rb +1 -1
- data/lib/uniword/wordprocessingml/document_root.rb +29 -220
- data/lib/uniword/wordprocessingml/document_styling.rb +246 -0
- data/lib/uniword/wordprocessingml/endnotes.rb +2 -3
- data/lib/uniword/wordprocessingml/footnotes.rb +2 -3
- data/lib/uniword/wordprocessingml/numbering_elements.rb +0 -15
- data/lib/uniword/wordprocessingml.rb +1 -2
- metadata +16 -2
|
@@ -5,9 +5,11 @@ module Uniword
|
|
|
5
5
|
# Declarative description of one OOXML package part kind.
|
|
6
6
|
#
|
|
7
7
|
# A PartDefinition is the single source of truth for a part's
|
|
8
|
-
# package path, content type, relationship type,
|
|
8
|
+
# package path, content type, relationship type, the form its
|
|
9
9
|
# [Content_Types].xml entry takes (Default by extension or Override
|
|
10
|
-
# by part name)
|
|
10
|
+
# by part name), how it is loaded from extracted ZIP content, and
|
|
11
|
+
# where it lives on both Wordprocessingml::DocumentRoot and
|
|
12
|
+
# Docx::Package. Instances are immutable value objects registered
|
|
11
13
|
# in {PartRegistry}; consumers derive every literal from them.
|
|
12
14
|
#
|
|
13
15
|
# Parts with instance-numbered paths (headers, footers, charts,
|
|
@@ -15,12 +17,21 @@ module Uniword
|
|
|
15
17
|
# +format+-style placeholders (e.g. "word/header%<counter>d.xml")
|
|
16
18
|
# instead of a fixed path.
|
|
17
19
|
#
|
|
20
|
+
# Load behavior beyond this data (parse rules for special part
|
|
21
|
+
# kinds) lives in named Docx::PartLoader strategy classes selected
|
|
22
|
+
# by +loader+; the definition itself stays a value object.
|
|
23
|
+
#
|
|
18
24
|
# @example Fixed part
|
|
19
25
|
# PartDefinition.new(
|
|
20
26
|
# key: :styles, path: "word/styles.xml", target: "styles.xml",
|
|
21
27
|
# content_type: "...styles+xml",
|
|
22
28
|
# rel_type: ".../relationships/styles",
|
|
23
29
|
# required: true, kind: :override, rels_scope: :document,
|
|
30
|
+
# loader: :xml_model,
|
|
31
|
+
# loader_model: Uniword::Wordprocessingml::StylesConfiguration,
|
|
32
|
+
# load_priority: 50,
|
|
33
|
+
# package_attribute: :styles,
|
|
34
|
+
# document_attribute: :styles_configuration,
|
|
24
35
|
# )
|
|
25
36
|
class PartDefinition
|
|
26
37
|
# @return [Symbol] unique registry key (e.g. :styles)
|
|
@@ -65,11 +76,60 @@ module Uniword
|
|
|
65
76
|
# the comprehensive [Content_Types].xml for new DOCX packages
|
|
66
77
|
attr_reader :standard
|
|
67
78
|
|
|
68
|
-
#
|
|
79
|
+
# @return [Symbol, nil] Docx::PartLoader strategy key
|
|
80
|
+
# (:xml_model, :custom_xml, :header_footer, :chart, :image,
|
|
81
|
+
# :embedding, :theme_media); nil when the part is not loaded
|
|
82
|
+
# from ZIP content
|
|
83
|
+
attr_reader :loader
|
|
84
|
+
|
|
85
|
+
# @return [Class, nil] model class parsing the part's XML
|
|
86
|
+
# (responds to +.from_xml+); used by loaders that parse
|
|
87
|
+
attr_reader :loader_model
|
|
88
|
+
|
|
89
|
+
# @return [Symbol, nil] dynamic path resolution rule:
|
|
90
|
+
# :office_document (resolve the main document path from the
|
|
91
|
+
# package-level officeDocument relationship) or
|
|
92
|
+
# :office_document_rels (sidecar .rels of the resolved main
|
|
93
|
+
# document); nil for fixed paths
|
|
94
|
+
attr_reader :path_resolution
|
|
95
|
+
|
|
96
|
+
# @return [Integer, nil] load ordering weight — lower loads
|
|
97
|
+
# first; entries sharing a priority load in registration order
|
|
98
|
+
attr_reader :load_priority
|
|
99
|
+
|
|
100
|
+
# @return [Symbol, nil] attribute (reader/writer) holding this
|
|
101
|
+
# part on Wordprocessingml::DocumentRoot
|
|
102
|
+
attr_reader :document_attribute
|
|
103
|
+
|
|
104
|
+
# @return [Symbol, nil] attribute (reader/writer) holding this
|
|
105
|
+
# part on Docx::Package
|
|
106
|
+
attr_reader :package_attribute
|
|
107
|
+
|
|
108
|
+
# @return [Boolean] whether the package→document copy includes
|
|
109
|
+
# this part (false when the loader already places the part on
|
|
110
|
+
# the document, or the part is never read from ZIP content)
|
|
111
|
+
attr_reader :copy_to_document
|
|
112
|
+
|
|
113
|
+
# @return [Symbol, nil] predicate on the document guarding the
|
|
114
|
+
# document→package copy (e.g. :numbering_configuration_loaded?
|
|
115
|
+
# for the lazily-initialized numbering configuration)
|
|
116
|
+
attr_reader :to_package_guard
|
|
117
|
+
|
|
118
|
+
# @return [Class, nil] class the value must be an instance of
|
|
119
|
+
# for the document→package copy (e.g. CommentsPart, since
|
|
120
|
+
# DocumentRoot#comments can hold non-part values)
|
|
121
|
+
attr_reader :to_package_type
|
|
122
|
+
|
|
123
|
+
# rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
|
|
124
|
+
# rubocop:disable Metrics/AbcSize
|
|
69
125
|
def initialize(key:, kind:, path: nil, path_pattern: nil,
|
|
70
126
|
target: nil, target_pattern: nil,
|
|
71
127
|
content_type: nil, rel_type: nil, extension: nil,
|
|
72
|
-
required: false, rels_scope: nil, standard: false
|
|
128
|
+
required: false, rels_scope: nil, standard: false,
|
|
129
|
+
loader: nil, loader_model: nil, path_resolution: nil,
|
|
130
|
+
load_priority: nil, document_attribute: nil,
|
|
131
|
+
package_attribute: nil, copy_to_document: true,
|
|
132
|
+
to_package_guard: nil, to_package_type: nil)
|
|
73
133
|
@key = key.to_sym
|
|
74
134
|
@kind = kind
|
|
75
135
|
@path = path
|
|
@@ -82,8 +142,18 @@ module Uniword
|
|
|
82
142
|
@required = required
|
|
83
143
|
@rels_scope = rels_scope
|
|
84
144
|
@standard = standard
|
|
145
|
+
@loader = loader
|
|
146
|
+
@loader_model = loader_model
|
|
147
|
+
@path_resolution = path_resolution
|
|
148
|
+
@load_priority = load_priority
|
|
149
|
+
@document_attribute = document_attribute
|
|
150
|
+
@package_attribute = package_attribute
|
|
151
|
+
@copy_to_document = copy_to_document
|
|
152
|
+
@to_package_guard = to_package_guard
|
|
153
|
+
@to_package_type = to_package_type
|
|
85
154
|
end
|
|
86
|
-
# rubocop:enable Metrics/ParameterLists
|
|
155
|
+
# rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
|
|
156
|
+
# rubocop:enable Metrics/AbcSize
|
|
87
157
|
|
|
88
158
|
# @return [Boolean] true for [Content_Types].xml Override entries
|
|
89
159
|
def override?
|
|
@@ -110,6 +180,40 @@ module Uniword
|
|
|
110
180
|
rels_scope == :package
|
|
111
181
|
end
|
|
112
182
|
|
|
183
|
+
# @return [Boolean] true when the part loads from ZIP content
|
|
184
|
+
def loadable?
|
|
185
|
+
!loader.nil?
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# @return [Boolean] true when the package→document copy mirrors
|
|
189
|
+
# this part onto Wordprocessingml::DocumentRoot
|
|
190
|
+
def copy_to_document?
|
|
191
|
+
!!copy_to_document
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Whether a package path matches this definition's fixed path or
|
|
195
|
+
# path pattern. Accepts both "word/styles.xml" and
|
|
196
|
+
# "/word/styles.xml" forms, and matches numbered paths against
|
|
197
|
+
# the pattern ("word/header2.xml" → :header).
|
|
198
|
+
#
|
|
199
|
+
# @param candidate [String] package-relative path
|
|
200
|
+
# @return [Boolean]
|
|
201
|
+
def match_path?(candidate)
|
|
202
|
+
normalized = candidate.to_s.delete_prefix("/")
|
|
203
|
+
return true if path == normalized
|
|
204
|
+
return false unless path_pattern
|
|
205
|
+
|
|
206
|
+
normalized.match?(template_regex(path_pattern))
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Static prefix of the path pattern up to the first placeholder
|
|
210
|
+
# (e.g. "word/theme/media/" for "word/theme/media/%<name>s").
|
|
211
|
+
#
|
|
212
|
+
# @return [String, nil] nil for fixed-path definitions
|
|
213
|
+
def pattern_prefix
|
|
214
|
+
path_pattern&.split(/%<\w+>[ds]/)&.first
|
|
215
|
+
end
|
|
216
|
+
|
|
113
217
|
# Resolve the package-relative path, interpolating pattern
|
|
114
218
|
# placeholders for instance-numbered parts.
|
|
115
219
|
#
|
|
@@ -143,6 +247,21 @@ module Uniword
|
|
|
143
247
|
path && "/#{path}"
|
|
144
248
|
end
|
|
145
249
|
|
|
250
|
+
# Package paths this part kind emits for the given package.
|
|
251
|
+
#
|
|
252
|
+
# Single-instance kinds emit their fixed +path+ when the backing
|
|
253
|
+
# model is present; collection families emit each stored part's
|
|
254
|
+
# own paths (each part object answers +package_paths+).
|
|
255
|
+
#
|
|
256
|
+
# @param package [Docx::Package] Package to enumerate for
|
|
257
|
+
# @return [Array<String>] emitted package paths (possibly empty)
|
|
258
|
+
def emitted_paths(package)
|
|
259
|
+
source = attribute_source(package)
|
|
260
|
+
return [] if source.nil?
|
|
261
|
+
|
|
262
|
+
collection? ? collection_paths(source) : [path]
|
|
263
|
+
end
|
|
264
|
+
|
|
146
265
|
# Two definitions are equal when every field matches.
|
|
147
266
|
def ==(other)
|
|
148
267
|
other.is_a?(PartDefinition) && fields == other.fields
|
|
@@ -152,20 +271,59 @@ module Uniword
|
|
|
152
271
|
|
|
153
272
|
# @return [Array] field values in declaration order, used for
|
|
154
273
|
# value equality
|
|
274
|
+
# rubocop:disable Metrics/AbcSize
|
|
155
275
|
def fields
|
|
156
276
|
[key, kind, path, path_pattern, target, target_pattern,
|
|
157
277
|
content_type, rel_type, extension, required, rels_scope,
|
|
158
|
-
standard
|
|
278
|
+
standard, loader, loader_model, path_resolution,
|
|
279
|
+
load_priority, document_attribute, package_attribute,
|
|
280
|
+
copy_to_document, to_package_guard, to_package_type]
|
|
159
281
|
end
|
|
282
|
+
# rubocop:enable Metrics/AbcSize
|
|
160
283
|
|
|
161
284
|
private
|
|
162
285
|
|
|
286
|
+
# The model object backing this part kind on the package: the
|
|
287
|
+
# package attribute when set, else the document attribute read
|
|
288
|
+
# through the package's document.
|
|
289
|
+
def attribute_source(package)
|
|
290
|
+
value = package_attribute && package.method(package_attribute).call
|
|
291
|
+
return value if value
|
|
292
|
+
|
|
293
|
+
document_attribute &&
|
|
294
|
+
package.document&.method(document_attribute)&.call
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Families (path_pattern, no fixed path) enumerate their parts;
|
|
298
|
+
# single-instance kinds do not.
|
|
299
|
+
def collection?
|
|
300
|
+
path.nil?
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def collection_paths(source)
|
|
304
|
+
values = source.is_a?(Docx::PartCollection) ? source.values : source
|
|
305
|
+
values.flat_map(&:package_paths)
|
|
306
|
+
end
|
|
307
|
+
|
|
163
308
|
def expand(template, vars)
|
|
164
309
|
return nil unless template
|
|
165
310
|
return template if vars.empty?
|
|
166
311
|
|
|
167
312
|
format(template, vars)
|
|
168
313
|
end
|
|
314
|
+
|
|
315
|
+
# Convert a pattern like "word/header%<counter>d.xml" into a
|
|
316
|
+
# matching regexp: %<name>d → digits, %<name>s → any path run.
|
|
317
|
+
def template_regex(template)
|
|
318
|
+
source = template.split(/(%<\w+>[ds])/).map do |part|
|
|
319
|
+
case part
|
|
320
|
+
when /%<\w+>d/ then "\\d+"
|
|
321
|
+
when /%<\w+>s/ then ".+?"
|
|
322
|
+
else Regexp.escape(part)
|
|
323
|
+
end
|
|
324
|
+
end.join
|
|
325
|
+
/\A#{source}\z/
|
|
326
|
+
end
|
|
169
327
|
end
|
|
170
328
|
end
|
|
171
329
|
end
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
3
5
|
module Uniword
|
|
4
6
|
module Ooxml
|
|
5
7
|
# Single source of truth for OOXML package part metadata.
|
|
6
8
|
#
|
|
7
|
-
# Maps every part kind the library writes to its package
|
|
8
|
-
# content type, relationship type,
|
|
9
|
-
# form (Default vs Override)
|
|
10
|
-
# literals live ONLY
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
9
|
+
# Maps every part kind the library reads or writes to its package
|
|
10
|
+
# path, content type, relationship type, [Content_Types].xml entry
|
|
11
|
+
# form (Default vs Override), loader strategy, and document/package
|
|
12
|
+
# attributes. Content type and relationship type literals live ONLY
|
|
13
|
+
# here; consumers (Uniword::ContentTypes, Docx::PackageDefaults,
|
|
14
|
+
# Docx::Reconciler::PackageStructure, Docx::PartLoader,
|
|
15
|
+
# DocumentFactory, and the Docx::PackageSerialization inject_*
|
|
16
|
+
# methods) derive from this registry instead of holding their own
|
|
17
|
+
# literals.
|
|
14
18
|
#
|
|
15
19
|
# Open/closed: a new part kind is added by registering a
|
|
16
20
|
# PartDefinition — consumers need no further changes.
|
|
@@ -46,6 +50,14 @@ module Uniword
|
|
|
46
50
|
# jpeg, png, gif, rels, xml — then overrides in the historic
|
|
47
51
|
# order). Non-standard parts follow; consumers select their own
|
|
48
52
|
# ordered subsets by key.
|
|
53
|
+
#
|
|
54
|
+
# Loader metadata: +loader+ selects the Docx::PartLoader
|
|
55
|
+
# strategy, +loader_model+ the parsing model class,
|
|
56
|
+
# +load_priority+ orders the load (content types and package
|
|
57
|
+
# rels first; the main document before the parts its rels
|
|
58
|
+
# describe), and +document_attribute+/+package_attribute+ name
|
|
59
|
+
# where the part lives on Wordprocessingml::DocumentRoot and
|
|
60
|
+
# Docx::Package for the two registry-driven copy loops.
|
|
49
61
|
BUILT_INS = [
|
|
50
62
|
# -- Content-type Defaults (extension → content type) --
|
|
51
63
|
{ key: :jpeg, kind: :default, extension: "jpeg",
|
|
@@ -65,104 +77,189 @@ module Uniword
|
|
|
65
77
|
{ key: :document, kind: :override, path: "word/document.xml",
|
|
66
78
|
content_type: "#{CT_WML}.document.main+xml",
|
|
67
79
|
rel_type: "#{OFFICE_REL_BASE}/officeDocument",
|
|
68
|
-
required: true, standard: true, rels_scope: :package
|
|
80
|
+
required: true, standard: true, rels_scope: :package,
|
|
81
|
+
loader: :xml_model,
|
|
82
|
+
loader_model: Uniword::Wordprocessingml::DocumentRoot,
|
|
83
|
+
path_resolution: :office_document, load_priority: 40,
|
|
84
|
+
package_attribute: :document },
|
|
69
85
|
{ key: :numbering, kind: :override, path: "word/numbering.xml",
|
|
70
86
|
target: "numbering.xml", content_type: "#{CT_WML}.numbering+xml",
|
|
71
87
|
rel_type: "#{OFFICE_REL_BASE}/numbering",
|
|
72
|
-
standard: true, rels_scope: :document
|
|
88
|
+
standard: true, rels_scope: :document,
|
|
89
|
+
loader: :xml_model,
|
|
90
|
+
loader_model: Uniword::Wordprocessingml::NumberingConfiguration,
|
|
91
|
+
load_priority: 50,
|
|
92
|
+
package_attribute: :numbering,
|
|
93
|
+
document_attribute: :numbering_configuration,
|
|
94
|
+
to_package_guard: :numbering_configuration_loaded? },
|
|
73
95
|
{ key: :styles, kind: :override, path: "word/styles.xml",
|
|
74
96
|
target: "styles.xml", content_type: "#{CT_WML}.styles+xml",
|
|
75
97
|
rel_type: "#{OFFICE_REL_BASE}/styles",
|
|
76
|
-
required: true, standard: true, rels_scope: :document
|
|
98
|
+
required: true, standard: true, rels_scope: :document,
|
|
99
|
+
loader: :xml_model,
|
|
100
|
+
loader_model: Uniword::Wordprocessingml::StylesConfiguration,
|
|
101
|
+
load_priority: 50,
|
|
102
|
+
package_attribute: :styles,
|
|
103
|
+
document_attribute: :styles_configuration },
|
|
77
104
|
{ key: :settings, kind: :override, path: "word/settings.xml",
|
|
78
105
|
target: "settings.xml", content_type: "#{CT_WML}.settings+xml",
|
|
79
106
|
rel_type: "#{OFFICE_REL_BASE}/settings",
|
|
80
|
-
required: true, standard: true, rels_scope: :document
|
|
107
|
+
required: true, standard: true, rels_scope: :document,
|
|
108
|
+
loader: :xml_model,
|
|
109
|
+
loader_model: Uniword::Wordprocessingml::Settings,
|
|
110
|
+
load_priority: 50,
|
|
111
|
+
package_attribute: :settings, document_attribute: :settings },
|
|
81
112
|
{ key: :web_settings, kind: :override, path: "word/webSettings.xml",
|
|
82
113
|
target: "webSettings.xml",
|
|
83
114
|
content_type: "#{CT_WML}.webSettings+xml",
|
|
84
115
|
rel_type: "#{OFFICE_REL_BASE}/webSettings",
|
|
85
|
-
required: true, standard: true, rels_scope: :document
|
|
116
|
+
required: true, standard: true, rels_scope: :document,
|
|
117
|
+
loader: :xml_model,
|
|
118
|
+
loader_model: Uniword::Wordprocessingml::WebSettings,
|
|
119
|
+
load_priority: 50,
|
|
120
|
+
package_attribute: :web_settings,
|
|
121
|
+
document_attribute: :web_settings },
|
|
86
122
|
{ key: :font_table, kind: :override, path: "word/fontTable.xml",
|
|
87
123
|
target: "fontTable.xml",
|
|
88
124
|
content_type: "#{CT_WML}.fontTable+xml",
|
|
89
125
|
rel_type: "#{OFFICE_REL_BASE}/fontTable",
|
|
90
|
-
required: true, standard: true, rels_scope: :document
|
|
126
|
+
required: true, standard: true, rels_scope: :document,
|
|
127
|
+
loader: :xml_model,
|
|
128
|
+
loader_model: Uniword::Wordprocessingml::FontTable,
|
|
129
|
+
load_priority: 50,
|
|
130
|
+
package_attribute: :font_table,
|
|
131
|
+
document_attribute: :font_table },
|
|
91
132
|
{ key: :theme, kind: :override, path: "word/theme/theme1.xml",
|
|
92
133
|
target: "theme/theme1.xml",
|
|
93
134
|
content_type: "#{CT_OFFICE}.theme+xml",
|
|
94
135
|
rel_type: "#{OFFICE_REL_BASE}/theme",
|
|
95
|
-
standard: true, rels_scope: :document
|
|
136
|
+
standard: true, rels_scope: :document,
|
|
137
|
+
loader: :xml_model, loader_model: Uniword::Drawingml::Theme,
|
|
138
|
+
load_priority: 70,
|
|
139
|
+
package_attribute: :theme, document_attribute: :theme },
|
|
96
140
|
{ key: :core_properties, kind: :override, path: "docProps/core.xml",
|
|
97
141
|
content_type: "#{CT_PACKAGE}.core-properties+xml",
|
|
98
142
|
rel_type: "#{PACKAGE_REL_BASE}/metadata/core-properties",
|
|
99
|
-
required: true, standard: true, rels_scope: :package
|
|
143
|
+
required: true, standard: true, rels_scope: :package,
|
|
144
|
+
loader: :xml_model, loader_model: CoreProperties,
|
|
145
|
+
load_priority: 30,
|
|
146
|
+
package_attribute: :core_properties,
|
|
147
|
+
document_attribute: :core_properties },
|
|
100
148
|
{ key: :app_properties, kind: :override, path: "docProps/app.xml",
|
|
101
149
|
content_type: "#{CT_OFFICE}.extended-properties+xml",
|
|
102
150
|
rel_type: "#{OFFICE_REL_BASE}/extended-properties",
|
|
103
|
-
required: true, standard: true, rels_scope: :package
|
|
151
|
+
required: true, standard: true, rels_scope: :package,
|
|
152
|
+
loader: :xml_model, loader_model: AppProperties,
|
|
153
|
+
load_priority: 30,
|
|
154
|
+
package_attribute: :app_properties,
|
|
155
|
+
document_attribute: :app_properties },
|
|
104
156
|
|
|
105
157
|
# -- Optional document parts --
|
|
106
158
|
{ key: :footnotes, kind: :override, path: "word/footnotes.xml",
|
|
107
159
|
target: "footnotes.xml",
|
|
108
160
|
content_type: "#{CT_WML}.footnotes+xml",
|
|
109
161
|
rel_type: "#{OFFICE_REL_BASE}/footnotes",
|
|
110
|
-
rels_scope: :document
|
|
162
|
+
rels_scope: :document,
|
|
163
|
+
loader: :xml_model,
|
|
164
|
+
loader_model: Uniword::Wordprocessingml::Footnotes,
|
|
165
|
+
load_priority: 80,
|
|
166
|
+
package_attribute: :footnotes, document_attribute: :footnotes },
|
|
111
167
|
{ key: :endnotes, kind: :override, path: "word/endnotes.xml",
|
|
112
168
|
target: "endnotes.xml",
|
|
113
169
|
content_type: "#{CT_WML}.endnotes+xml",
|
|
114
170
|
rel_type: "#{OFFICE_REL_BASE}/endnotes",
|
|
115
|
-
rels_scope: :document
|
|
171
|
+
rels_scope: :document,
|
|
172
|
+
loader: :xml_model,
|
|
173
|
+
loader_model: Uniword::Wordprocessingml::Endnotes,
|
|
174
|
+
load_priority: 80,
|
|
175
|
+
package_attribute: :endnotes, document_attribute: :endnotes },
|
|
116
176
|
{ key: :comments, kind: :override, path: "word/comments.xml",
|
|
117
177
|
target: "comments.xml",
|
|
118
178
|
content_type: "#{CT_WML}.comments+xml",
|
|
119
179
|
rel_type: "#{OFFICE_REL_BASE}/comments",
|
|
120
|
-
rels_scope: :document
|
|
180
|
+
rels_scope: :document,
|
|
181
|
+
loader: :xml_model, loader_model: Uniword::CommentsPart,
|
|
182
|
+
load_priority: 80,
|
|
183
|
+
package_attribute: :comments, document_attribute: :comments,
|
|
184
|
+
to_package_type: Uniword::CommentsPart },
|
|
185
|
+
# Bibliography sources live only on the document (single home:
|
|
186
|
+
# DocumentRoot#bibliography_sources); nothing reads
|
|
187
|
+
# word/sources.xml back, so there is no loader.
|
|
121
188
|
{ key: :bibliography, kind: :override, path: "word/sources.xml",
|
|
122
189
|
target: "sources.xml",
|
|
123
190
|
content_type: "#{CT_OFFICE}.bibliography+xml",
|
|
124
191
|
rel_type: "#{OFFICE_REL_BASE}/bibliography",
|
|
125
|
-
rels_scope: :document
|
|
192
|
+
rels_scope: :document,
|
|
193
|
+
document_attribute: :bibliography_sources },
|
|
126
194
|
{ key: :header, kind: :override,
|
|
127
195
|
path_pattern: "word/header%<counter>d.xml",
|
|
128
196
|
target_pattern: "header%<counter>d.xml",
|
|
129
197
|
content_type: "#{CT_WML}.header+xml",
|
|
130
198
|
rel_type: "#{OFFICE_REL_BASE}/header",
|
|
131
|
-
rels_scope: :document
|
|
199
|
+
rels_scope: :document,
|
|
200
|
+
loader: :header_footer,
|
|
201
|
+
loader_model: Uniword::Wordprocessingml::Header,
|
|
202
|
+
load_priority: 90,
|
|
203
|
+
document_attribute: :header_footer_parts },
|
|
132
204
|
{ key: :footer, kind: :override,
|
|
133
205
|
path_pattern: "word/footer%<counter>d.xml",
|
|
134
206
|
target_pattern: "footer%<counter>d.xml",
|
|
135
207
|
content_type: "#{CT_WML}.footer+xml",
|
|
136
208
|
rel_type: "#{OFFICE_REL_BASE}/footer",
|
|
137
|
-
rels_scope: :document
|
|
209
|
+
rels_scope: :document,
|
|
210
|
+
loader: :header_footer,
|
|
211
|
+
loader_model: Uniword::Wordprocessingml::Footer,
|
|
212
|
+
load_priority: 90,
|
|
213
|
+
document_attribute: :header_footer_parts },
|
|
138
214
|
{ key: :custom_properties, kind: :override,
|
|
139
215
|
path: "docProps/custom.xml",
|
|
140
216
|
content_type: "#{CT_OFFICE}.custom-properties+xml",
|
|
141
217
|
rel_type: "#{OFFICE_REL_BASE}/custom-properties",
|
|
142
|
-
rels_scope: :package
|
|
218
|
+
rels_scope: :package,
|
|
219
|
+
loader: :xml_model, loader_model: CustomProperties,
|
|
220
|
+
load_priority: 30,
|
|
221
|
+
package_attribute: :custom_properties,
|
|
222
|
+
document_attribute: :custom_properties },
|
|
223
|
+
# The chart loader keys parts by document relationship id, so
|
|
224
|
+
# they land directly on the document — the single home for
|
|
225
|
+
# chart parts (DocumentRoot#chart_parts).
|
|
143
226
|
{ key: :chart, kind: :override,
|
|
144
227
|
path_pattern: "word/charts/chart%<n>d.xml",
|
|
145
228
|
target_pattern: "charts/chart%<n>d.xml",
|
|
146
229
|
content_type: "#{CT_OFFICE}.drawingml.chart+xml",
|
|
147
230
|
rel_type: "#{OFFICE_REL_BASE}/chart",
|
|
148
|
-
rels_scope: :document
|
|
231
|
+
rels_scope: :document,
|
|
232
|
+
loader: :chart, load_priority: 100,
|
|
233
|
+
document_attribute: :chart_parts },
|
|
149
234
|
# Images carry a per-extension Default content type resolved
|
|
150
235
|
# from the image data at save time, so content_type is nil.
|
|
151
236
|
{ key: :image, kind: :default,
|
|
152
237
|
path_pattern: "word/media/%<name>s",
|
|
153
238
|
target_pattern: "media/%<name>s",
|
|
154
239
|
rel_type: "#{OFFICE_REL_BASE}/image",
|
|
155
|
-
rels_scope: :document
|
|
240
|
+
rels_scope: :document,
|
|
241
|
+
loader: :image, load_priority: 110,
|
|
242
|
+
document_attribute: :image_parts },
|
|
243
|
+
# The embedding loader keys parts by target on the package's
|
|
244
|
+
# embeddings collection; no package→document copy.
|
|
156
245
|
{ key: :ole_object, kind: :override,
|
|
157
246
|
path_pattern: "word/embeddings/%<name>s",
|
|
158
247
|
target_pattern: "embeddings/%<name>s",
|
|
159
248
|
content_type: "#{CT_OFFICE}.oleObject",
|
|
160
249
|
rel_type: "#{OFFICE_REL_BASE}/oleObject",
|
|
161
|
-
rels_scope: :document
|
|
250
|
+
rels_scope: :document,
|
|
251
|
+
loader: :embedding, load_priority: 120,
|
|
252
|
+
package_attribute: :embeddings,
|
|
253
|
+
document_attribute: :embeddings,
|
|
254
|
+
copy_to_document: false },
|
|
162
255
|
# customXml items themselves fall under the "xml" Default;
|
|
163
|
-
# only their itemProps parts get an Override.
|
|
256
|
+
# only their itemProps parts get an Override. The :custom_xml
|
|
257
|
+
# loader also pulls each item's itemProps and .rels sidecars.
|
|
164
258
|
{ key: :custom_xml_item, kind: :none,
|
|
165
|
-
path_pattern: "customXml/item%<index>d.xml"
|
|
259
|
+
path_pattern: "customXml/item%<index>d.xml",
|
|
260
|
+
loader: :custom_xml, load_priority: 30,
|
|
261
|
+
package_attribute: :custom_xml_items,
|
|
262
|
+
document_attribute: :custom_xml_items },
|
|
166
263
|
{ key: :custom_xml_item_props, kind: :override,
|
|
167
264
|
path_pattern: "customXml/itemProps%<index>d.xml",
|
|
168
265
|
content_type: "#{CT_OFFICE}.customXmlProperties+xml" },
|
|
@@ -174,6 +271,73 @@ module Uniword
|
|
|
174
271
|
# Theme part of THMX (theme) packages, rooted at /theme.
|
|
175
272
|
{ key: :thmx_theme, kind: :override, path: "theme/theme1.xml",
|
|
176
273
|
content_type: "#{CT_OFFICE}.theme+xml", required: true },
|
|
274
|
+
|
|
275
|
+
# -- Read-side infrastructure parts (no content-type entry
|
|
276
|
+
# of their own; kind :none keeps emission untouched) --
|
|
277
|
+
{ key: :content_types, kind: :none, path: "[Content_Types].xml",
|
|
278
|
+
loader: :xml_model, loader_model: Uniword::ContentTypes::Types,
|
|
279
|
+
load_priority: 10,
|
|
280
|
+
package_attribute: :content_types,
|
|
281
|
+
document_attribute: :content_types },
|
|
282
|
+
{ key: :package_rels, kind: :none, path: "_rels/.rels",
|
|
283
|
+
loader: :xml_model,
|
|
284
|
+
loader_model: Relationships::PackageRelationships,
|
|
285
|
+
load_priority: 20,
|
|
286
|
+
package_attribute: :package_rels,
|
|
287
|
+
document_attribute: :package_rels },
|
|
288
|
+
{ key: :document_rels, kind: :none,
|
|
289
|
+
path: "word/_rels/document.xml.rels",
|
|
290
|
+
loader: :xml_model,
|
|
291
|
+
loader_model: Relationships::PackageRelationships,
|
|
292
|
+
path_resolution: :office_document_rels, load_priority: 60,
|
|
293
|
+
package_attribute: :document_rels,
|
|
294
|
+
document_attribute: :document_rels },
|
|
295
|
+
{ key: :settings_rels, kind: :none,
|
|
296
|
+
path: "word/_rels/settings.xml.rels",
|
|
297
|
+
loader: :xml_model,
|
|
298
|
+
loader_model: Relationships::PackageRelationships,
|
|
299
|
+
load_priority: 50,
|
|
300
|
+
package_attribute: :settings_rels,
|
|
301
|
+
document_attribute: :settings_rels },
|
|
302
|
+
{ key: :theme_rels, kind: :none,
|
|
303
|
+
path: "word/theme/_rels/theme1.xml.rels",
|
|
304
|
+
loader: :xml_model,
|
|
305
|
+
loader_model: Relationships::PackageRelationships,
|
|
306
|
+
load_priority: 75,
|
|
307
|
+
package_attribute: :theme_rels,
|
|
308
|
+
document_attribute: :theme_rels },
|
|
309
|
+
{ key: :footnotes_rels, kind: :none,
|
|
310
|
+
path: "word/_rels/footnotes.xml.rels",
|
|
311
|
+
loader: :xml_model,
|
|
312
|
+
loader_model: Relationships::PackageRelationships,
|
|
313
|
+
load_priority: 85,
|
|
314
|
+
package_attribute: :footnotes_rels,
|
|
315
|
+
document_attribute: :footnotes_rels },
|
|
316
|
+
{ key: :endnotes_rels, kind: :none,
|
|
317
|
+
path: "word/_rels/endnotes.xml.rels",
|
|
318
|
+
loader: :xml_model,
|
|
319
|
+
loader_model: Relationships::PackageRelationships,
|
|
320
|
+
load_priority: 85,
|
|
321
|
+
package_attribute: :endnotes_rels,
|
|
322
|
+
document_attribute: :endnotes_rels },
|
|
323
|
+
# Theme media files live on the theme's media_files, not on a
|
|
324
|
+
# package/document attribute; loads only when a theme part was
|
|
325
|
+
# parsed (see PartLoader::ThemeMediaLoader).
|
|
326
|
+
{ key: :theme_media, kind: :none,
|
|
327
|
+
path_pattern: "word/theme/media/%<name>s",
|
|
328
|
+
loader: :theme_media, load_priority: 72 },
|
|
329
|
+
# Fallback passthrough for parts no definition claims
|
|
330
|
+
# (unmodelled parts such as docProps/meta.xml, glossary
|
|
331
|
+
# documents, VBA projects): the :raw loader runs last and
|
|
332
|
+
# carries the remainder byte-for-byte on the package's
|
|
333
|
+
# raw_parts collection. Emission is derived from the stored
|
|
334
|
+
# parts like any collection family, so emitted_paths — and
|
|
335
|
+
# with it the reconciler's dangling-relationship sweep — sees
|
|
336
|
+
# raw-carried parts as first-class.
|
|
337
|
+
{ key: :raw_parts, kind: :none,
|
|
338
|
+
loader: :raw, load_priority: 130,
|
|
339
|
+
package_attribute: :raw_parts,
|
|
340
|
+
document_attribute: :raw_parts },
|
|
177
341
|
].freeze
|
|
178
342
|
|
|
179
343
|
class << self
|
|
@@ -278,6 +442,51 @@ module Uniword
|
|
|
278
442
|
definitions.filter_map { |d| d.rel_type if d.package_rel? }
|
|
279
443
|
end
|
|
280
444
|
|
|
445
|
+
# Definitions loadable from extracted ZIP content, in load
|
|
446
|
+
# order: ascending PartDefinition#load_priority, registration
|
|
447
|
+
# order within a priority (explicit index tiebreak — Ruby's
|
|
448
|
+
# sort_by is not stable).
|
|
449
|
+
#
|
|
450
|
+
# @return [Array<PartDefinition>]
|
|
451
|
+
def loadable
|
|
452
|
+
definitions.select(&:loadable?).each_with_index
|
|
453
|
+
.sort_by { |d, i| [d.load_priority || 100, i] }
|
|
454
|
+
.map(&:first)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Definitions mirrored package→document by
|
|
458
|
+
# DocumentFactory.copy_package_parts_to_document.
|
|
459
|
+
#
|
|
460
|
+
# @return [Array<PartDefinition>] in registration order
|
|
461
|
+
def copied_to_document
|
|
462
|
+
definitions.select do |d|
|
|
463
|
+
d.document_attribute && d.package_attribute &&
|
|
464
|
+
d.copy_to_document?
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Definitions mirrored document→package by
|
|
469
|
+
# Docx::PackageDefaults.copy_document_parts_to_package.
|
|
470
|
+
#
|
|
471
|
+
# @return [Array<PartDefinition>] in registration order
|
|
472
|
+
def copied_to_package
|
|
473
|
+
definitions.select { |d| d.document_attribute && d.package_attribute }
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Package paths the given package will emit, derived from the
|
|
477
|
+
# definitions: single-instance kinds by model presence,
|
|
478
|
+
# collection families by their stored parts.
|
|
479
|
+
#
|
|
480
|
+
# Single authority for "will this part be emitted?" — used by
|
|
481
|
+
# the reconciler's dangling-relationship sweep.
|
|
482
|
+
#
|
|
483
|
+
# @param package [Docx::Package] Package to enumerate for
|
|
484
|
+
# @return [Set<String>] emitted package paths
|
|
485
|
+
def emitted_paths(package)
|
|
486
|
+
all.flat_map { |definition| definition.emitted_paths(package) }
|
|
487
|
+
.compact.to_set
|
|
488
|
+
end
|
|
489
|
+
|
|
281
490
|
private
|
|
282
491
|
|
|
283
492
|
def definitions
|
|
@@ -285,23 +494,7 @@ module Uniword
|
|
|
285
494
|
end
|
|
286
495
|
|
|
287
496
|
def path_match?(definition, normalized_path)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
pattern = definition.path_pattern
|
|
291
|
-
!pattern.nil? && normalized_path.match?(template_regex(pattern))
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
# Convert a pattern like "word/header%<counter>d.xml" into a
|
|
295
|
-
# matching regexp: %<name>d → digits, %<name>s → any path run.
|
|
296
|
-
def template_regex(template)
|
|
297
|
-
source = template.split(/(%<\w+>[ds])/).map do |part|
|
|
298
|
-
case part
|
|
299
|
-
when /%<\w+>d/ then "\\d+"
|
|
300
|
-
when /%<\w+>s/ then ".+?"
|
|
301
|
-
else Regexp.escape(part)
|
|
302
|
-
end
|
|
303
|
-
end.join
|
|
304
|
-
/\A#{source}\z/
|
|
497
|
+
definition.match_path?(normalized_path)
|
|
305
498
|
end
|
|
306
499
|
end
|
|
307
500
|
|
|
@@ -9,9 +9,12 @@ module Uniword
|
|
|
9
9
|
#
|
|
10
10
|
# Used for image relationships in documents
|
|
11
11
|
class ImageRelationship < Relationship
|
|
12
|
-
|
|
12
|
+
# @param id [String] relationship ID (allocated by
|
|
13
|
+
# Docx::IdAllocator — the single rId authority)
|
|
14
|
+
# @param target [String] image part target (e.g. "media/image1.png")
|
|
15
|
+
def initialize(id:, target:)
|
|
13
16
|
super(
|
|
14
|
-
id:
|
|
17
|
+
id: id,
|
|
15
18
|
type: PartRegistry.find_by_key(:image).rel_type,
|
|
16
19
|
target: target
|
|
17
20
|
)
|
data/lib/uniword/ooxml.rb
CHANGED
|
@@ -10,6 +10,7 @@ module Uniword
|
|
|
10
10
|
module Ooxml
|
|
11
11
|
# Core OOXML packages
|
|
12
12
|
autoload :DotxPackage, "#{__dir__}/ooxml/dotx_package"
|
|
13
|
+
autoload :ElementOrder, "#{__dir__}/ooxml/element_order"
|
|
13
14
|
autoload :ThmxPackage, "#{__dir__}/ooxml/thmx_package"
|
|
14
15
|
autoload :MhtmlPackage, "#{__dir__}/ooxml/mhtml_package"
|
|
15
16
|
autoload :StylesetPackage, "#{__dir__}/ooxml/styleset_package"
|