uniword 1.0.11 → 1.2.4
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/config/font_metadata.yml +56 -0
- data/lib/uniword/accessibility/rules/descriptive_headings_rule.rb +1 -1
- data/lib/uniword/assembly/toc.rb +4 -14
- data/lib/uniword/assembly/variable_substitutor.rb +3 -4
- data/lib/uniword/batch/stages/normalize_styles_stage.rb +4 -2
- data/lib/uniword/builder/base_builder.rb +25 -0
- data/lib/uniword/builder/chart_builder.rb +33 -14
- data/lib/uniword/builder/deterministic_id.rb +17 -0
- data/lib/uniword/builder/document_builder.rb +129 -34
- data/lib/uniword/builder/footnote_builder.rb +22 -23
- data/lib/uniword/builder/header_footer_builder.rb +10 -10
- data/lib/uniword/builder/image_builder.rb +24 -16
- data/lib/uniword/builder/list_builder.rb +2 -1
- data/lib/uniword/builder/paragraph_builder.rb +95 -20
- data/lib/uniword/builder/run_utils.rb +97 -0
- data/lib/uniword/builder/section_builder.rb +40 -67
- data/lib/uniword/builder/table_builder.rb +32 -13
- data/lib/uniword/builder/table_cell_builder.rb +19 -0
- data/lib/uniword/builder.rb +27 -19
- data/lib/uniword/cli/diff_cli.rb +0 -1
- data/lib/uniword/cli/generate_cli.rb +0 -1
- data/lib/uniword/cli/headers_cli.rb +0 -1
- data/lib/uniword/cli/images_cli.rb +0 -1
- data/lib/uniword/cli/main.rb +6 -7
- data/lib/uniword/cli/protect_cli.rb +0 -1
- data/lib/uniword/cli/resources_cli.rb +0 -1
- data/lib/uniword/cli/review_cli.rb +0 -1
- data/lib/uniword/cli/spellcheck_cli.rb +0 -1
- data/lib/uniword/cli/styleset_cli.rb +0 -1
- data/lib/uniword/cli/template_cli.rb +0 -1
- data/lib/uniword/cli/theme_cli.rb +0 -1
- data/lib/uniword/cli/toc_cli.rb +0 -1
- data/lib/uniword/cli/watermark_cli.rb +0 -1
- data/lib/uniword/cli.rb +6 -17
- data/lib/uniword/diff/document_differ.rb +23 -5
- data/lib/uniword/diff/package_differ.rb +0 -1
- data/lib/uniword/diff.rb +4 -0
- data/lib/uniword/document_factory.rb +16 -25
- data/lib/uniword/docx/document_statistics.rb +42 -54
- data/lib/uniword/docx/id_allocator.rb +143 -0
- data/lib/uniword/docx/package.rb +42 -6
- data/lib/uniword/docx/package_defaults.rb +15 -25
- data/lib/uniword/docx/package_serialization.rb +131 -127
- data/lib/uniword/docx/reconciler/body.rb +197 -0
- data/lib/uniword/docx/reconciler/fix_codes.rb +74 -0
- data/lib/uniword/docx/reconciler/helpers.rb +337 -0
- data/lib/uniword/docx/reconciler/notes.rb +301 -0
- data/lib/uniword/docx/reconciler/package_structure.rb +322 -0
- data/lib/uniword/docx/reconciler/parts.rb +484 -0
- data/lib/uniword/docx/reconciler/referential_integrity.rb +449 -0
- data/lib/uniword/docx/reconciler/tables.rb +162 -0
- data/lib/uniword/docx/reconciler/theme.rb +141 -0
- data/lib/uniword/docx/reconciler.rb +44 -1097
- data/lib/uniword/docx.rb +3 -0
- data/lib/uniword/drawingml/color_scheme.rb +38 -5
- data/lib/uniword/hyperlink.rb +27 -26
- data/lib/uniword/infrastructure/zip_packager.rb +41 -38
- data/lib/uniword/lazy_loader.rb +36 -62
- data/lib/uniword/math.rb +3 -86
- data/lib/uniword/mhtml/document.rb +1 -3
- data/lib/uniword/model_attribute_access.rb +27 -0
- data/lib/uniword/ooxml/namespaces.rb +62 -2
- data/lib/uniword/ooxml/relationships/package_relationships.rb +7 -0
- data/lib/uniword/presentationml/slide_id.rb +0 -1
- data/lib/uniword/presentationml/slide_master_id.rb +0 -1
- data/lib/uniword/properties/bold.rb +0 -2
- data/lib/uniword/properties/boolean_formatting.rb +0 -2
- data/lib/uniword/properties/borders.rb +1 -1
- data/lib/uniword/properties/italic.rb +0 -2
- data/lib/uniword/properties/numbering_properties.rb +1 -1
- data/lib/uniword/properties/style_reference.rb +13 -0
- data/lib/uniword/quality/rules/heading_hierarchy_rule.rb +1 -1
- data/lib/uniword/quality/rules/style_consistency_rule.rb +1 -1
- data/lib/uniword/review/review_manager.rb +0 -2
- data/lib/uniword/template/helpers/loop_helper.rb +2 -2
- data/lib/uniword/template/variable_resolver.rb +14 -1
- data/lib/uniword/theme/theme_xml_parser.rb +1 -1
- data/lib/uniword/toc/toc_generator.rb +3 -14
- data/lib/uniword/transformation/mhtml_element_renderer.rb +94 -37
- data/lib/uniword/transformation/mhtml_metadata_builder.rb +1 -1
- data/lib/uniword/transformation/ooxml_to_html_converter.rb +3 -5
- data/lib/uniword/validation/checkers/internal_link_checker.rb +2 -3
- data/lib/uniword/version.rb +1 -1
- data/lib/uniword/visitor/text_extractor.rb +2 -1
- data/lib/uniword/watermark/manager.rb +2 -2
- data/lib/uniword/wordprocessingml/allow_png.rb +14 -0
- data/lib/uniword/wordprocessingml/body.rb +51 -34
- data/lib/uniword/wordprocessingml/body_div.rb +17 -0
- data/lib/uniword/wordprocessingml/div_border.rb +23 -0
- data/lib/uniword/wordprocessingml/div_borders.rb +24 -0
- data/lib/uniword/wordprocessingml/divs_child.rb +17 -0
- data/lib/uniword/wordprocessingml/document_root.rb +4 -3
- data/lib/uniword/wordprocessingml/endnote.rb +1 -1
- data/lib/uniword/wordprocessingml/endnotes.rb +4 -0
- data/lib/uniword/wordprocessingml/footer.rb +4 -0
- data/lib/uniword/wordprocessingml/footer_reference.rb +0 -1
- data/lib/uniword/wordprocessingml/footnote.rb +1 -1
- data/lib/uniword/wordprocessingml/footnotes.rb +4 -0
- data/lib/uniword/wordprocessingml/header.rb +4 -0
- data/lib/uniword/wordprocessingml/header_reference.rb +0 -1
- data/lib/uniword/wordprocessingml/hyperlink.rb +2 -2
- data/lib/uniword/wordprocessingml/instr_text.rb +20 -5
- data/lib/uniword/wordprocessingml/level.rb +2 -0
- data/lib/uniword/wordprocessingml/mar_bottom.rb +17 -0
- data/lib/uniword/wordprocessingml/mar_left.rb +17 -0
- data/lib/uniword/wordprocessingml/mar_right.rb +17 -0
- data/lib/uniword/wordprocessingml/mar_top.rb +17 -0
- data/lib/uniword/wordprocessingml/num.rb +4 -0
- data/lib/uniword/wordprocessingml/numbering.rb +1 -22
- data/lib/uniword/wordprocessingml/optimize_for_browser.rb +14 -0
- data/lib/uniword/wordprocessingml/page_defaults.rb +39 -0
- data/lib/uniword/wordprocessingml/paragraph.rb +16 -13
- data/lib/uniword/wordprocessingml/paragraph_properties.rb +12 -8
- data/lib/uniword/wordprocessingml/run.rb +32 -36
- data/lib/uniword/wordprocessingml/run_properties/merging.rb +45 -0
- data/lib/uniword/wordprocessingml/run_properties.rb +8 -5
- data/lib/uniword/wordprocessingml/section_properties.rb +3 -3
- data/lib/uniword/wordprocessingml/separator_char.rb +21 -0
- data/lib/uniword/wordprocessingml/settings.rb +19 -17
- data/lib/uniword/wordprocessingml/styles/style_definition.rb +1 -1
- data/lib/uniword/wordprocessingml/styles/style_library.rb +16 -0
- data/lib/uniword/wordprocessingml/table_defaults.rb +44 -0
- data/lib/uniword/wordprocessingml/text.rb +8 -2
- data/lib/uniword/wordprocessingml/web_div.rb +41 -0
- data/lib/uniword/wordprocessingml/web_divs.rb +18 -0
- data/lib/uniword/wordprocessingml/web_encoding.rb +17 -0
- data/lib/uniword/wordprocessingml/web_settings.rb +5 -21
- data/lib/uniword/wordprocessingml/zoom.rb +2 -0
- data/lib/uniword/wordprocessingml.rb +23 -0
- data/lib/uniword.rb +27 -0
- metadata +46 -70
- data/lib/uniword/math/accent.rb +0 -25
- data/lib/uniword/math/accent_properties.rb +0 -25
- data/lib/uniword/math/argument_properties.rb +0 -22
- data/lib/uniword/math/bar.rb +0 -25
- data/lib/uniword/math/bar_properties.rb +0 -25
- data/lib/uniword/math/begin_char.rb +0 -22
- data/lib/uniword/math/border_box.rb +0 -25
- data/lib/uniword/math/border_box_properties.rb +0 -39
- data/lib/uniword/math/box.rb +0 -25
- data/lib/uniword/math/box_properties.rb +0 -33
- data/lib/uniword/math/char.rb +0 -22
- data/lib/uniword/math/control_properties.rb +0 -27
- data/lib/uniword/math/degree.rb +0 -26
- data/lib/uniword/math/delimiter.rb +0 -25
- data/lib/uniword/math/delimiter_properties.rb +0 -33
- data/lib/uniword/math/denominator.rb +0 -26
- data/lib/uniword/math/element.rb +0 -61
- data/lib/uniword/math/end_char.rb +0 -22
- data/lib/uniword/math/equation_array.rb +0 -25
- data/lib/uniword/math/equation_array_properties.rb +0 -33
- data/lib/uniword/math/fraction.rb +0 -27
- data/lib/uniword/math/fraction_properties.rb +0 -25
- data/lib/uniword/math/function.rb +0 -27
- data/lib/uniword/math/function_name.rb +0 -26
- data/lib/uniword/math/function_properties.rb +0 -23
- data/lib/uniword/math/group_char.rb +0 -25
- data/lib/uniword/math/group_char_properties.rb +0 -29
- data/lib/uniword/math/lim.rb +0 -26
- data/lib/uniword/math/lower_limit.rb +0 -27
- data/lib/uniword/math/lower_limit_properties.rb +0 -23
- data/lib/uniword/math/math_break.rb +0 -22
- data/lib/uniword/math/math_font.rb +0 -22
- data/lib/uniword/math/math_properties.rb +0 -53
- data/lib/uniword/math/math_run.rb +0 -42
- data/lib/uniword/math/math_run_properties.rb +0 -37
- data/lib/uniword/math/math_simple_int_val.rb +0 -24
- data/lib/uniword/math/math_simple_val.rb +0 -24
- data/lib/uniword/math/math_style.rb +0 -22
- data/lib/uniword/math/math_text.rb +0 -23
- data/lib/uniword/math/matrix.rb +0 -25
- data/lib/uniword/math/matrix_column.rb +0 -23
- data/lib/uniword/math/matrix_column_properties.rb +0 -24
- data/lib/uniword/math/matrix_columns.rb +0 -23
- data/lib/uniword/math/matrix_properties.rb +0 -37
- data/lib/uniword/math/matrix_row.rb +0 -23
- data/lib/uniword/math/nary.rb +0 -29
- data/lib/uniword/math/nary_properties.rb +0 -33
- data/lib/uniword/math/numerator.rb +0 -26
- data/lib/uniword/math/o_math.rb +0 -65
- data/lib/uniword/math/o_math_para.rb +0 -25
- data/lib/uniword/math/o_math_para_properties.rb +0 -22
- data/lib/uniword/math/phantom.rb +0 -25
- data/lib/uniword/math/phantom_properties.rb +0 -33
- data/lib/uniword/math/pre_sub_superscript.rb +0 -29
- data/lib/uniword/math/pre_sub_superscript_properties.rb +0 -23
- data/lib/uniword/math/radical.rb +0 -27
- data/lib/uniword/math/radical_properties.rb +0 -25
- data/lib/uniword/math/separator_char.rb +0 -22
- data/lib/uniword/math/sub.rb +0 -26
- data/lib/uniword/math/sub_superscript.rb +0 -29
- data/lib/uniword/math/sub_superscript_properties.rb +0 -25
- data/lib/uniword/math/subscript.rb +0 -27
- data/lib/uniword/math/subscript_properties.rb +0 -23
- data/lib/uniword/math/sup.rb +0 -26
- data/lib/uniword/math/superscript.rb +0 -27
- data/lib/uniword/math/superscript_properties.rb +0 -23
- data/lib/uniword/math/upper_limit.rb +0 -27
- data/lib/uniword/math/upper_limit_properties.rb +0 -23
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Docx
|
|
5
|
+
class Reconciler
|
|
6
|
+
# Cross-part referential integrity enforcement.
|
|
7
|
+
#
|
|
8
|
+
# Verifies and repairs references between document.xml and other
|
|
9
|
+
# package parts. Each method follows the principle of least surprise:
|
|
10
|
+
# - For removable inline refs (notes): strip dangling silently
|
|
11
|
+
# - For structural refs (styles, numbering): warn and strip
|
|
12
|
+
# - For relationship refs (rId): reconcile against actual parts
|
|
13
|
+
# - For uniqueness constraints: deduplicate with deterministic resolution
|
|
14
|
+
module ReferentialIntegrity
|
|
15
|
+
def reconcile_referential_integrity
|
|
16
|
+
return unless package.document&.body
|
|
17
|
+
|
|
18
|
+
if allocator
|
|
19
|
+
validate_builder_references
|
|
20
|
+
else
|
|
21
|
+
repair_all_references
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
reconcile_style_references
|
|
25
|
+
reconcile_style_inheritance
|
|
26
|
+
reconcile_numbering_body_references
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Allocator present: builders produce correct output.
|
|
32
|
+
# Validate builder-constrained references — warn on issues
|
|
33
|
+
# but don't silently strip (indicates a builder bug).
|
|
34
|
+
def validate_builder_references
|
|
35
|
+
validate_note_references(:footnote)
|
|
36
|
+
validate_note_references(:endnote)
|
|
37
|
+
reconcile_image_references
|
|
38
|
+
validate_hyperlink_references
|
|
39
|
+
ensure_para_id_uniqueness
|
|
40
|
+
ensure_rid_uniqueness
|
|
41
|
+
reconcile_sect_pr_references
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# No allocator: legacy repair path for template-loaded content.
|
|
45
|
+
def repair_all_references
|
|
46
|
+
reconcile_note_body_references(:footnote)
|
|
47
|
+
reconcile_note_body_references(:endnote)
|
|
48
|
+
reconcile_sect_pr_references
|
|
49
|
+
reconcile_image_references
|
|
50
|
+
reconcile_hyperlink_references
|
|
51
|
+
ensure_para_id_uniqueness
|
|
52
|
+
ensure_rid_uniqueness
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Validate that note references in the body match existing notes.
|
|
56
|
+
# Logs warnings but does NOT strip — builder should produce correct refs.
|
|
57
|
+
def validate_note_references(type)
|
|
58
|
+
notes = notes_collection_for(type)
|
|
59
|
+
return unless notes
|
|
60
|
+
|
|
61
|
+
entries = note_entries_for(notes, type)
|
|
62
|
+
defined_ids = entries
|
|
63
|
+
.reject { |e| VALID_NOTE_TYPES.include?(e.type) }
|
|
64
|
+
.filter_map(&:id).to_set
|
|
65
|
+
|
|
66
|
+
dangling = 0
|
|
67
|
+
walk_body_paragraphs(package.document.body) do |para|
|
|
68
|
+
para.runs.each do |run|
|
|
69
|
+
ref = note_reference_from_run(run, type)
|
|
70
|
+
next unless ref&.id
|
|
71
|
+
next if defined_ids.include?(ref.id)
|
|
72
|
+
|
|
73
|
+
dangling += 1
|
|
74
|
+
Uniword.logger&.warn do
|
|
75
|
+
"Dangling #{type}note reference id=#{ref.id} in body — " \
|
|
76
|
+
"builder produced invalid reference"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
return unless dangling.positive?
|
|
82
|
+
|
|
83
|
+
record_fix(FixCodes::DANGLING_NOTE_REFERENCE_WARNING,
|
|
84
|
+
"WARNING: Found #{dangling} dangling #{type}note reference(s) " \
|
|
85
|
+
"in body (allocator path — builder bug)")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Validate that hyperlink references match existing rels.
|
|
89
|
+
# Logs warnings but does NOT strip — builder should register hyperlinks.
|
|
90
|
+
def validate_hyperlink_references
|
|
91
|
+
rels = package.document_rels
|
|
92
|
+
return unless rels
|
|
93
|
+
|
|
94
|
+
valid_rids = rels.relationships.to_set(&:id)
|
|
95
|
+
return if valid_rids.empty?
|
|
96
|
+
|
|
97
|
+
dangling = 0
|
|
98
|
+
walk_body_paragraphs(package.document.body) do |para|
|
|
99
|
+
(para.hyperlinks || []).each do |hl|
|
|
100
|
+
next if hl.anchor
|
|
101
|
+
next unless hl.id
|
|
102
|
+
next unless hl.id.match?(/\ArId\d+\z/i)
|
|
103
|
+
next if valid_rids.include?(hl.id)
|
|
104
|
+
|
|
105
|
+
dangling += 1
|
|
106
|
+
Uniword.logger&.warn do
|
|
107
|
+
"Dangling hyperlink r:id=#{hl.id} in body — " \
|
|
108
|
+
"builder failed to register hyperlink with allocator"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
return unless dangling.positive?
|
|
114
|
+
|
|
115
|
+
record_fix(FixCodes::DANGLING_HYPERLINK_WARNING,
|
|
116
|
+
"WARNING: Found #{dangling} dangling hyperlink reference(s) " \
|
|
117
|
+
"in body (allocator path — builder bug)")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# -- Note references (body → footnotes.xml / endnotes.xml) --
|
|
121
|
+
|
|
122
|
+
def reconcile_note_body_references(type)
|
|
123
|
+
notes = notes_collection_for(type)
|
|
124
|
+
return unless notes
|
|
125
|
+
|
|
126
|
+
entries = note_entries_for(notes, type)
|
|
127
|
+
defined_ids = entries
|
|
128
|
+
.reject { |e| VALID_NOTE_TYPES.include?(e.type) }
|
|
129
|
+
.filter_map(&:id).to_set
|
|
130
|
+
|
|
131
|
+
removed = 0
|
|
132
|
+
|
|
133
|
+
walk_body_paragraphs(package.document.body) do |para|
|
|
134
|
+
para.runs.reject! do |run|
|
|
135
|
+
ref = note_reference_from_run(run, type)
|
|
136
|
+
next false unless ref&.id
|
|
137
|
+
next false if defined_ids.include?(ref.id)
|
|
138
|
+
|
|
139
|
+
removed += 1
|
|
140
|
+
true
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
return unless removed.positive?
|
|
145
|
+
|
|
146
|
+
record_fix(FixCodes::DANGLING_NOTE_REFERENCE_REMOVED,
|
|
147
|
+
"Removed #{removed} dangling #{type}note reference(s) in body")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# -- Section property references (sectPr → document.xml.rels) --
|
|
151
|
+
|
|
152
|
+
def reconcile_sect_pr_references
|
|
153
|
+
sect_pr = package.document&.body&.section_properties
|
|
154
|
+
return unless sect_pr
|
|
155
|
+
|
|
156
|
+
valid_rids = collect_valid_header_footer_rids
|
|
157
|
+
return if valid_rids.empty?
|
|
158
|
+
|
|
159
|
+
removed = 0
|
|
160
|
+
[sect_pr.header_references, sect_pr.footer_references].each do |refs|
|
|
161
|
+
next unless refs
|
|
162
|
+
|
|
163
|
+
removed += refs.count { |r| r.r_id && !valid_rids.include?(r.r_id) }
|
|
164
|
+
refs.reject! { |r| r.r_id && !valid_rids.include?(r.r_id) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
return unless removed.positive?
|
|
168
|
+
|
|
169
|
+
record_fix(FixCodes::DANGLING_HEADER_FOOTER_REMOVED,
|
|
170
|
+
"Removed #{removed} dangling header/footer reference(s) from sectPr")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def collect_valid_header_footer_rids
|
|
174
|
+
rids = Set.new
|
|
175
|
+
|
|
176
|
+
(package.document&.header_footer_parts || []).each do |part|
|
|
177
|
+
rids << part[:r_id] if part[:r_id]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
collect_rels_by_type(rids,
|
|
181
|
+
"officeDocument/2006/relationships/header",
|
|
182
|
+
"officeDocument/2006/relationships/footer")
|
|
183
|
+
|
|
184
|
+
rids
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# -- Style references (body → styles.xml) --
|
|
188
|
+
|
|
189
|
+
def reconcile_style_references
|
|
190
|
+
styles = package.styles
|
|
191
|
+
return unless styles
|
|
192
|
+
|
|
193
|
+
defined_ids = styles.styles.to_set(&:id)
|
|
194
|
+
style_names = styles.styles.to_set { |s| s.name&.val }.compact
|
|
195
|
+
removed = 0
|
|
196
|
+
|
|
197
|
+
walk_body_paragraphs(package.document.body) do |para|
|
|
198
|
+
removed += reconcile_paragraph_style(para, defined_ids, style_names)
|
|
199
|
+
removed += reconcile_run_styles(para, defined_ids, style_names)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
walk_body_tables(package.document.body) do |tbl|
|
|
203
|
+
tbl_ref = tbl.properties&.style
|
|
204
|
+
next unless tbl_ref&.val
|
|
205
|
+
next if defined_ids.include?(tbl_ref.val)
|
|
206
|
+
next if style_names.include?(tbl_ref.val)
|
|
207
|
+
|
|
208
|
+
tbl.properties.style = nil
|
|
209
|
+
removed += 1
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
return unless removed.positive?
|
|
213
|
+
|
|
214
|
+
record_fix(FixCodes::DANGLING_STYLE_REFERENCE_REMOVED,
|
|
215
|
+
"Removed #{removed} dangling style reference(s) from body")
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# -- Style inheritance (styles.xml self-references) --
|
|
219
|
+
|
|
220
|
+
def reconcile_style_inheritance
|
|
221
|
+
styles = package.styles
|
|
222
|
+
return unless styles
|
|
223
|
+
|
|
224
|
+
defined_ids = styles.styles.to_set(&:id)
|
|
225
|
+
stripped = 0
|
|
226
|
+
|
|
227
|
+
styles.styles.each do |style|
|
|
228
|
+
based_on = style.basedOn
|
|
229
|
+
if based_on&.val && !defined_ids.include?(based_on.val)
|
|
230
|
+
style.basedOn = nil
|
|
231
|
+
stripped += 1
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
link = style.link
|
|
235
|
+
if link&.val && !defined_ids.include?(link.val)
|
|
236
|
+
style.link = nil
|
|
237
|
+
stripped += 1
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
return unless stripped.positive?
|
|
242
|
+
|
|
243
|
+
record_fix(FixCodes::DANGLING_BASED_ON_REMOVED,
|
|
244
|
+
"Removed #{stripped} dangling basedOn/link reference(s) in styles")
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# -- Numbering references (body → numbering.xml) --
|
|
248
|
+
|
|
249
|
+
def reconcile_numbering_body_references
|
|
250
|
+
numbering = package.numbering
|
|
251
|
+
return unless numbering
|
|
252
|
+
|
|
253
|
+
defined_num_ids = numbering.instances.to_set(&:num_id)
|
|
254
|
+
return if defined_num_ids.empty?
|
|
255
|
+
|
|
256
|
+
removed = 0
|
|
257
|
+
|
|
258
|
+
walk_body_paragraphs(package.document.body) do |para|
|
|
259
|
+
num_pr = para.properties&.numbering_properties
|
|
260
|
+
next unless num_pr
|
|
261
|
+
|
|
262
|
+
num_id = numbering_num_id_value(num_pr)
|
|
263
|
+
next unless num_id
|
|
264
|
+
next if num_id.zero?
|
|
265
|
+
next if defined_num_ids.include?(num_id)
|
|
266
|
+
|
|
267
|
+
para.properties.numbering_properties = nil
|
|
268
|
+
removed += 1
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
return unless removed.positive?
|
|
272
|
+
|
|
273
|
+
record_fix(FixCodes::DANGLING_NUMBERING_REMOVED,
|
|
274
|
+
"Removed #{removed} dangling numbering reference(s) from body")
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# -- Image references (blip/@r:embed → document.xml.rels) --
|
|
278
|
+
|
|
279
|
+
def reconcile_image_references
|
|
280
|
+
rels = package.document_rels
|
|
281
|
+
return unless rels
|
|
282
|
+
|
|
283
|
+
valid_rids = rels.relationships.to_set(&:id)
|
|
284
|
+
return if valid_rids.empty?
|
|
285
|
+
|
|
286
|
+
dangling = 0
|
|
287
|
+
|
|
288
|
+
walk_body_paragraphs(package.document.body) do |para|
|
|
289
|
+
(para.runs || []).each do |run|
|
|
290
|
+
(run.drawings || []).each do |drawing|
|
|
291
|
+
drawing_embed_rids(drawing).each do |rid|
|
|
292
|
+
next if valid_rids.include?(rid)
|
|
293
|
+
|
|
294
|
+
dangling += 1
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
return unless dangling.positive?
|
|
301
|
+
|
|
302
|
+
record_fix(FixCodes::DANGLING_DRAWING_REMOVED,
|
|
303
|
+
"Found #{dangling} drawing(s) with dangling image reference(s)")
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# -- Hyperlink references (hyperlink/@r:id → document.xml.rels) --
|
|
307
|
+
|
|
308
|
+
def reconcile_hyperlink_references
|
|
309
|
+
rels = package.document_rels
|
|
310
|
+
return unless rels
|
|
311
|
+
|
|
312
|
+
valid_rids = rels.relationships.to_set(&:id)
|
|
313
|
+
return if valid_rids.empty?
|
|
314
|
+
|
|
315
|
+
removed = 0
|
|
316
|
+
|
|
317
|
+
walk_body_paragraphs(package.document.body) do |para|
|
|
318
|
+
(para.hyperlinks || []).reject! do |hl|
|
|
319
|
+
next false if hl.anchor
|
|
320
|
+
next false unless hl.id
|
|
321
|
+
next false unless hl.id.match?(/\ArId\d+\z/i)
|
|
322
|
+
next false if valid_rids.include?(hl.id)
|
|
323
|
+
|
|
324
|
+
removed += 1
|
|
325
|
+
true
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
return unless removed.positive?
|
|
330
|
+
|
|
331
|
+
record_fix(FixCodes::DANGLING_HYPERLINK_REMOVED,
|
|
332
|
+
"Removed #{removed} dangling hyperlink reference(s) from body")
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# -- Uniqueness constraints --
|
|
336
|
+
|
|
337
|
+
def ensure_para_id_uniqueness
|
|
338
|
+
seen = {}
|
|
339
|
+
collisions = 0
|
|
340
|
+
|
|
341
|
+
walk_all_paragraphs do |para|
|
|
342
|
+
pid = para.para_id
|
|
343
|
+
next unless pid
|
|
344
|
+
|
|
345
|
+
if seen.key?(pid)
|
|
346
|
+
para.para_id = generate_hex_id("collision:#{collisions}")
|
|
347
|
+
collisions += 1
|
|
348
|
+
else
|
|
349
|
+
seen[pid] = true
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
return unless collisions.positive?
|
|
354
|
+
|
|
355
|
+
record_fix(FixCodes::PARAGRAPH_BACKFILL,
|
|
356
|
+
"Resolved #{collisions} paraId collision(s)")
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def ensure_rid_uniqueness
|
|
360
|
+
rels = package.document_rels
|
|
361
|
+
return unless rels
|
|
362
|
+
|
|
363
|
+
seen = {}
|
|
364
|
+
duplicates = []
|
|
365
|
+
|
|
366
|
+
rels.relationships.each do |rel|
|
|
367
|
+
next unless rel.id
|
|
368
|
+
|
|
369
|
+
if seen[rel.id]
|
|
370
|
+
duplicates << rel
|
|
371
|
+
else
|
|
372
|
+
seen[rel.id] = true
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
return if duplicates.empty?
|
|
377
|
+
|
|
378
|
+
duplicates.each do |rel|
|
|
379
|
+
old_id = rel.id
|
|
380
|
+
rel.id = derive_unique_rid(rels, old_id)
|
|
381
|
+
record_fix(FixCodes::RELATIONSHIPS_ASSEMBLED,
|
|
382
|
+
"Deduplicated rId #{old_id} → #{rel.id}")
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
record_fix(FixCodes::RELATIONSHIPS_ASSEMBLED, "Resolved #{duplicates.size} rId collision(s)")
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# -- Traversal helpers --
|
|
389
|
+
|
|
390
|
+
def reconcile_paragraph_style(para, defined_ids, style_names)
|
|
391
|
+
style_ref = Array(para.properties&.style).first
|
|
392
|
+
return 0 unless style_ref&.value
|
|
393
|
+
return 0 if defined_ids.include?(style_ref.value)
|
|
394
|
+
return 0 if style_names.include?(style_ref.value)
|
|
395
|
+
|
|
396
|
+
para.properties.style = nil
|
|
397
|
+
1
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def reconcile_run_styles(para, defined_ids, style_names)
|
|
401
|
+
removed = 0
|
|
402
|
+
(para.runs || []).each do |run|
|
|
403
|
+
r_style = run.properties&.style
|
|
404
|
+
next unless r_style&.value
|
|
405
|
+
next if defined_ids.include?(r_style.value)
|
|
406
|
+
next if style_names.include?(r_style.value)
|
|
407
|
+
|
|
408
|
+
run.properties.style = nil
|
|
409
|
+
removed += 1
|
|
410
|
+
end
|
|
411
|
+
removed
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Extract rId from the blip chain: drawing → inline/anchor → graphic →
|
|
415
|
+
# graphicData → picture → blipFill → blip → embed
|
|
416
|
+
def drawing_embed_rids(drawing)
|
|
417
|
+
rids = []
|
|
418
|
+
[drawing.inline, drawing.anchor].compact.each do |container|
|
|
419
|
+
blip = container.graphic&.graphic_data&.picture&.blip_fill&.blip
|
|
420
|
+
rids << blip.embed.to_s if blip&.embed
|
|
421
|
+
end
|
|
422
|
+
rids
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def numbering_num_id_value(num_pr)
|
|
426
|
+
val = num_pr.num_id
|
|
427
|
+
return nil unless val
|
|
428
|
+
|
|
429
|
+
val.is_a?(Integer) ? val : val.value
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def collect_rels_by_type(rids, *type_fragments)
|
|
433
|
+
return unless package.document_rels
|
|
434
|
+
|
|
435
|
+
package.document_rels.relationships.each do |rel|
|
|
436
|
+
t = rel.type.to_s
|
|
437
|
+
rids << rel.id if type_fragments.any? { |frag| t.include?(frag) }
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def derive_unique_rid(relationships, _current_rel)
|
|
442
|
+
Ooxml::Relationships::PackageRelationships.next_available_rid(
|
|
443
|
+
relationships,
|
|
444
|
+
)
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Docx
|
|
5
|
+
class Reconciler
|
|
6
|
+
# Table structure reconciliation.
|
|
7
|
+
#
|
|
8
|
+
# Ensures tables have required properties, grid, and cell widths.
|
|
9
|
+
# Adds gridAfter for rows that don't cover all grid columns.
|
|
10
|
+
module Tables
|
|
11
|
+
def reconcile_tables
|
|
12
|
+
return unless package.document&.body
|
|
13
|
+
|
|
14
|
+
tables = package.document.body.tables
|
|
15
|
+
return unless tables
|
|
16
|
+
|
|
17
|
+
tables.each_with_index do |tbl, idx|
|
|
18
|
+
if allocator
|
|
19
|
+
reconcile_table_cell_order_only(tbl, idx)
|
|
20
|
+
else
|
|
21
|
+
reconcile_single_table(tbl, idx)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Allocator path: builders create complete tables.
|
|
29
|
+
# Only fix element order issues and calculate gridAfter.
|
|
30
|
+
def reconcile_table_cell_order_only(tbl, idx)
|
|
31
|
+
tbl.rows&.each do |row|
|
|
32
|
+
row.cells&.each do |cell|
|
|
33
|
+
reconcile_table_cell_order(cell)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
reconcile_grid_after(tbl)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def reconcile_single_table(tbl, idx)
|
|
40
|
+
fixed = false
|
|
41
|
+
|
|
42
|
+
unless tbl.properties
|
|
43
|
+
tbl.properties = Wordprocessingml::TableProperties.new
|
|
44
|
+
fixed = true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
tw = tbl.properties.table_width
|
|
48
|
+
if tw.nil?
|
|
49
|
+
tbl.properties.table_width = Properties::TableWidth.new(w: 0, type: "auto")
|
|
50
|
+
fixed = true
|
|
51
|
+
elsif tw.w.nil? || tw.type.nil?
|
|
52
|
+
tw.w ||= 0
|
|
53
|
+
tw.type ||= "auto"
|
|
54
|
+
fixed = true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if tbl.properties.table_look.nil?
|
|
58
|
+
tbl.properties.table_look = Wordprocessingml::TableDefaults.default_table_look
|
|
59
|
+
fixed = true
|
|
60
|
+
elsif tbl.properties.table_look.val.nil?
|
|
61
|
+
Wordprocessingml::TableDefaults.fill_missing_table_look(tbl.properties.table_look)
|
|
62
|
+
fixed = true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
col_count = tbl.column_count
|
|
66
|
+
if tbl.grid.nil?
|
|
67
|
+
grid_cols = Array.new(col_count) { Wordprocessingml::GridCol.new }
|
|
68
|
+
tbl.grid = Wordprocessingml::TableGrid.new(columns: grid_cols)
|
|
69
|
+
fixed = true
|
|
70
|
+
elsif tbl.grid.columns.size != col_count
|
|
71
|
+
current = tbl.grid.columns.size
|
|
72
|
+
if current < col_count
|
|
73
|
+
(col_count - current).times do
|
|
74
|
+
tbl.grid.columns << Wordprocessingml::GridCol.new
|
|
75
|
+
end
|
|
76
|
+
else
|
|
77
|
+
tbl.grid.columns = tbl.grid.columns.first(col_count)
|
|
78
|
+
end
|
|
79
|
+
fixed = true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
tbl.grid&.columns&.each_with_index do |col, ci|
|
|
83
|
+
next if col.width
|
|
84
|
+
|
|
85
|
+
Uniword.logger&.warn do
|
|
86
|
+
"Table #{idx}: gridCol[#{ci}] has no w:w — " \
|
|
87
|
+
"table may render with incorrect column widths"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
record_fix(FixCodes::TABLE_STRUCTURE_RECONCILED, "Reconciled table structure for table #{idx}") if fixed
|
|
92
|
+
|
|
93
|
+
tbl.rows&.each do |row|
|
|
94
|
+
row.cells&.each do |cell|
|
|
95
|
+
tc_pr = cell.properties
|
|
96
|
+
if tc_pr.nil?
|
|
97
|
+
cell.properties = Wordprocessingml::TableCellProperties.new(
|
|
98
|
+
cell_width: Uniword::Properties::CellWidth.new(w: 0, type: "auto"),
|
|
99
|
+
)
|
|
100
|
+
insert_element_order(cell, "tcPr", 0)
|
|
101
|
+
record_fix(FixCodes::TABLE_CELL_DEFAULTS, "Added missing tcPr with tcW to table cell")
|
|
102
|
+
elsif tc_pr.cell_width.nil?
|
|
103
|
+
tc_pr.cell_width = Uniword::Properties::CellWidth.new(w: 0, type: "auto")
|
|
104
|
+
record_fix(FixCodes::TABLE_CELL_DEFAULTS, "Added missing tcW to table cell")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
reconcile_table_cell_order(cell)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
reconcile_grid_after(tbl)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def reconcile_table_cell_order(cell)
|
|
115
|
+
order = cell.element_order
|
|
116
|
+
return unless order && !order.empty?
|
|
117
|
+
|
|
118
|
+
tcPr_idx = order.index { |e| e.name == "tcPr" }
|
|
119
|
+
first_p_idx = order.index { |e| e.name == "p" }
|
|
120
|
+
|
|
121
|
+
return if tcPr_idx.nil? || first_p_idx.nil?
|
|
122
|
+
return if tcPr_idx < first_p_idx
|
|
123
|
+
|
|
124
|
+
tcPr_entry = order.delete_at(tcPr_idx)
|
|
125
|
+
order.insert(first_p_idx, tcPr_entry)
|
|
126
|
+
|
|
127
|
+
record_fix(FixCodes::TABLE_CELL_PR_REORDERED, "Moved tcPr before p in table cell")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def reconcile_grid_after(tbl)
|
|
131
|
+
grid_col_count = tbl.grid&.columns&.size || 0
|
|
132
|
+
return unless grid_col_count.positive?
|
|
133
|
+
|
|
134
|
+
tbl.rows&.each do |row|
|
|
135
|
+
cells = row.cells
|
|
136
|
+
next unless cells && !cells.empty?
|
|
137
|
+
|
|
138
|
+
covered = cells.sum do |cell|
|
|
139
|
+
span = cell.properties&.grid_span&.value
|
|
140
|
+
span && span > 0 ? span : 1
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
missing = grid_col_count - covered
|
|
144
|
+
next if missing <= 0
|
|
145
|
+
|
|
146
|
+
row_props = row.properties
|
|
147
|
+
if row_props.nil?
|
|
148
|
+
row.properties = Wordprocessingml::TableRowProperties.new(
|
|
149
|
+
grid_after: Wordprocessingml::GridAfter.new(value: missing),
|
|
150
|
+
)
|
|
151
|
+
insert_element_order(row, "trPr", 0)
|
|
152
|
+
elsif row_props.grid_after.nil? || row_props.grid_after.value != missing
|
|
153
|
+
row_props.grid_after ||= Wordprocessingml::GridAfter.new
|
|
154
|
+
row_props.grid_after.value = missing
|
|
155
|
+
end
|
|
156
|
+
record_fix(FixCodes::TABLE_ROW_GRID_AFTER, "Added gridAfter=#{missing} to table row (grid has #{grid_col_count} cols, row covers #{covered})")
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|