uniword 1.2.5 → 1.3.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/CHANGELOG.md +223 -0
- data/CONTRIBUTING.md +1 -1
- data/config/ooxml/schemas/shared_types.yml +5 -4
- data/data/schemas/ecma/dc.xsd +118 -0
- data/data/schemas/ecma/dcmitype.xsd +50 -0
- data/data/schemas/ecma/dcterms.xsd +331 -0
- data/data/schemas/ecma/opc-coreProperties.xsd +2 -2
- data/data/schemas/ecma/xml.xsd +117 -0
- data/lib/uniword/builder/bibliography_builder.rb +1 -1
- data/lib/uniword/builder/chart_builder.rb +19 -9
- data/lib/uniword/builder/comment_anchorer.rb +221 -0
- data/lib/uniword/builder/document_builder.rb +97 -6
- data/lib/uniword/builder/image_builder.rb +6 -5
- data/lib/uniword/builder/run_utils.rb +2 -0
- data/lib/uniword/builder.rb +1 -0
- data/lib/uniword/cli/fonts_cli.rb +52 -0
- data/lib/uniword/cli/main.rb +110 -11
- data/lib/uniword/cli/page_cli.rb +85 -0
- data/lib/uniword/cli/styles_cli.rb +204 -0
- data/lib/uniword/cli/theme_cli.rb +88 -0
- data/lib/uniword/cli/toc_cli.rb +22 -2
- data/lib/uniword/comment.rb +20 -15
- data/lib/uniword/comment_range.rb +3 -3
- data/lib/uniword/comments_part.rb +51 -19
- data/lib/uniword/configuration/configuration_loader.rb +5 -3
- data/lib/uniword/configuration.rb +93 -1
- data/lib/uniword/content_types.rb +19 -39
- data/lib/uniword/document_factory.rb +9 -1
- data/lib/uniword/document_writer.rb +16 -5
- data/lib/uniword/docx/chart_part.rb +34 -0
- data/lib/uniword/docx/custom_xml_item.rb +83 -0
- data/lib/uniword/docx/header_footer_part.rb +140 -0
- data/lib/uniword/docx/header_footer_part_collection.rb +135 -0
- data/lib/uniword/docx/header_footer_view.rb +225 -0
- data/lib/uniword/docx/id_allocator.rb +130 -36
- data/lib/uniword/docx/package.rb +223 -35
- data/lib/uniword/docx/package_defaults.rb +58 -78
- data/lib/uniword/docx/package_integrity_checker.rb +312 -0
- data/lib/uniword/docx/package_serialization.rb +103 -231
- data/lib/uniword/docx/part.rb +106 -0
- data/lib/uniword/docx/part_collection.rb +123 -0
- data/lib/uniword/docx/reconciler/body.rb +59 -68
- data/lib/uniword/docx/reconciler/fix.rb +46 -0
- data/lib/uniword/docx/reconciler/fix_codes.rb +37 -30
- data/lib/uniword/docx/reconciler/helpers.rb +9 -1
- data/lib/uniword/docx/reconciler/notes.rb +17 -8
- data/lib/uniword/docx/reconciler/package_structure.rb +169 -221
- data/lib/uniword/docx/reconciler/parts.rb +36 -13
- data/lib/uniword/docx/reconciler/referential_integrity.rb +297 -123
- data/lib/uniword/docx/reconciler/tables.rb +78 -16
- data/lib/uniword/docx/reconciler/theme.rb +7 -4
- data/lib/uniword/docx/reconciler.rb +36 -14
- data/lib/uniword/docx.rb +10 -0
- data/lib/uniword/drawingml/blip.rb +0 -1
- data/lib/uniword/drawingml/color_scheme.rb +5 -6
- data/lib/uniword/drawingml/font_scheme.rb +4 -8
- data/lib/uniword/drawingml/theme.rb +12 -6
- data/lib/uniword/errors.rb +8 -1
- data/lib/uniword/footer.rb +4 -0
- data/lib/uniword/header.rb +4 -0
- data/lib/uniword/hyperlink.rb +1 -1
- data/lib/uniword/image.rb +6 -6
- data/lib/uniword/lazy_loader.rb +1 -2
- data/lib/uniword/model_attribute_access.rb +20 -4
- data/lib/uniword/ooxml/dotx_package.rb +42 -2
- data/lib/uniword/ooxml/part_definition.rb +171 -0
- data/lib/uniword/ooxml/part_registry.rb +311 -0
- data/lib/uniword/ooxml/relationships/image_relationship.rb +1 -1
- data/lib/uniword/ooxml/relationships/package_relationships.rb +20 -22
- data/lib/uniword/ooxml/schema/element_serializer.rb +8 -7
- data/lib/uniword/ooxml/types/hex_color_value.rb +38 -0
- data/lib/uniword/ooxml/types/ooxml_boolean.rb +27 -14
- data/lib/uniword/ooxml/types/ooxml_boolean_optional.rb +21 -16
- data/lib/uniword/ooxml/types/theme_color_value.rb +22 -0
- data/lib/uniword/ooxml/types/unsigned_decimal_number.rb +30 -0
- data/lib/uniword/ooxml/types.rb +10 -0
- data/lib/uniword/ooxml.rb +2 -0
- data/lib/uniword/properties/alignment.rb +8 -3
- data/lib/uniword/properties/border.rb +52 -6
- data/lib/uniword/properties/cell_vertical_align.rb +4 -1
- data/lib/uniword/properties/color_value.rb +8 -3
- data/lib/uniword/properties/highlight.rb +9 -5
- data/lib/uniword/properties/shading.rb +4 -3
- data/lib/uniword/properties/tab_stop.rb +6 -1
- data/lib/uniword/properties/table_justification.rb +4 -1
- data/lib/uniword/properties/underline.rb +4 -2
- data/lib/uniword/properties/vertical_align.rb +6 -3
- data/lib/uniword/review/review_manager.rb +23 -9
- data/lib/uniword/revision.rb +9 -9
- data/lib/uniword/schema/model_generator.rb +31 -15
- data/lib/uniword/shared_types/hex_color.rb +4 -1
- data/lib/uniword/shared_types/pixel_measure.rb +4 -1
- data/lib/uniword/shared_types/point_measure.rb +4 -1
- data/lib/uniword/shared_types/text_alignment.rb +6 -1
- data/lib/uniword/shared_types/twips_measure.rb +4 -1
- data/lib/uniword/spreadsheetml/phonetic_pr.rb +1 -1
- data/lib/uniword/spreadsheetml/shared_string_table.rb +2 -2
- data/lib/uniword/spreadsheetml/table_formula.rb +1 -1
- data/lib/uniword/template/variable_resolver.rb +40 -7
- data/lib/uniword/themes/theme_transformation.rb +55 -41
- data/lib/uniword/toc/toc_generator.rb +2 -0
- data/lib/uniword/validation/engine.rb +35 -0
- data/lib/uniword/validation/link_checker.rb +1 -1
- data/lib/uniword/validation/opc_validator.rb +0 -1
- data/lib/uniword/validation/report/layer_result.rb +0 -1
- data/lib/uniword/validation/report/terminal_formatter.rb +0 -1
- data/lib/uniword/validation/report/verification_report.rb +0 -2
- data/lib/uniword/validation/report.rb +20 -0
- data/lib/uniword/validation/rules/base.rb +10 -2
- data/lib/uniword/validation/rules/bookmark_pairing_rule.rb +38 -0
- data/lib/uniword/validation/rules/bookmark_uniqueness_rule.rb +48 -0
- data/lib/uniword/validation/rules/bookmarks_rule.rb +0 -2
- data/lib/uniword/validation/rules/content_types_coverage_rule.rb +0 -2
- data/lib/uniword/validation/rules/core_properties_namespace_rule.rb +0 -2
- data/lib/uniword/validation/rules/document_body_rule.rb +25 -0
- data/lib/uniword/validation/rules/document_context.rb +7 -0
- data/lib/uniword/validation/rules/empty_paragraphs_rule.rb +27 -0
- data/lib/uniword/validation/rules/font_table_signature_rule.rb +0 -2
- data/lib/uniword/validation/rules/fonts_rule.rb +0 -2
- data/lib/uniword/validation/rules/footnotes_rule.rb +0 -2
- data/lib/uniword/validation/rules/headers_footers_rule.rb +0 -2
- data/lib/uniword/validation/rules/images_rule.rb +0 -2
- data/lib/uniword/validation/rules/mc_ignorable_namespace_rule.rb +0 -2
- data/lib/uniword/validation/rules/model_context.rb +32 -0
- data/lib/uniword/validation/rules/model_rule.rb +34 -0
- data/lib/uniword/validation/rules/numbering_preservation_rule.rb +0 -2
- data/lib/uniword/validation/rules/numbering_rule.rb +0 -2
- data/lib/uniword/validation/rules/relationship_integrity_rule.rb +0 -1
- data/lib/uniword/validation/rules/rsid_rule.rb +0 -2
- data/lib/uniword/validation/rules/section_properties_rule.rb +0 -2
- data/lib/uniword/validation/rules/settings_rule.rb +0 -2
- data/lib/uniword/validation/rules/settings_values_rule.rb +0 -2
- data/lib/uniword/validation/rules/style_references_rule.rb +0 -2
- data/lib/uniword/validation/rules/table_grid_rule.rb +27 -0
- data/lib/uniword/validation/rules/table_properties_rule.rb +27 -0
- data/lib/uniword/validation/rules/tables_rule.rb +0 -2
- data/lib/uniword/validation/rules/theme_completeness_rule.rb +0 -2
- data/lib/uniword/validation/rules/theme_rule.rb +0 -2
- data/lib/uniword/validation/rules.rb +61 -23
- data/lib/uniword/validation/schema_registry.rb +11 -0
- data/lib/uniword/validation/validators/document_semantics_validator.rb +1 -9
- data/lib/uniword/validation/validators/xml_schema_validator.rb +0 -2
- data/lib/uniword/validation/validators.rb +0 -9
- data/lib/uniword/validation/verify_orchestrator.rb +0 -7
- data/lib/uniword/validation.rb +7 -2
- data/lib/uniword/version.rb +1 -1
- data/lib/uniword/vml/imagedata.rb +0 -1
- data/lib/uniword/wordprocessingml/attached_template.rb +0 -1
- data/lib/uniword/wordprocessingml/deleted_text.rb +1 -1
- data/lib/uniword/wordprocessingml/document_root.rb +211 -21
- data/lib/uniword/wordprocessingml/endnotes.rb +4 -0
- data/lib/uniword/wordprocessingml/font_replacer.rb +147 -0
- data/lib/uniword/wordprocessingml/footnotes.rb +4 -0
- data/lib/uniword/wordprocessingml/hdr_shape_defaults.rb +0 -1
- data/lib/uniword/wordprocessingml/hyperlink.rb +0 -1
- data/lib/uniword/wordprocessingml/level.rb +6 -20
- data/lib/uniword/wordprocessingml/math_pr.rb +0 -11
- data/lib/uniword/wordprocessingml/numbering_definition.rb +16 -8
- data/lib/uniword/wordprocessingml/page_setup.rb +181 -0
- data/lib/uniword/wordprocessingml/paragraph_properties.rb +33 -28
- data/lib/uniword/wordprocessingml/rsids.rb +0 -2
- data/lib/uniword/wordprocessingml/run.rb +3 -0
- data/lib/uniword/wordprocessingml/section_properties.rb +2 -2
- data/lib/uniword/wordprocessingml/settings.rb +8 -34
- data/lib/uniword/wordprocessingml/shape_defaults.rb +0 -1
- data/lib/uniword/wordprocessingml/style.rb +2 -1
- data/lib/uniword/wordprocessingml/style_cleanup.rb +198 -0
- data/lib/uniword/wordprocessingml/table_cell_properties.rb +6 -6
- data/lib/uniword/wordprocessingml/update_fields.rb +27 -0
- data/lib/uniword/wordprocessingml/w14_attributes.rb +30 -2
- data/lib/uniword/wordprocessingml.rb +4 -0
- data/lib/uniword.rb +27 -2
- metadata +38 -19
- data/config/validation_rules.yml +0 -60
- data/config/warning_rules.yml +0 -57
- data/lib/uniword/validation/document_validator.rb +0 -272
- data/lib/uniword/validation/structural_validator.rb +0 -116
- data/lib/uniword/validation/validators/content_type_validator.rb +0 -165
- data/lib/uniword/validation/validators/file_structure_validator.rb +0 -104
- data/lib/uniword/validation/validators/ooxml_part_validator.rb +0 -128
- data/lib/uniword/validation/validators/relationship_validator.rb +0 -147
- data/lib/uniword/validation/validators/zip_integrity_validator.rb +0 -110
- data/lib/uniword/validators/element_validator.rb +0 -93
- data/lib/uniword/validators/paragraph_validator.rb +0 -116
- data/lib/uniword/validators/table_validator.rb +0 -134
- data/lib/uniword/validators.rb +0 -9
- data/lib/uniword/warnings/warning.rb +0 -130
- data/lib/uniword/warnings/warning_collector.rb +0 -234
- data/lib/uniword/warnings/warning_report.rb +0 -159
- data/lib/uniword/warnings.rb +0 -9
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Docx
|
|
5
|
+
# Kind-filtered view over a document's HeaderFooterPartCollection.
|
|
6
|
+
#
|
|
7
|
+
# +document.headers+ and +document.footers+ return these views, so
|
|
8
|
+
# the Builder, the headers/footers managers and the serializer all
|
|
9
|
+
# share the single underlying store.
|
|
10
|
+
#
|
|
11
|
+
# Two historic access shapes are preserved:
|
|
12
|
+
# - Hash-style (Builder): +view["default"] = header_model+,
|
|
13
|
+
# +view["default"]+, +each_value+, +values+, +keys+, +key?+,
|
|
14
|
+
# +delete+. Content access yields the Header/Footer models.
|
|
15
|
+
# - Array-style (managers): +view << header+, +each+, +map+,
|
|
16
|
+
# +find+, +reject+. Iteration yields HeaderFooterPart objects,
|
|
17
|
+
# which carry the sectPr +type+ and delegate +paragraphs+,
|
|
18
|
+
# +tables+ and +empty?+ to their content.
|
|
19
|
+
#
|
|
20
|
+
# Upsert semantics: assigning a type that already exists replaces
|
|
21
|
+
# that part's content in place (keeping its target and rId), so
|
|
22
|
+
# loaded and builder-created parts can never duplicate.
|
|
23
|
+
class HeaderFooterView
|
|
24
|
+
include Enumerable
|
|
25
|
+
|
|
26
|
+
# @return [Symbol] :header or :footer
|
|
27
|
+
attr_reader :kind
|
|
28
|
+
|
|
29
|
+
# @param store [HeaderFooterPartCollection] the unified store
|
|
30
|
+
# @param kind [Symbol] :header or :footer
|
|
31
|
+
def initialize(store, kind)
|
|
32
|
+
@store = store
|
|
33
|
+
@kind = kind
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Iterate over the parts of this kind (Array-style access).
|
|
37
|
+
def each(&block)
|
|
38
|
+
@store.of_kind(kind).each(&block)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Hash-style read: content model for a sectPr type.
|
|
42
|
+
#
|
|
43
|
+
# @param type [String, Symbol] "default"/"first"/"even"
|
|
44
|
+
# @return [Wordprocessingml::Header, Wordprocessingml::Footer, nil]
|
|
45
|
+
def [](type)
|
|
46
|
+
@store.find_part(kind, type)&.content
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Hash-style write: upsert a content model by sectPr type.
|
|
50
|
+
# Replaces the existing part's content in place when the type
|
|
51
|
+
# exists; otherwise appends a new part with the next free
|
|
52
|
+
# numbered target.
|
|
53
|
+
#
|
|
54
|
+
# @param type [String, Symbol] sectPr reference type
|
|
55
|
+
# @param model [Object] Wordprocessingml::Header/Footer or
|
|
56
|
+
# legacy Uniword::Header/Footer
|
|
57
|
+
# @return [Object] the assigned model
|
|
58
|
+
def []=(type, model)
|
|
59
|
+
type = type.to_s
|
|
60
|
+
content = coerce_content(model)
|
|
61
|
+
part = @store.find_part(kind, type)
|
|
62
|
+
if part
|
|
63
|
+
part.content = content
|
|
64
|
+
part.loaded = false
|
|
65
|
+
else
|
|
66
|
+
@store << HeaderFooterPart.new(
|
|
67
|
+
kind: kind, type: type, content: content,
|
|
68
|
+
target: @store.next_target(kind)
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
model
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Append a model (Array-style). Legacy Uniword::Header/Footer
|
|
75
|
+
# models supply their own +type+; other models are stored
|
|
76
|
+
# without a type.
|
|
77
|
+
#
|
|
78
|
+
# @param model [Object] content model or HeaderFooterPart
|
|
79
|
+
# @return [Object] the appended model
|
|
80
|
+
def <<(model)
|
|
81
|
+
if model.is_a?(HeaderFooterPart)
|
|
82
|
+
adopt_part(model)
|
|
83
|
+
else
|
|
84
|
+
append_model(model)
|
|
85
|
+
end
|
|
86
|
+
model
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Iterate over sectPr types.
|
|
90
|
+
def each_key(&block)
|
|
91
|
+
map(&:type).each(&block)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Iterate over content models.
|
|
95
|
+
def each_value(&block)
|
|
96
|
+
map(&:content).each(&block)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @return [Array<String, nil>] sectPr types
|
|
100
|
+
def keys
|
|
101
|
+
map(&:type)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @return [Array] content models
|
|
105
|
+
def values
|
|
106
|
+
map(&:content)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @param type [String, Symbol]
|
|
110
|
+
# @return [Boolean]
|
|
111
|
+
def key?(type)
|
|
112
|
+
!@store.find_part(kind, type).nil?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Remove the part for a sectPr type.
|
|
116
|
+
#
|
|
117
|
+
# @param type [String, Symbol]
|
|
118
|
+
# @return [Object, nil] the removed part's content
|
|
119
|
+
def delete(type)
|
|
120
|
+
part = @store.find_part(kind, type)
|
|
121
|
+
return nil unless part
|
|
122
|
+
|
|
123
|
+
@store.delete(part)
|
|
124
|
+
part.content
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @return [Integer]
|
|
128
|
+
def size
|
|
129
|
+
@store.of_kind(kind).size
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @return [Boolean]
|
|
133
|
+
def empty?
|
|
134
|
+
size.zero?
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Bulk assignment (+document.headers = value+). Accepts nil
|
|
138
|
+
# (clears this kind), a Hash of type => model, an Array of
|
|
139
|
+
# models/parts, or the view itself (no-op).
|
|
140
|
+
#
|
|
141
|
+
# @param value [nil, Hash, Array, HeaderFooterView]
|
|
142
|
+
# @return [Object] the assigned value
|
|
143
|
+
def replace(value)
|
|
144
|
+
case value
|
|
145
|
+
when nil then @store.replace_kind(kind, [])
|
|
146
|
+
when HeaderFooterView then nil
|
|
147
|
+
when Hash then replace_from_hash(value)
|
|
148
|
+
when Array then replace_from_array(value)
|
|
149
|
+
else
|
|
150
|
+
raise ArgumentError,
|
|
151
|
+
"cannot assign #{value.class} to document.#{kind}s"
|
|
152
|
+
end
|
|
153
|
+
value
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
def adopt_part(part)
|
|
159
|
+
part.target ||= @store.next_target(part.kind || kind)
|
|
160
|
+
@store << part
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def append_model(model)
|
|
164
|
+
type = legacy_type(model)
|
|
165
|
+
return self[type] = model if type
|
|
166
|
+
|
|
167
|
+
@store << HeaderFooterPart.new(
|
|
168
|
+
kind: kind, content: coerce_content(model),
|
|
169
|
+
target: @store.next_target(kind)
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def replace_from_hash(hash)
|
|
174
|
+
hash.each { |type, model| self[type] = model }
|
|
175
|
+
kept = hash.keys.map(&:to_s)
|
|
176
|
+
@store.of_kind(kind).each do |part|
|
|
177
|
+
@store.delete(part) if stale_fresh_part?(part, kept)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# A programmatically-added part is stale after a Hash assignment
|
|
182
|
+
# when its type is not among the assigned keys (loaded parts are
|
|
183
|
+
# never removed by view assignment).
|
|
184
|
+
def stale_fresh_part?(part, kept_types)
|
|
185
|
+
!part.loaded? && !part.type.nil? && !kept_types.include?(part.type)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def replace_from_array(array)
|
|
189
|
+
parts = array.map { |entry| coerce_to_part(entry) }
|
|
190
|
+
@store.replace_kind(kind, [])
|
|
191
|
+
parts.each do |part|
|
|
192
|
+
part.target ||= @store.next_target(kind)
|
|
193
|
+
@store << part
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def coerce_to_part(entry)
|
|
198
|
+
case entry
|
|
199
|
+
when HeaderFooterPart then entry
|
|
200
|
+
when Hash then HeaderFooterPartCollection.wrap(entry)
|
|
201
|
+
else
|
|
202
|
+
HeaderFooterPart.new(
|
|
203
|
+
kind: kind, type: legacy_type(entry),
|
|
204
|
+
content: coerce_content(entry)
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Legacy Uniword::Header/Footer carry their sectPr type.
|
|
210
|
+
def legacy_type(model)
|
|
211
|
+
case model
|
|
212
|
+
when Uniword::Header, Uniword::Footer then model.type
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Content is stored as given: legacy Uniword::Header/Footer
|
|
217
|
+
# models stay live (callers may keep mutating them) and are
|
|
218
|
+
# converted to their Wordprocessingml counterparts only at
|
|
219
|
+
# serialization time (HeaderFooterPart#serializable_content).
|
|
220
|
+
def coerce_content(model)
|
|
221
|
+
model
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -17,22 +17,32 @@ module Uniword
|
|
|
17
17
|
#
|
|
18
18
|
# This is the "populate-first" principle: when loading a template DOCX,
|
|
19
19
|
# parse and seed ALL existing IDs from the template before modification.
|
|
20
|
+
#
|
|
21
|
+
# rId namespaces are per relationships part: "rId1" in _rels/.rels and
|
|
22
|
+
# "rId1" in word/_rels/document.xml.rels do not collide. The allocator
|
|
23
|
+
# therefore tracks rIds per +scope+ (:document for document-level rels,
|
|
24
|
+
# :package for package-level rels); each scope has its own registry,
|
|
25
|
+
# counter and uniqueness domain.
|
|
20
26
|
class IdAllocator
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
# Relationship type namespace base (also the r: namespace URI).
|
|
28
|
+
# Individual rel type constants derive from Ooxml::PartRegistry,
|
|
29
|
+
# the single source of truth for part metadata.
|
|
30
|
+
REL_TYPE_BASE = Ooxml::PartRegistry::OFFICE_REL_BASE
|
|
31
|
+
|
|
32
|
+
IMAGE_REL_TYPE = Ooxml::PartRegistry.find_by_key(:image).rel_type
|
|
33
|
+
HEADER_REL_TYPE = Ooxml::PartRegistry.find_by_key(:header).rel_type
|
|
34
|
+
FOOTER_REL_TYPE = Ooxml::PartRegistry.find_by_key(:footer).rel_type
|
|
35
|
+
HYPERLINK_REL_TYPE = Ooxml::PartRegistry.find_by_key(:hyperlink).rel_type
|
|
36
|
+
CHART_REL_TYPE = Ooxml::PartRegistry.find_by_key(:chart).rel_type
|
|
37
|
+
FOOTNOTES_REL_TYPE = Ooxml::PartRegistry.find_by_key(:footnotes).rel_type
|
|
38
|
+
ENDNOTES_REL_TYPE = Ooxml::PartRegistry.find_by_key(:endnotes).rel_type
|
|
39
|
+
THEME_REL_TYPE = Ooxml::PartRegistry.find_by_key(:theme).rel_type
|
|
40
|
+
NUMBERING_REL_TYPE = Ooxml::PartRegistry.find_by_key(:numbering).rel_type
|
|
32
41
|
|
|
33
42
|
def initialize
|
|
34
|
-
@
|
|
35
|
-
|
|
43
|
+
@rid_counters = Hash.new(0) # scope -> high-water mark
|
|
44
|
+
# [scope, target, type] -> { id, type, target, target_mode, scope }
|
|
45
|
+
@rid_entries = {}
|
|
36
46
|
@footnote_counter = 1
|
|
37
47
|
@endnote_counter = 1
|
|
38
48
|
@bookmark_counter = 0
|
|
@@ -43,16 +53,56 @@ module Uniword
|
|
|
43
53
|
|
|
44
54
|
# Allocate a relationship ID for a target+type pair.
|
|
45
55
|
# Returns existing rId if this target+type was already registered.
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
#
|
|
57
|
+
# @param target [String] relationship target
|
|
58
|
+
# @param type [String] relationship type
|
|
59
|
+
# @param target_mode [String, nil] e.g. "External"
|
|
60
|
+
# @param scope [Symbol] :document or :package (rels part namespace)
|
|
61
|
+
# @return [String] the allocated (or existing) rId
|
|
62
|
+
def alloc_rid(target:, type:, target_mode: nil, scope: :document)
|
|
63
|
+
key = [scope, target, type.to_s]
|
|
48
64
|
@rid_entries[key] ||= begin
|
|
49
|
-
@
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
@rid_counters[scope] += 1
|
|
66
|
+
@rid_counters[scope] += 1 while rid_id_taken?(
|
|
67
|
+
"rId#{@rid_counters[scope]}", scope
|
|
68
|
+
)
|
|
69
|
+
{ id: "rId#{@rid_counters[scope]}", type: type.to_s,
|
|
70
|
+
target: target, target_mode: target_mode, scope: scope }
|
|
52
71
|
end
|
|
53
72
|
@rid_entries[key][:id]
|
|
54
73
|
end
|
|
55
74
|
|
|
75
|
+
# Register an explicit rId for a target+type pair.
|
|
76
|
+
# Used when a relationship id is assigned outside the counter
|
|
77
|
+
# (rId dedup repair) so later lookups stay consistent.
|
|
78
|
+
#
|
|
79
|
+
# @param id [String] relationship ID to register
|
|
80
|
+
# @param target [String] relationship target
|
|
81
|
+
# @param type [String] relationship type
|
|
82
|
+
# @param target_mode [String, nil] e.g. "External"
|
|
83
|
+
# @param scope [Symbol] :document or :package
|
|
84
|
+
# @return [String] the registered id
|
|
85
|
+
def register_rid(id, target:, type:, target_mode: nil, scope: :document)
|
|
86
|
+
@rid_entries[[scope, target, type.to_s]] = {
|
|
87
|
+
id: id, type: type.to_s, target: target, target_mode: target_mode,
|
|
88
|
+
scope: scope
|
|
89
|
+
}
|
|
90
|
+
num = id[/\ArId(\d+)\z/, 1]&.to_i || 0
|
|
91
|
+
@rid_counters[scope] = [@rid_counters[scope], num].max
|
|
92
|
+
id
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The next free rId past the high-water mark of a scope —
|
|
96
|
+
# guaranteed not registered to any target+type in that scope.
|
|
97
|
+
#
|
|
98
|
+
# @param scope [Symbol] :document or :package
|
|
99
|
+
# @return [String] e.g. "rId7"
|
|
100
|
+
def next_free_rid(scope: :document)
|
|
101
|
+
candidate = @rid_counters[scope] + 1
|
|
102
|
+
candidate += 1 while rid_id_taken?("rId#{candidate}", scope)
|
|
103
|
+
"rId#{candidate}"
|
|
104
|
+
end
|
|
105
|
+
|
|
56
106
|
def alloc_footnote_id
|
|
57
107
|
id = @footnote_counter
|
|
58
108
|
@footnote_counter += 1
|
|
@@ -85,21 +135,19 @@ module Uniword
|
|
|
85
135
|
Digest::SHA256.hexdigest("rsid:#{@rsid_counter}").upcase[0, 8]
|
|
86
136
|
end
|
|
87
137
|
|
|
88
|
-
# Seed from a relationships collection — preserves existing rIds
|
|
89
|
-
|
|
138
|
+
# Seed from a relationships collection — preserves existing rIds
|
|
139
|
+
# verbatim so a load→save round-trip is rId-stable. A loaded rId
|
|
140
|
+
# already registered to a different target+type in the same scope
|
|
141
|
+
# (only possible when allocation preceded seeding, or the source
|
|
142
|
+
# has duplicate ids) yields a fresh counter allocation for the
|
|
143
|
+
# seeded pair instead — uniqueness wins over verbatim preservation.
|
|
144
|
+
#
|
|
145
|
+
# @param relationships [Array, nil] rels to seed
|
|
146
|
+
# @param scope [Symbol] :document or :package (rels part namespace)
|
|
147
|
+
def seed_from_rels(relationships, scope: :document)
|
|
90
148
|
return unless relationships
|
|
91
149
|
|
|
92
|
-
relationships.each
|
|
93
|
-
key = [r.target, r.type.to_s]
|
|
94
|
-
@rid_entries[key] = {
|
|
95
|
-
id: r.id,
|
|
96
|
-
type: r.type.to_s,
|
|
97
|
-
target: r.target,
|
|
98
|
-
target_mode: r.target_mode,
|
|
99
|
-
}
|
|
100
|
-
num = r.id[/\ArId(\d+)\z/, 1]&.to_i || 0
|
|
101
|
-
@rid_counter = [@rid_counter, num].max
|
|
102
|
-
end
|
|
150
|
+
relationships.each { |r| seed_rel(r, scope) }
|
|
103
151
|
end
|
|
104
152
|
|
|
105
153
|
# Seed footnote/endnote counters from existing note entries.
|
|
@@ -114,14 +162,26 @@ module Uniword
|
|
|
114
162
|
end
|
|
115
163
|
end
|
|
116
164
|
|
|
165
|
+
# Seed the comment counter from existing comment entries so newly
|
|
166
|
+
# allocated comment IDs do not collide with loaded ones.
|
|
167
|
+
def seed_from_comments(comments)
|
|
168
|
+
comments&.each do |c|
|
|
169
|
+
@comment_counter = [@comment_counter, c.comment_id.to_i].max
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
117
173
|
# Produce the final ordered list of all allocated relationships.
|
|
118
|
-
|
|
119
|
-
|
|
174
|
+
#
|
|
175
|
+
# @param scope [Symbol, nil] restrict to one scope; nil for all
|
|
176
|
+
def all_rels(scope: nil)
|
|
177
|
+
entries = @rid_entries.values
|
|
178
|
+
entries = entries.select { |r| r[:scope] == scope } if scope
|
|
179
|
+
entries.sort_by { |r| r[:id][/\d+/]&.to_i || 0 }
|
|
120
180
|
end
|
|
121
181
|
|
|
122
182
|
# Check if a relationship has been registered for a target+type.
|
|
123
|
-
def rid_for(target:, type:)
|
|
124
|
-
key = [target, type.to_s]
|
|
183
|
+
def rid_for(target:, type:, scope: :document)
|
|
184
|
+
key = [scope, target, type.to_s]
|
|
125
185
|
@rid_entries[key]&.fetch(:id, nil)
|
|
126
186
|
end
|
|
127
187
|
|
|
@@ -131,13 +191,47 @@ module Uniword
|
|
|
131
191
|
def self.populate_from_package(package)
|
|
132
192
|
alloc = new
|
|
133
193
|
alloc.seed_from_rels(package.document_rels&.relationships)
|
|
134
|
-
alloc.seed_from_rels(package.package_rels&.relationships
|
|
194
|
+
alloc.seed_from_rels(package.package_rels&.relationships,
|
|
195
|
+
scope: :package)
|
|
135
196
|
alloc.seed_from_notes(
|
|
136
197
|
package.footnotes&.footnote_entries,
|
|
137
198
|
package.endnotes&.endnote_entries,
|
|
138
199
|
)
|
|
200
|
+
alloc.seed_from_comments(comment_entries_of(package))
|
|
139
201
|
alloc
|
|
140
202
|
end
|
|
203
|
+
|
|
204
|
+
# Comment entries of a package or document, tolerating the legacy
|
|
205
|
+
# Array form of DocumentRoot#comments.
|
|
206
|
+
def self.comment_entries_of(source)
|
|
207
|
+
part = source.comments
|
|
208
|
+
part.is_a?(Uniword::CommentsPart) ? part.comments : part
|
|
209
|
+
end
|
|
210
|
+
private_class_method :comment_entries_of
|
|
211
|
+
|
|
212
|
+
private
|
|
213
|
+
|
|
214
|
+
# Seed one relationship (see seed_from_rels for the collision rule).
|
|
215
|
+
def seed_rel(rel, scope)
|
|
216
|
+
key = [scope, rel.target, rel.type.to_s]
|
|
217
|
+
return if @rid_entries.key?(key)
|
|
218
|
+
|
|
219
|
+
if rid_id_taken?(rel.id, scope)
|
|
220
|
+
alloc_rid(target: rel.target, type: rel.type,
|
|
221
|
+
target_mode: rel.target_mode, scope: scope)
|
|
222
|
+
return
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
register_rid(rel.id, target: rel.target, type: rel.type,
|
|
226
|
+
target_mode: rel.target_mode, scope: scope)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Whether an rId string is already registered within a scope.
|
|
230
|
+
def rid_id_taken?(id, scope)
|
|
231
|
+
@rid_entries.any? do |key, entry|
|
|
232
|
+
key[0] == scope && entry[:id] == id
|
|
233
|
+
end
|
|
234
|
+
end
|
|
141
235
|
end
|
|
142
236
|
end
|
|
143
237
|
end
|