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
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Docx
|
|
5
|
+
class PartLoader
|
|
6
|
+
# Loads fixed-path XML parts: parses the part's XML with the
|
|
7
|
+
# definition's loader_model and assigns the model to the
|
|
8
|
+
# definition's package attribute. Covers every part whose load
|
|
9
|
+
# is "parse one part into one attribute": the content types,
|
|
10
|
+
# all rels parts, docProps, the main document, styles,
|
|
11
|
+
# numbering, settings, font table, web settings, theme,
|
|
12
|
+
# footnotes, endnotes, and comments.
|
|
13
|
+
#
|
|
14
|
+
# Definitions with a path_resolution rule resolve their path
|
|
15
|
+
# dynamically (main document via the package officeDocument
|
|
16
|
+
# relationship; its rels part as the sidecar of the resolved
|
|
17
|
+
# path), falling back to the definition's fixed path.
|
|
18
|
+
class XmlModelLoader
|
|
19
|
+
# @param context [LoadContext] shared load state
|
|
20
|
+
# @param definition [Ooxml::PartDefinition] part to load
|
|
21
|
+
# @return [void]
|
|
22
|
+
def load(context, definition)
|
|
23
|
+
content = read_content(context, definition)
|
|
24
|
+
return unless content
|
|
25
|
+
|
|
26
|
+
model = definition.loader_model.from_xml(content)
|
|
27
|
+
context.package.method(:"#{definition.package_attribute}=")
|
|
28
|
+
.call(model)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Part content from the first candidate path present in the
|
|
34
|
+
# ZIP, preserving the historic resolved-then-fixed fallback.
|
|
35
|
+
def read_content(context, definition)
|
|
36
|
+
candidate_paths(context, definition).each do |path|
|
|
37
|
+
content = context.zip_content[path]
|
|
38
|
+
return content if content
|
|
39
|
+
end
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def candidate_paths(context, definition)
|
|
44
|
+
case definition.path_resolution
|
|
45
|
+
when :office_document
|
|
46
|
+
[context.main_document_path, definition.path].compact
|
|
47
|
+
when :office_document_rels
|
|
48
|
+
[context.main_document_rels_path, definition.path].compact
|
|
49
|
+
else
|
|
50
|
+
[definition.path]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Docx
|
|
5
|
+
# Registry-driven loader for DOCX package parts.
|
|
6
|
+
#
|
|
7
|
+
# Replaces the former hand-written per-part sequence in
|
|
8
|
+
# Package.from_zip_content: parts load by iterating
|
|
9
|
+
# Ooxml::PartRegistry.loadable (ordered by
|
|
10
|
+
# Ooxml::PartDefinition#load_priority) and dispatching each
|
|
11
|
+
# definition to the loader strategy registered under
|
|
12
|
+
# Ooxml::PartDefinition#loader.
|
|
13
|
+
#
|
|
14
|
+
# Open/closed: a new part kind adds a strategy class plus a
|
|
15
|
+
# register_loader call and a PartRegistry registration — this loop
|
|
16
|
+
# never changes.
|
|
17
|
+
#
|
|
18
|
+
# @example Load parts into a package
|
|
19
|
+
# package = Package.new
|
|
20
|
+
# PartLoader.load(zip_content, package, zip_path: "doc.docx")
|
|
21
|
+
class PartLoader
|
|
22
|
+
autoload :LoadContext, "#{__dir__}/part_loader/load_context"
|
|
23
|
+
autoload :XmlModelLoader, "#{__dir__}/part_loader/xml_model_loader"
|
|
24
|
+
autoload :CustomXmlLoader,
|
|
25
|
+
"#{__dir__}/part_loader/custom_xml_loader"
|
|
26
|
+
autoload :HeaderFooterLoader,
|
|
27
|
+
"#{__dir__}/part_loader/header_footer_loader"
|
|
28
|
+
autoload :ChartLoader, "#{__dir__}/part_loader/chart_loader"
|
|
29
|
+
autoload :ImageLoader, "#{__dir__}/part_loader/image_loader"
|
|
30
|
+
autoload :EmbeddingLoader,
|
|
31
|
+
"#{__dir__}/part_loader/embedding_loader"
|
|
32
|
+
autoload :ThemeMediaLoader,
|
|
33
|
+
"#{__dir__}/part_loader/theme_media_loader"
|
|
34
|
+
autoload :RawPartLoader,
|
|
35
|
+
"#{__dir__}/part_loader/raw_part_loader"
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
# Load every registered part from extracted ZIP content into
|
|
39
|
+
# the package.
|
|
40
|
+
#
|
|
41
|
+
# @param zip_content [Hash] extracted ZIP entries
|
|
42
|
+
# (package path => content)
|
|
43
|
+
# @param package [Package] package to populate
|
|
44
|
+
# @param zip_path [String, nil] original ZIP path for binary
|
|
45
|
+
# re-extraction (image bytes)
|
|
46
|
+
# @return [Package] the populated package
|
|
47
|
+
def load(zip_content, package, zip_path: nil)
|
|
48
|
+
context = LoadContext.new(zip_content: zip_content,
|
|
49
|
+
package: package, zip_path: zip_path)
|
|
50
|
+
Ooxml::PartRegistry.loadable.each do |definition|
|
|
51
|
+
loader_for(definition.loader).load(context, definition)
|
|
52
|
+
end
|
|
53
|
+
package
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Register a loader strategy under a key.
|
|
57
|
+
#
|
|
58
|
+
# @param key [Symbol] PartDefinition#loader value
|
|
59
|
+
# @param strategy [#load] strategy responding to
|
|
60
|
+
# +load(context, definition)+
|
|
61
|
+
# @return [Object] the registered strategy
|
|
62
|
+
def register_loader(key, strategy)
|
|
63
|
+
loaders[key.to_sym] = strategy
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @param key [Symbol] PartDefinition#loader value
|
|
67
|
+
# @return [#load] the registered strategy
|
|
68
|
+
# @raise [ArgumentError] when no strategy is registered
|
|
69
|
+
def loader_for(key)
|
|
70
|
+
loaders.fetch(key.to_sym) do
|
|
71
|
+
raise ArgumentError, "unknown part loader: #{key.inspect}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def loaders
|
|
78
|
+
@loaders ||= {}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
register_loader(:xml_model, XmlModelLoader.new)
|
|
83
|
+
register_loader(:custom_xml, CustomXmlLoader.new)
|
|
84
|
+
register_loader(:header_footer, HeaderFooterLoader.new)
|
|
85
|
+
register_loader(:chart, ChartLoader.new)
|
|
86
|
+
register_loader(:image, ImageLoader.new)
|
|
87
|
+
register_loader(:embedding, EmbeddingLoader.new)
|
|
88
|
+
register_loader(:theme_media, ThemeMediaLoader.new)
|
|
89
|
+
register_loader(:raw, RawPartLoader.new)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Docx
|
|
5
|
+
# A package part no Ooxml::PartDefinition models, carried verbatim
|
|
6
|
+
# for round-trip fidelity (e.g. docProps/meta.xml, glossary
|
|
7
|
+
# documents, VBA projects, unmodelled .rels sidecars).
|
|
8
|
+
#
|
|
9
|
+
# Unlike document-scoped parts (charts, images, embeddings), a raw
|
|
10
|
+
# part keeps its full package-relative +path+ — raw parts live
|
|
11
|
+
# anywhere in the package, not only under word/. Content is the
|
|
12
|
+
# exact source byte stream (binary-safe; never parsed or
|
|
13
|
+
# re-encoded), plus the content type the source
|
|
14
|
+
# [Content_Types].xml declared for it (Override first, then
|
|
15
|
+
# Default by extension; nil when the source declared neither).
|
|
16
|
+
#
|
|
17
|
+
# The relationship referencing the part (when one exists in a
|
|
18
|
+
# modelled .rels part) is recorded as +r_id+/+rel_type+ metadata
|
|
19
|
+
# for introspection parity with other part kinds; emission never
|
|
20
|
+
# uses it — relationships re-serialize from the loaded rels
|
|
21
|
+
# models, so the source rel survives untouched.
|
|
22
|
+
#
|
|
23
|
+
# Raw parts are a fallback: PartLoader claims every registry-known
|
|
24
|
+
# path first and RawPartLoader carries only the remainder, so a
|
|
25
|
+
# part is never emitted twice.
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# part = RawPart.new(
|
|
29
|
+
# path: "docProps/meta.xml",
|
|
30
|
+
# content: bytes,
|
|
31
|
+
# content_type: "application/xml",
|
|
32
|
+
# )
|
|
33
|
+
# part.package_paths # => ["docProps/meta.xml"]
|
|
34
|
+
class RawPart < Part
|
|
35
|
+
# @return [String, nil] package-relative path, verbatim
|
|
36
|
+
attr_writer :path
|
|
37
|
+
|
|
38
|
+
# @param path [String, nil] package-relative path
|
|
39
|
+
# (e.g. "docProps/meta.xml")
|
|
40
|
+
# @param content [String, nil] raw part bytes (binary-safe)
|
|
41
|
+
# @param content_type [String, nil] content type declared by the
|
|
42
|
+
# source [Content_Types].xml
|
|
43
|
+
# @param r_id [String, nil] id of the relationship referencing
|
|
44
|
+
# this part (metadata only)
|
|
45
|
+
# @param rel_type [String, nil] type of the relationship
|
|
46
|
+
# referencing this part (metadata only)
|
|
47
|
+
def initialize(path: nil, content: nil, content_type: nil,
|
|
48
|
+
r_id: nil, rel_type: nil)
|
|
49
|
+
super(content: content, content_type: content_type,
|
|
50
|
+
r_id: r_id, rel_type: rel_type)
|
|
51
|
+
@path = path
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Wrap a raw-hash entry ({ path:, content:, content_type: })
|
|
55
|
+
# into a RawPart. Used by PartCollection to normalize hash
|
|
56
|
+
# assignments.
|
|
57
|
+
#
|
|
58
|
+
# @param hash [Hash] raw hash entry
|
|
59
|
+
# @return [RawPart]
|
|
60
|
+
def self.from_hash(hash)
|
|
61
|
+
new(path: hash[:path], content: hash[:content],
|
|
62
|
+
content_type: hash[:content_type],
|
|
63
|
+
r_id: hash[:r_id], rel_type: hash[:rel_type])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Package-relative path of the emitted part, carried verbatim
|
|
67
|
+
# (overrides Part's word/-prefixed target derivation).
|
|
68
|
+
#
|
|
69
|
+
# @return [String, nil] e.g. "word/glossary/document.xml"
|
|
70
|
+
def path
|
|
71
|
+
@path
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -98,45 +98,24 @@ module Uniword
|
|
|
98
98
|
end
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
# -- Element order --
|
|
101
|
+
# -- Element order (delegates to Ooxml::ElementOrder) --
|
|
102
102
|
|
|
103
|
+
# Idempotent singleton insert in schema position.
|
|
103
104
|
def ensure_element_in_order(model, tag_name, after: nil, before: nil)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return if order.any? { |e| e.name == tag_name }
|
|
108
|
-
|
|
109
|
-
entry = Lutaml::Xml::Element.new("Element", tag_name)
|
|
110
|
-
|
|
111
|
-
if after
|
|
112
|
-
idx = order.index { |e| e.name == after }
|
|
113
|
-
thaw_and_insert(model, order, idx ? idx + 1 : order.size, entry)
|
|
114
|
-
elsif before
|
|
115
|
-
idx = order.index { |e| e.name == before }
|
|
116
|
-
thaw_and_insert(model, order, idx || 0, entry)
|
|
117
|
-
else
|
|
118
|
-
thaw_and_append(model, order, entry)
|
|
119
|
-
end
|
|
105
|
+
Ooxml::ElementOrder.insert_once(model, tag_name,
|
|
106
|
+
after: after, before: before)
|
|
120
107
|
end
|
|
121
108
|
|
|
109
|
+
# Positional insert for repeatable elements.
|
|
122
110
|
def insert_element_order(obj, name, position)
|
|
123
111
|
order = obj.element_order
|
|
124
112
|
return unless order
|
|
125
|
-
|
|
126
113
|
return if order.any? { |e| e.name == name }
|
|
127
114
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
# lutaml-model freezes element_order after XML parsing.
|
|
133
|
-
# Replace the frozen array with a mutable copy when modification is needed.
|
|
134
|
-
def thaw_and_insert(model, order, position, entry)
|
|
135
|
-
model.element_order = order.dup.insert(position, entry)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def thaw_and_append(model, order, entry)
|
|
139
|
-
model.element_order = order.dup << entry
|
|
115
|
+
Ooxml::ElementOrder.insert_at(
|
|
116
|
+
obj, position, Lutaml::Xml::Element.new("Element", name,
|
|
117
|
+
node_type: :element)
|
|
118
|
+
)
|
|
140
119
|
end
|
|
141
120
|
|
|
142
121
|
# -- Run utilities (delegate to RunUtils) --
|
|
@@ -110,8 +110,7 @@ module Uniword
|
|
|
110
110
|
[Ooxml::PartRegistry.find_by_key(:comments), package.comments],
|
|
111
111
|
]
|
|
112
112
|
|
|
113
|
-
sources = package.bibliography_sources
|
|
114
|
-
package.document&.bibliography_sources
|
|
113
|
+
sources = package.document&.bibliography_sources
|
|
115
114
|
if sources
|
|
116
115
|
defs << [Ooxml::PartRegistry.find_by_key(:bibliography), sources]
|
|
117
116
|
end
|
|
@@ -172,10 +171,10 @@ module Uniword
|
|
|
172
171
|
end
|
|
173
172
|
|
|
174
173
|
chart = Ooxml::PartRegistry.find_by_key(:chart)
|
|
175
|
-
(package.document&.chart_parts&.values || []).each do |
|
|
176
|
-
next unless
|
|
174
|
+
(package.document&.chart_parts&.values || []).each do |part|
|
|
175
|
+
next unless part.target
|
|
177
176
|
|
|
178
|
-
alloc.alloc_rid(target:
|
|
177
|
+
alloc.alloc_rid(target: part.target, type: chart.rel_type)
|
|
179
178
|
end
|
|
180
179
|
end
|
|
181
180
|
|
|
@@ -483,86 +483,11 @@ module Uniword
|
|
|
483
483
|
segments.join("/")
|
|
484
484
|
end
|
|
485
485
|
|
|
486
|
-
# Package paths the save path emits
|
|
487
|
-
#
|
|
486
|
+
# Package paths the save path emits — answered by the
|
|
487
|
+
# PartRegistry (single authority), derived from model presence
|
|
488
|
+
# and part collections.
|
|
488
489
|
def carried_part_paths
|
|
489
|
-
|
|
490
|
-
paths.concat(custom_xml_item_paths)
|
|
491
|
-
paths.concat(document_part_paths)
|
|
492
|
-
paths.to_set
|
|
493
|
-
end
|
|
494
|
-
|
|
495
|
-
# [model, package path] pairs for single-instance word/ parts.
|
|
496
|
-
def carried_word_parts
|
|
497
|
-
pairs = core_word_pairs + note_word_pairs
|
|
498
|
-
pairs.filter_map { |model, path| path if model }
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
def core_word_pairs
|
|
502
|
-
[
|
|
503
|
-
[package.document, "word/document.xml"],
|
|
504
|
-
[package.styles, "word/styles.xml"],
|
|
505
|
-
[package.numbering, "word/numbering.xml"],
|
|
506
|
-
[package.settings, "word/settings.xml"],
|
|
507
|
-
[package.font_table, "word/fontTable.xml"],
|
|
508
|
-
[package.web_settings, "word/webSettings.xml"],
|
|
509
|
-
]
|
|
510
|
-
end
|
|
511
|
-
|
|
512
|
-
def note_word_pairs
|
|
513
|
-
[
|
|
514
|
-
[package.theme, "word/theme/theme1.xml"],
|
|
515
|
-
[package.footnotes, "word/footnotes.xml"],
|
|
516
|
-
[package.endnotes, "word/endnotes.xml"],
|
|
517
|
-
[package.comments, "word/comments.xml"],
|
|
518
|
-
[package.document&.bibliography_sources, "word/sources.xml"],
|
|
519
|
-
]
|
|
520
|
-
end
|
|
521
|
-
|
|
522
|
-
# [model, package path] pairs for single-instance docProps/ parts.
|
|
523
|
-
def carried_docprops_parts
|
|
524
|
-
pairs = [
|
|
525
|
-
[package.core_properties, "docProps/core.xml"],
|
|
526
|
-
[package.app_properties, "docProps/app.xml"],
|
|
527
|
-
[package.custom_properties, "docProps/custom.xml"],
|
|
528
|
-
]
|
|
529
|
-
pairs.filter_map { |model, path| path if model }
|
|
530
|
-
end
|
|
531
|
-
|
|
532
|
-
def custom_xml_item_paths
|
|
533
|
-
(package.custom_xml_items || []).flat_map do |item|
|
|
534
|
-
paths = ["customXml/item#{item[:index]}.xml"]
|
|
535
|
-
if item[:props_xml]
|
|
536
|
-
paths << "customXml/itemProps#{item[:index]}.xml"
|
|
537
|
-
end
|
|
538
|
-
paths
|
|
539
|
-
end
|
|
540
|
-
end
|
|
541
|
-
|
|
542
|
-
# Paths emitted from the document model: media, charts,
|
|
543
|
-
# embeddings, headers and footers (unified part store).
|
|
544
|
-
def document_part_paths
|
|
545
|
-
doc = package.document
|
|
546
|
-
return [] unless doc
|
|
547
|
-
|
|
548
|
-
media_chart_paths(doc) + header_footer_paths(doc)
|
|
549
|
-
end
|
|
550
|
-
|
|
551
|
-
def media_chart_paths(doc)
|
|
552
|
-
paths = [doc.image_parts, doc.chart_parts].compact.flat_map do |parts|
|
|
553
|
-
parts.values.map { |data| "word/#{data[:target]}" }
|
|
554
|
-
end
|
|
555
|
-
paths.concat(embedding_paths)
|
|
556
|
-
end
|
|
557
|
-
|
|
558
|
-
def embedding_paths
|
|
559
|
-
(package.embeddings || {}).keys.map { |target| "word/#{target}" }
|
|
560
|
-
end
|
|
561
|
-
|
|
562
|
-
def header_footer_paths(doc)
|
|
563
|
-
doc.header_footer_parts.filter_map do |part|
|
|
564
|
-
part.target && "word/#{part.target}"
|
|
565
|
-
end
|
|
490
|
+
Ooxml::PartRegistry.emitted_paths(package)
|
|
566
491
|
end
|
|
567
492
|
|
|
568
493
|
# -- Traversal helpers --
|
data/lib/uniword/docx.rb
CHANGED
|
@@ -9,7 +9,9 @@ module Uniword
|
|
|
9
9
|
module Docx
|
|
10
10
|
autoload :IdAllocator, "#{__dir__}/docx/id_allocator"
|
|
11
11
|
autoload :Part, "#{__dir__}/docx/part"
|
|
12
|
+
autoload :RawPart, "#{__dir__}/docx/raw_part"
|
|
12
13
|
autoload :ChartPart, "#{__dir__}/docx/chart_part"
|
|
14
|
+
autoload :ImagePart, "#{__dir__}/docx/image_part"
|
|
13
15
|
autoload :HeaderFooterPart, "#{__dir__}/docx/header_footer_part"
|
|
14
16
|
autoload :CustomXmlItem, "#{__dir__}/docx/custom_xml_item"
|
|
15
17
|
autoload :PartCollection, "#{__dir__}/docx/part_collection"
|
|
@@ -21,6 +23,7 @@ module Uniword
|
|
|
21
23
|
autoload :PackageIntegrityChecker,
|
|
22
24
|
"#{__dir__}/docx/package_integrity_checker"
|
|
23
25
|
autoload :PackageSerialization, "#{__dir__}/docx/package_serialization"
|
|
26
|
+
autoload :PartLoader, "#{__dir__}/docx/part_loader"
|
|
24
27
|
autoload :Profile, "#{__dir__}/docx/profile"
|
|
25
28
|
autoload :DocumentStatistics, "#{__dir__}/docx/document_statistics"
|
|
26
29
|
autoload :Reconciler, "#{__dir__}/docx/reconciler"
|
|
@@ -7,9 +7,9 @@ module Uniword
|
|
|
7
7
|
module Images
|
|
8
8
|
# Orchestrator for image operations on a DocumentRoot.
|
|
9
9
|
#
|
|
10
|
-
# Works with the document's +image_parts+
|
|
11
|
-
# loading) and the package ZIP to enumerate, extract,
|
|
12
|
-
# remove images.
|
|
10
|
+
# Works with the document's +image_parts+ collection (populated
|
|
11
|
+
# during DOCX loading) and the package ZIP to enumerate, extract,
|
|
12
|
+
# insert, and remove images.
|
|
13
13
|
#
|
|
14
14
|
# @example List images
|
|
15
15
|
# doc = Uniword::DocumentFactory.from_file("report.docx")
|
|
@@ -50,15 +50,15 @@ module Uniword
|
|
|
50
50
|
parts = @document.image_parts
|
|
51
51
|
return [] unless parts && !parts.empty?
|
|
52
52
|
|
|
53
|
-
parts.map do |_r_id,
|
|
54
|
-
data =
|
|
53
|
+
parts.map do |_r_id, part|
|
|
54
|
+
data = part.data
|
|
55
55
|
px_w, px_h = detect_pixel_dimensions(data) if data
|
|
56
|
-
name = File.basename(
|
|
56
|
+
name = File.basename(part.target.to_s)
|
|
57
57
|
|
|
58
58
|
ImageInfo.new(
|
|
59
59
|
name: name,
|
|
60
|
-
path: "word/#{
|
|
61
|
-
content_type:
|
|
60
|
+
path: "word/#{part.target}",
|
|
61
|
+
content_type: part.content_type.to_s,
|
|
62
62
|
size: data ? data.bytesize : 0,
|
|
63
63
|
width: px_w,
|
|
64
64
|
height: px_h,
|
|
@@ -79,11 +79,11 @@ module Uniword
|
|
|
79
79
|
parts = @document.image_parts
|
|
80
80
|
return 0 unless parts && !parts.empty?
|
|
81
81
|
|
|
82
|
-
parts.each_value do |
|
|
83
|
-
data =
|
|
82
|
+
parts.each_value do |part|
|
|
83
|
+
data = part.data
|
|
84
84
|
next unless data
|
|
85
85
|
|
|
86
|
-
filename = File.basename(
|
|
86
|
+
filename = File.basename(part.target.to_s)
|
|
87
87
|
File.binwrite(File.join(output_dir, filename), data)
|
|
88
88
|
end
|
|
89
89
|
|
|
@@ -160,8 +160,8 @@ module Uniword
|
|
|
160
160
|
parts = @document.image_parts
|
|
161
161
|
return false unless parts && !parts.empty?
|
|
162
162
|
|
|
163
|
-
key = parts.find do |_k,
|
|
164
|
-
File.basename(
|
|
163
|
+
key = parts.find do |_k, part|
|
|
164
|
+
File.basename(part.target.to_s) == image_name
|
|
165
165
|
end&.first
|
|
166
166
|
|
|
167
167
|
return false unless key
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Ooxml
|
|
5
|
+
# Safe mutation of lutaml-model `element_order` arrays.
|
|
6
|
+
#
|
|
7
|
+
# lutaml-model freezes `element_order` on parsed models; mutating
|
|
8
|
+
# it raises FrozenError. These helpers thaw on demand (dup +
|
|
9
|
+
# assign back) and insert entries in schema position — the single
|
|
10
|
+
# mechanism for the whole library, not a monkey-patch.
|
|
11
|
+
#
|
|
12
|
+
# @example Thaw and append
|
|
13
|
+
# order = Ooxml::ElementOrder.mutable_order(footnotes)
|
|
14
|
+
# order << Lutaml::Xml::Element.new("Element", "footnote")
|
|
15
|
+
#
|
|
16
|
+
# @example Idempotent singleton insert
|
|
17
|
+
# Ooxml::ElementOrder.insert_once(settings, "updateFields",
|
|
18
|
+
# after: "characterSpacingControl")
|
|
19
|
+
module ElementOrder
|
|
20
|
+
# Return the model's element_order as a mutable array.
|
|
21
|
+
#
|
|
22
|
+
# The frozen parsed array is replaced by a dup assigned back to
|
|
23
|
+
# the model; already-mutable arrays are returned as-is (so
|
|
24
|
+
# repeated calls keep mutating the same registered array).
|
|
25
|
+
#
|
|
26
|
+
# @param model [Lutaml::Model::Serializable] model to thaw
|
|
27
|
+
# @return [Array, nil] mutable element_order, or nil when the
|
|
28
|
+
# model carries none
|
|
29
|
+
def mutable_order(model)
|
|
30
|
+
order = model.element_order
|
|
31
|
+
return unless order
|
|
32
|
+
return order unless order.frozen?
|
|
33
|
+
|
|
34
|
+
model.element_order = order.dup
|
|
35
|
+
end
|
|
36
|
+
module_function :mutable_order
|
|
37
|
+
|
|
38
|
+
# Append an entry (repeatable elements allowed).
|
|
39
|
+
#
|
|
40
|
+
# @param model [Lutaml::Model::Serializable] model to mutate
|
|
41
|
+
# @param entry [Lutaml::Xml::Element] entry to append
|
|
42
|
+
# @return [Array, nil] the mutated order
|
|
43
|
+
def append(model, entry)
|
|
44
|
+
mutable_order(model)&.<<(entry)
|
|
45
|
+
end
|
|
46
|
+
module_function :append
|
|
47
|
+
|
|
48
|
+
# Insert an entry at an index (repeatable elements allowed).
|
|
49
|
+
#
|
|
50
|
+
# @param model [Lutaml::Model::Serializable] model to mutate
|
|
51
|
+
# @param position [Integer] insertion index
|
|
52
|
+
# @param entry [Lutaml::Xml::Element] entry to insert
|
|
53
|
+
# @return [Array, nil] the mutated order
|
|
54
|
+
def insert_at(model, position, entry)
|
|
55
|
+
mutable_order(model)&.insert(position, entry)
|
|
56
|
+
end
|
|
57
|
+
module_function :insert_at
|
|
58
|
+
|
|
59
|
+
# Insert a singleton element in schema position, by name.
|
|
60
|
+
# Idempotent: no-op when an entry of that name already exists.
|
|
61
|
+
# Missing anchors fall back to end (after:) or start (before:).
|
|
62
|
+
#
|
|
63
|
+
# @param model [Lutaml::Model::Serializable] model to mutate
|
|
64
|
+
# @param name [String] element name to insert
|
|
65
|
+
# @param after [String, nil] insert after this element name
|
|
66
|
+
# @param before [String, nil] insert before this element name
|
|
67
|
+
# @param position [Integer, nil] explicit index (when neither
|
|
68
|
+
# after nor before is given; defaults to end)
|
|
69
|
+
# @return [Array, nil] the mutated order
|
|
70
|
+
def insert_once(model, name, after: nil, before: nil, position: nil)
|
|
71
|
+
order = mutable_order(model)
|
|
72
|
+
return unless order
|
|
73
|
+
return if order.any? { |e| e.name == name }
|
|
74
|
+
|
|
75
|
+
idx = insertion_index(order, after, before, position)
|
|
76
|
+
order.insert(idx, Lutaml::Xml::Element.new("Element", name))
|
|
77
|
+
end
|
|
78
|
+
module_function :insert_once
|
|
79
|
+
|
|
80
|
+
# @return [Integer] index for the insert
|
|
81
|
+
def insertion_index(order, after, before, position)
|
|
82
|
+
if after
|
|
83
|
+
anchor = order.index { |e| e.name == after }
|
|
84
|
+
anchor ? anchor + 1 : order.size
|
|
85
|
+
elsif before
|
|
86
|
+
order.index { |e| e.name == before } || 0
|
|
87
|
+
else
|
|
88
|
+
position || order.size
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
module_function :insertion_index
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|