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,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Docx
|
|
5
|
+
# Keyed collection of Docx::Part objects.
|
|
6
|
+
#
|
|
7
|
+
# Backs +chart_parts+ (keyed by relationship id) and +embeddings+
|
|
8
|
+
# (keyed by relationship target). Keeps the legacy Hash-like
|
|
9
|
+
# access patterns working: assignment accepts Part objects, the
|
|
10
|
+
# former raw hashes ({ xml:, target: }) and raw content Strings,
|
|
11
|
+
# normalizing them into Part objects.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# parts = PartCollection.new(:r_id)
|
|
15
|
+
# parts["rIdChart1"] = { xml: "...", target: "charts/chart1.xml" }
|
|
16
|
+
# parts.values.first[:target] # => "charts/chart1.xml"
|
|
17
|
+
class PartCollection
|
|
18
|
+
include Enumerable
|
|
19
|
+
|
|
20
|
+
# @param key_attribute [Symbol] part attribute serving as the
|
|
21
|
+
# collection key (:r_id for charts, :target for embeddings)
|
|
22
|
+
# @param part_class [Class] Part subclass used to wrap raw values
|
|
23
|
+
def initialize(key_attribute, part_class)
|
|
24
|
+
@key_attribute = key_attribute
|
|
25
|
+
@part_class = part_class
|
|
26
|
+
@parts = {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param key [String] collection key (rId or target)
|
|
30
|
+
# @return [Part, nil]
|
|
31
|
+
def [](key)
|
|
32
|
+
@parts[key]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Store a part, normalizing raw hashes and raw content.
|
|
36
|
+
#
|
|
37
|
+
# @param key [String] collection key
|
|
38
|
+
# @param value [Part, Hash, String]
|
|
39
|
+
def []=(key, value)
|
|
40
|
+
@parts[key] = wrap(key, value)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Iterate over [key, Part] pairs.
|
|
44
|
+
def each(&block)
|
|
45
|
+
@parts.each(&block)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Iterate over collection keys.
|
|
49
|
+
def each_key(&block)
|
|
50
|
+
@parts.each_key(&block)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Iterate over stored parts.
|
|
54
|
+
def each_value(&block)
|
|
55
|
+
@parts.each_value(&block)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Array<Part>]
|
|
59
|
+
def values
|
|
60
|
+
@parts.values
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [Array<String>]
|
|
64
|
+
def keys
|
|
65
|
+
@parts.keys
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Integer]
|
|
69
|
+
def size
|
|
70
|
+
@parts.size
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [Boolean]
|
|
74
|
+
def empty?
|
|
75
|
+
@parts.empty?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [Boolean]
|
|
79
|
+
def key?(key)
|
|
80
|
+
@parts.key?(key)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [Part, nil] the removed part
|
|
84
|
+
def delete(key)
|
|
85
|
+
@parts.delete(key)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Replace the whole collection from a Hash (or another
|
|
89
|
+
# PartCollection); nil clears it.
|
|
90
|
+
#
|
|
91
|
+
# @param value [Hash, PartCollection, nil]
|
|
92
|
+
# @return [void]
|
|
93
|
+
def replace_all(value)
|
|
94
|
+
@parts.clear
|
|
95
|
+
return if value.nil?
|
|
96
|
+
|
|
97
|
+
value.each { |key, part| self[key] = part }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def wrap(key, value)
|
|
103
|
+
case value
|
|
104
|
+
when Part
|
|
105
|
+
value
|
|
106
|
+
when Hash
|
|
107
|
+
@part_class.new(
|
|
108
|
+
target: value[:target], content: value[:content] || value[:xml],
|
|
109
|
+
).tap { |part| assign_key(part, key) }
|
|
110
|
+
else
|
|
111
|
+
@part_class.new(content: value).tap { |part| assign_key(part, key) }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def assign_key(part, key)
|
|
116
|
+
case @key_attribute
|
|
117
|
+
when :r_id then part.r_id ||= key.to_s
|
|
118
|
+
when :target then part.target ||= key.to_s
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -21,7 +21,8 @@ module Uniword
|
|
|
21
21
|
doc_grid: Wordprocessingml::PageDefaults.default_doc_grid,
|
|
22
22
|
)
|
|
23
23
|
record_fix(FixCodes::SECTION_PROPERTIES_DEFAULTED,
|
|
24
|
-
"Added default section properties with US Letter page size"
|
|
24
|
+
"Added default section properties with US Letter page size",
|
|
25
|
+
part: "word/document.xml")
|
|
25
26
|
return
|
|
26
27
|
end
|
|
27
28
|
|
|
@@ -41,70 +42,82 @@ module Uniword
|
|
|
41
42
|
end
|
|
42
43
|
if fixed
|
|
43
44
|
record_fix(FixCodes::SECTION_PROPERTIES_DEFAULTED,
|
|
44
|
-
"Filled missing pgSz/pgMar/cols in existing section properties"
|
|
45
|
+
"Filled missing pgSz/pgMar/cols in existing section properties",
|
|
46
|
+
part: "word/document.xml")
|
|
45
47
|
end
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def reconcile_headers_footers
|
|
51
|
+
parts = package.document&.header_footer_parts
|
|
49
52
|
ignorable = Ooxml::Types::McIgnorable.new(FULL_IGNORABLE)
|
|
50
|
-
set_header_footer_ignorable(package.document&.headers, ignorable)
|
|
51
|
-
set_header_footer_ignorable(package.document&.footers, ignorable)
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
backfill_header_footer_parts(package.document&.footers, rsid, "ftr")
|
|
54
|
+
parts&.each do |part|
|
|
55
|
+
next unless part.content
|
|
56
|
+
next if part.loaded?
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
part.content.mc_ignorable = ignorable
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
unless builder_managed?
|
|
62
|
+
backfill_header_footer_paragraphs(parts, generate_rsid)
|
|
62
63
|
end
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
wire_header_footer_parts
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
private
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def backfill_header_footer_parts(parts, rsid, prefix)
|
|
70
|
+
# Assign rsid/paraId defaults to header/footer paragraphs
|
|
71
|
+
# (legacy, non-builder-managed path only). Seeds preserve the
|
|
72
|
+
# historic per-kind ("hdr:N"/"ftr:N") and per-store ("hfp:N")
|
|
73
|
+
# schemes so generated IDs stay stable across the unified store.
|
|
74
|
+
def backfill_header_footer_paragraphs(parts, rsid)
|
|
76
75
|
return unless parts
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
fresh_counts = Hash.new(0)
|
|
78
|
+
parts.each_with_index do |part, pidx|
|
|
79
|
+
next unless part.content
|
|
80
|
+
|
|
81
|
+
if part.loaded?
|
|
82
|
+
backfill_paragraphs(part.content.paragraphs, rsid, "hfp:#{pidx}")
|
|
83
|
+
else
|
|
84
|
+
abbrev = part.kind == :footer ? "ftr" : "hdr"
|
|
85
|
+
idx = fresh_counts[part.kind]
|
|
86
|
+
fresh_counts[part.kind] += 1
|
|
87
|
+
backfill_paragraphs(part.content.paragraphs, rsid,
|
|
88
|
+
"#{abbrev}:#{idx}")
|
|
89
|
+
end
|
|
80
90
|
end
|
|
81
91
|
end
|
|
82
92
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
def
|
|
93
|
+
# Wire builder-added (fresh) header/footer parts into
|
|
94
|
+
# document_rels and sectPr during reconciliation — the single
|
|
95
|
+
# wiring implementation. Loaded parts keep the relationships
|
|
96
|
+
# and section references they arrived with. rIds come from the
|
|
97
|
+
# allocator, the single rId authority.
|
|
98
|
+
def wire_header_footer_parts
|
|
89
99
|
doc = package.document
|
|
90
100
|
return unless doc&.body
|
|
91
|
-
|
|
101
|
+
|
|
102
|
+
parts = doc.header_footer_parts
|
|
103
|
+
return if parts.nil? || parts.empty?
|
|
92
104
|
|
|
93
105
|
rels = package.document_rels
|
|
94
106
|
return unless rels
|
|
95
107
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
wire_sect_pr_reference(doc, :header, type, r_id)
|
|
100
|
-
end
|
|
108
|
+
wired = false
|
|
109
|
+
parts.each do |part|
|
|
110
|
+
next if part.loaded? || part.target.nil?
|
|
101
111
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
wire_sect_pr_reference(doc,
|
|
112
|
+
r_id = allocator.alloc_rid(target: part.target,
|
|
113
|
+
type: part.rel_type)
|
|
114
|
+
part.r_id = r_id
|
|
115
|
+
wire_sect_pr_reference(doc, part.kind, part.type, r_id) if part.type
|
|
116
|
+
wired = true
|
|
106
117
|
end
|
|
107
118
|
|
|
119
|
+
return unless wired
|
|
120
|
+
|
|
108
121
|
# Clear element_order so header/footer references serialize correctly.
|
|
109
122
|
# Ordered mode only outputs elements in element_order; the template's
|
|
110
123
|
# order may not include newly-added references.
|
|
@@ -112,33 +125,6 @@ module Uniword
|
|
|
112
125
|
sect_pr.element_order = nil if sect_pr&.element_order
|
|
113
126
|
end
|
|
114
127
|
|
|
115
|
-
def wire_parts_to_rels(parts, rels, rel_type, file_prefix, counter)
|
|
116
|
-
return unless parts && !parts.empty?
|
|
117
|
-
|
|
118
|
-
parts.each_key do |type|
|
|
119
|
-
counter += 1
|
|
120
|
-
target = "#{file_prefix}#{counter}.xml"
|
|
121
|
-
r_id = if allocator
|
|
122
|
-
allocator.alloc_rid(target: target, type: rel_type)
|
|
123
|
-
else
|
|
124
|
-
find_or_create_rel(rels, rel_type, target)
|
|
125
|
-
end
|
|
126
|
-
yield type, r_id
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def find_or_create_rel(rels, rel_type, target)
|
|
131
|
-
existing = rels.relationships.find { |r| r.target == target }
|
|
132
|
-
return existing.id if existing
|
|
133
|
-
|
|
134
|
-
r_id = Ooxml::Relationships::PackageRelationships
|
|
135
|
-
.next_available_rid(rels)
|
|
136
|
-
rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
137
|
-
id: r_id, type: rel_type, target: target,
|
|
138
|
-
)
|
|
139
|
-
r_id
|
|
140
|
-
end
|
|
141
|
-
|
|
142
128
|
def wire_sect_pr_reference(doc, kind, type, r_id)
|
|
143
129
|
sect_pr = doc.body.section_properties
|
|
144
130
|
return unless sect_pr
|
|
@@ -169,14 +155,15 @@ module Uniword
|
|
|
169
155
|
doc = package.document
|
|
170
156
|
set_mc_ignorable(doc, prefixes: FULL_IGNORABLE)
|
|
171
157
|
|
|
172
|
-
record_fix(FixCodes::MC_IGNORABLE, "Added mc:Ignorable to document body"
|
|
158
|
+
record_fix(FixCodes::MC_IGNORABLE, "Added mc:Ignorable to document body",
|
|
159
|
+
part: "word/document.xml")
|
|
173
160
|
|
|
174
161
|
body = doc.body
|
|
175
162
|
rsid = generate_rsid
|
|
176
163
|
|
|
177
164
|
body.paragraphs.each_with_index do |para, idx|
|
|
178
165
|
strip_empty_runs(para)
|
|
179
|
-
next if
|
|
166
|
+
next if builder_managed?
|
|
180
167
|
|
|
181
168
|
para.rsid_r ||= rsid
|
|
182
169
|
para.rsid_r_default ||= "00000000"
|
|
@@ -184,7 +171,11 @@ module Uniword
|
|
|
184
171
|
para.text_id ||= "77777777"
|
|
185
172
|
end
|
|
186
173
|
|
|
187
|
-
|
|
174
|
+
unless builder_managed?
|
|
175
|
+
record_fix(FixCodes::PARAGRAPH_BACKFILL,
|
|
176
|
+
"Assigned rsid and paraId to paragraphs",
|
|
177
|
+
part: "word/document.xml")
|
|
178
|
+
end
|
|
188
179
|
|
|
189
180
|
sect_pr = body.section_properties
|
|
190
181
|
return unless sect_pr
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Docx
|
|
5
|
+
class Reconciler
|
|
6
|
+
# A single repair applied by the Reconciler during save.
|
|
7
|
+
#
|
|
8
|
+
# Immutable value object exposed as the reconciliation report via
|
|
9
|
+
# `Package#applied_fixes` after a save. The Reconciler stays the only
|
|
10
|
+
# mutating pass; this report is a by-product value, not a global.
|
|
11
|
+
#
|
|
12
|
+
# @example Inspect fixes after save
|
|
13
|
+
# package.to_file("out.docx")
|
|
14
|
+
# package.applied_fixes.each do |fix|
|
|
15
|
+
# puts "#{fix.code} [#{fix.part}] #{fix.message}"
|
|
16
|
+
# end
|
|
17
|
+
class Fix
|
|
18
|
+
# @return [String] Fix code (see Reconciler::FixCodes)
|
|
19
|
+
attr_reader :code
|
|
20
|
+
|
|
21
|
+
# @return [String] Human-readable description of the repair
|
|
22
|
+
attr_reader :message
|
|
23
|
+
|
|
24
|
+
# @return [String, nil] Package part the repair applies to
|
|
25
|
+
attr_reader :part
|
|
26
|
+
|
|
27
|
+
# Create a fix record.
|
|
28
|
+
#
|
|
29
|
+
# @param code [String] Fix code (a FixCodes constant value)
|
|
30
|
+
# @param message [String] Human-readable description of the repair
|
|
31
|
+
# @param part [String, nil] Package part the repair applies to
|
|
32
|
+
def initialize(code:, message:, part: nil)
|
|
33
|
+
@code = code
|
|
34
|
+
@message = message
|
|
35
|
+
@part = part
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [String] Compact single-line representation
|
|
39
|
+
def to_s
|
|
40
|
+
location = part ? " [#{part}]" : ""
|
|
41
|
+
"#{code}#{location}: #{message}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -7,14 +7,15 @@ module Uniword
|
|
|
7
7
|
#
|
|
8
8
|
# Every `record_fix` call site references a constant from this module
|
|
9
9
|
# instead of a bare string literal. Adding a new fix category means
|
|
10
|
-
# adding one constant here
|
|
10
|
+
# adding one constant here with the next free code.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
12
|
+
# One code per concern: each constant has a unique, self-describing
|
|
13
|
+
# wire value. Codes "R1".."R16" are frozen — external validation rules
|
|
14
|
+
# (lib/uniword/validation/rules) and audit-log consumers pattern-match
|
|
15
|
+
# on them, so the original concerns keep their historic codes. Codes
|
|
16
|
+
# "R17" and up were introduced when the overloaded "R10" (and the
|
|
17
|
+
# remaining shared codes "R4"/"R11"/"R12"/"R13") were split into
|
|
18
|
+
# per-concern values.
|
|
18
19
|
module FixCodes
|
|
19
20
|
# Settings / package structure
|
|
20
21
|
MC_IGNORABLE = "R1"
|
|
@@ -29,17 +30,12 @@ module Uniword
|
|
|
29
30
|
CONTENT_TYPES_ASSEMBLED = "R7"
|
|
30
31
|
APP_PROPERTIES_ENSURED = "R8"
|
|
31
32
|
|
|
32
|
-
# Notes — pair creation
|
|
33
|
+
# Notes — pair creation and definition integrity
|
|
33
34
|
NOTE_PAIR_CREATED = "R9"
|
|
34
35
|
NOTE_DEFINITION_CREATED = "R10"
|
|
35
36
|
NOTE_INVALID_TYPE_STRIPPED = "R15"
|
|
36
37
|
NOTE_DUPLICATE_ID_REMOVED = "R16"
|
|
37
38
|
|
|
38
|
-
# Notes — referential integrity warnings and removals
|
|
39
|
-
DANGLING_NOTE_REFERENCE_WARNING = "R9"
|
|
40
|
-
DANGLING_NOTE_REFERENCE_REMOVED = "R10"
|
|
41
|
-
DANGLING_HYPERLINK_WARNING = "R9"
|
|
42
|
-
|
|
43
39
|
# Body
|
|
44
40
|
SECTION_PROPERTIES_DEFAULTED = "R11"
|
|
45
41
|
PARAGRAPH_BACKFILL = "R12"
|
|
@@ -48,26 +44,37 @@ module Uniword
|
|
|
48
44
|
FONT_TABLE_CREATED = "R13"
|
|
49
45
|
CORE_PROPERTIES_REBUILT = "R14"
|
|
50
46
|
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
# Referential integrity — dangling-reference repairs (split from
|
|
48
|
+
# the overloaded R10/R4/R11/R12 codes; one code per concern)
|
|
49
|
+
DANGLING_NUMBERING_REMOVED = "R17"
|
|
50
|
+
DANGLING_NOTE_REFERENCE_REMOVED = "R18"
|
|
51
|
+
DANGLING_STYLE_REFERENCE_REMOVED = "R19"
|
|
52
|
+
DANGLING_BASED_ON_REMOVED = "R20"
|
|
53
|
+
DANGLING_HYPERLINK_REMOVED = "R21"
|
|
54
|
+
DANGLING_HEADER_FOOTER_REMOVED = "R22"
|
|
55
|
+
DANGLING_DRAWING_REMOVED = "R23"
|
|
56
|
+
DANGLING_RELATIONSHIP_TARGET_REMOVED = "R32"
|
|
57
|
+
|
|
58
|
+
# Referential integrity — literal hyperlink targets promoted to
|
|
59
|
+
# proper External relationships (repairs Builder.hyperlink output)
|
|
60
|
+
HYPERLINK_RELATIONSHIP_CREATED = "R31"
|
|
61
|
+
|
|
62
|
+
# Styles
|
|
63
|
+
STYLE_DEFAULTS_ADDED = "R24"
|
|
64
|
+
SEMI_HIDDEN_ADDED = "R25"
|
|
54
65
|
|
|
55
|
-
# Tables
|
|
56
|
-
TABLE_STRUCTURE_RECONCILED = "
|
|
57
|
-
TABLE_CELL_PR_REORDERED = "
|
|
58
|
-
TABLE_CELL_DEFAULTS = "
|
|
59
|
-
TABLE_ROW_GRID_AFTER = "
|
|
66
|
+
# Tables
|
|
67
|
+
TABLE_STRUCTURE_RECONCILED = "R26"
|
|
68
|
+
TABLE_CELL_PR_REORDERED = "R27"
|
|
69
|
+
TABLE_CELL_DEFAULTS = "R28"
|
|
70
|
+
TABLE_ROW_GRID_AFTER = "R29"
|
|
60
71
|
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
DANGLING_BASED_ON_REMOVED = "R10"
|
|
64
|
-
DANGLING_NUMBERING_REMOVED = "R4"
|
|
65
|
-
DANGLING_HYPERLINK_REMOVED = "R10"
|
|
66
|
-
DANGLING_HEADER_FOOTER_REMOVED = "R11"
|
|
67
|
-
DANGLING_DRAWING_REMOVED = "R12"
|
|
72
|
+
# Run cleanup
|
|
73
|
+
EMPTY_RUNS_STRIPPED = "R30"
|
|
68
74
|
|
|
69
|
-
#
|
|
70
|
-
|
|
75
|
+
# Tables — gridCol widths defaulted to equal shares of the
|
|
76
|
+
# section content width (Word's fallback behavior)
|
|
77
|
+
TABLE_GRID_COL_WIDTHS_DEFAULTED = "R33"
|
|
71
78
|
end
|
|
72
79
|
end
|
|
73
80
|
end
|
|
@@ -85,7 +85,7 @@ module Uniword
|
|
|
85
85
|
walk_body_paragraphs(package.document.body, &block)
|
|
86
86
|
|
|
87
87
|
(package.document&.header_footer_parts || []).each do |part|
|
|
88
|
-
(part
|
|
88
|
+
(part.content&.paragraphs || []).each(&block)
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
91
|
|
|
@@ -295,6 +295,14 @@ module Uniword
|
|
|
295
295
|
end
|
|
296
296
|
end
|
|
297
297
|
|
|
298
|
+
# Package part path of the notes collection for the note type.
|
|
299
|
+
def notes_part_for(type)
|
|
300
|
+
case type
|
|
301
|
+
when :footnote then "word/footnotes.xml"
|
|
302
|
+
when :endnote then "word/endnotes.xml"
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
298
306
|
def note_entries_for(notes, type)
|
|
299
307
|
return [] unless notes
|
|
300
308
|
|
|
@@ -38,7 +38,8 @@ module Uniword
|
|
|
38
38
|
)
|
|
39
39
|
end
|
|
40
40
|
record_fix(FixCodes::NOTE_DEFINITION_CREATED,
|
|
41
|
-
"Created missing #{type}note definitions for ids=#{missing.join(', ')}"
|
|
41
|
+
"Created missing #{type}note definitions for ids=#{missing.join(', ')}",
|
|
42
|
+
part: notes_part_for(type))
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
|
|
@@ -70,11 +71,13 @@ module Uniword
|
|
|
70
71
|
create_message:, pr_message:)
|
|
71
72
|
if has_pr && !notes
|
|
72
73
|
notes_setter.call(minimal_notes(type))
|
|
73
|
-
record_fix(FixCodes::NOTE_PAIR_CREATED, create_message
|
|
74
|
+
record_fix(FixCodes::NOTE_PAIR_CREATED, create_message,
|
|
75
|
+
part: notes_part_for(type))
|
|
74
76
|
elsif notes && !has_pr
|
|
75
77
|
package.settings ||= Wordprocessingml::Settings.new
|
|
76
78
|
assign_note_pr(package.settings, type)
|
|
77
|
-
record_fix(FixCodes::NOTE_PAIR_CREATED, pr_message
|
|
79
|
+
record_fix(FixCodes::NOTE_PAIR_CREATED, pr_message,
|
|
80
|
+
part: "word/settings.xml")
|
|
78
81
|
end
|
|
79
82
|
|
|
80
83
|
current = notes_collection_for(type)
|
|
@@ -92,7 +95,7 @@ module Uniword
|
|
|
92
95
|
strip_empty_runs_from_notes(entries)
|
|
93
96
|
reorder_notes_by_reference(entries, type)
|
|
94
97
|
|
|
95
|
-
unless
|
|
98
|
+
unless builder_managed?
|
|
96
99
|
prefix = type == :footnote ? "fn" : "en"
|
|
97
100
|
|
|
98
101
|
entries.each_with_index do |entry, eidx|
|
|
@@ -182,7 +185,8 @@ module Uniword
|
|
|
182
185
|
return if stripped.empty?
|
|
183
186
|
|
|
184
187
|
record_fix(FixCodes::NOTE_INVALID_TYPE_STRIPPED,
|
|
185
|
-
"Stripped invalid w:type from #{type}note ids=#{stripped.join(', ')}"
|
|
188
|
+
"Stripped invalid w:type from #{type}note ids=#{stripped.join(', ')}",
|
|
189
|
+
part: notes_part_for(type))
|
|
186
190
|
end
|
|
187
191
|
|
|
188
192
|
def deduplicate_note_ids(entries, type)
|
|
@@ -200,7 +204,8 @@ module Uniword
|
|
|
200
204
|
return if dup_ids.empty?
|
|
201
205
|
|
|
202
206
|
record_fix(FixCodes::NOTE_DUPLICATE_ID_REMOVED,
|
|
203
|
-
"Removed duplicate #{type}note ids=#{dup_ids.join(', ')}"
|
|
207
|
+
"Removed duplicate #{type}note ids=#{dup_ids.join(', ')}",
|
|
208
|
+
part: notes_part_for(type))
|
|
204
209
|
end
|
|
205
210
|
|
|
206
211
|
def reorder_notes_by_reference(entries, type)
|
|
@@ -227,7 +232,9 @@ module Uniword
|
|
|
227
232
|
structural.each { |e| entries << e }
|
|
228
233
|
reordered.each { |e| entries << e }
|
|
229
234
|
|
|
230
|
-
record_fix(FixCodes::NOTE_PAIR_CREATED,
|
|
235
|
+
record_fix(FixCodes::NOTE_PAIR_CREATED,
|
|
236
|
+
"Reordered #{type}notes by first reference in document body",
|
|
237
|
+
part: notes_part_for(type))
|
|
231
238
|
end
|
|
232
239
|
|
|
233
240
|
def renumber_notes(entries, type)
|
|
@@ -255,7 +262,9 @@ module Uniword
|
|
|
255
262
|
end
|
|
256
263
|
end
|
|
257
264
|
|
|
258
|
-
record_fix(FixCodes::NOTE_PAIR_CREATED,
|
|
265
|
+
record_fix(FixCodes::NOTE_PAIR_CREATED,
|
|
266
|
+
"Renumbered #{type}note IDs sequentially (#{id_map.size} changed)",
|
|
267
|
+
part: notes_part_for(type))
|
|
259
268
|
end
|
|
260
269
|
|
|
261
270
|
def collect_note_reference_order(body, type)
|