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
|
@@ -15,7 +15,7 @@ module Uniword
|
|
|
15
15
|
return unless tables
|
|
16
16
|
|
|
17
17
|
tables.each_with_index do |tbl, idx|
|
|
18
|
-
if
|
|
18
|
+
if builder_managed?
|
|
19
19
|
reconcile_table_cell_order_only(tbl, idx)
|
|
20
20
|
else
|
|
21
21
|
reconcile_single_table(tbl, idx)
|
|
@@ -25,14 +25,16 @@ module Uniword
|
|
|
25
25
|
|
|
26
26
|
private
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
# Only fix element order issues
|
|
28
|
+
# Builder-managed path: builders create complete tables.
|
|
29
|
+
# Only fix element order issues, default gridCol widths, and
|
|
30
|
+
# calculate gridAfter.
|
|
30
31
|
def reconcile_table_cell_order_only(tbl, idx)
|
|
31
32
|
tbl.rows&.each do |row|
|
|
32
33
|
row.cells&.each do |cell|
|
|
33
34
|
reconcile_table_cell_order(cell)
|
|
34
35
|
end
|
|
35
36
|
end
|
|
37
|
+
reconcile_grid_col_widths(tbl)
|
|
36
38
|
reconcile_grid_after(tbl)
|
|
37
39
|
end
|
|
38
40
|
|
|
@@ -79,16 +81,11 @@ module Uniword
|
|
|
79
81
|
fixed = true
|
|
80
82
|
end
|
|
81
83
|
|
|
82
|
-
tbl
|
|
83
|
-
next if col.width
|
|
84
|
+
reconcile_grid_col_widths(tbl)
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
record_fix(FixCodes::TABLE_STRUCTURE_RECONCILED, "Reconciled table structure for table #{idx}") if fixed
|
|
86
|
+
record_fix(FixCodes::TABLE_STRUCTURE_RECONCILED,
|
|
87
|
+
"Reconciled table structure for table #{idx}",
|
|
88
|
+
part: "word/document.xml") if fixed
|
|
92
89
|
|
|
93
90
|
tbl.rows&.each do |row|
|
|
94
91
|
row.cells&.each do |cell|
|
|
@@ -98,10 +95,14 @@ module Uniword
|
|
|
98
95
|
cell_width: Uniword::Properties::CellWidth.new(w: 0, type: "auto"),
|
|
99
96
|
)
|
|
100
97
|
insert_element_order(cell, "tcPr", 0)
|
|
101
|
-
record_fix(FixCodes::TABLE_CELL_DEFAULTS,
|
|
98
|
+
record_fix(FixCodes::TABLE_CELL_DEFAULTS,
|
|
99
|
+
"Added missing tcPr with tcW to table cell",
|
|
100
|
+
part: "word/document.xml")
|
|
102
101
|
elsif tc_pr.cell_width.nil?
|
|
103
102
|
tc_pr.cell_width = Uniword::Properties::CellWidth.new(w: 0, type: "auto")
|
|
104
|
-
record_fix(FixCodes::TABLE_CELL_DEFAULTS,
|
|
103
|
+
record_fix(FixCodes::TABLE_CELL_DEFAULTS,
|
|
104
|
+
"Added missing tcW to table cell",
|
|
105
|
+
part: "word/document.xml")
|
|
105
106
|
end
|
|
106
107
|
|
|
107
108
|
reconcile_table_cell_order(cell)
|
|
@@ -111,6 +112,63 @@ module Uniword
|
|
|
111
112
|
reconcile_grid_after(tbl)
|
|
112
113
|
end
|
|
113
114
|
|
|
115
|
+
# Default missing gridCol widths to an equal share of the
|
|
116
|
+
# section's content width (page width minus left/right margins) —
|
|
117
|
+
# the same fallback Word applies, keeping the package free of
|
|
118
|
+
# width-less grids. Explicit widths are kept; width-less columns
|
|
119
|
+
# share the remaining width equally.
|
|
120
|
+
def reconcile_grid_col_widths(tbl)
|
|
121
|
+
columns = tbl.grid&.columns
|
|
122
|
+
return unless columns&.any?
|
|
123
|
+
|
|
124
|
+
missing = columns.count { |col| col.width.nil? }
|
|
125
|
+
return if missing.zero?
|
|
126
|
+
|
|
127
|
+
default_missing_widths(columns, missing)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def default_missing_widths(columns, missing)
|
|
131
|
+
width = shared_column_width(columns, missing)
|
|
132
|
+
columns.each { |col| col.width ||= width }
|
|
133
|
+
record_fix(FixCodes::TABLE_GRID_COL_WIDTHS_DEFAULTED,
|
|
134
|
+
"Defaulted #{missing} gridCol width(s) to " \
|
|
135
|
+
"#{width} twips",
|
|
136
|
+
part: "word/document.xml")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Equal content-width share for `missing` width-less columns
|
|
140
|
+
# after explicit widths take their part.
|
|
141
|
+
def shared_column_width(columns, missing)
|
|
142
|
+
content = section_content_width
|
|
143
|
+
explicit = columns.sum { |col| col.width || 0 }
|
|
144
|
+
remainder = [content - explicit, 0].max
|
|
145
|
+
(remainder.positive? ? remainder : content) / missing
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Content width (page width minus left/right margins) in twips.
|
|
149
|
+
# Tables do not track section ownership, so the body-level sectPr
|
|
150
|
+
# governs; falls back to US Letter defaults.
|
|
151
|
+
def section_content_width
|
|
152
|
+
left, right = section_side_margins
|
|
153
|
+
[section_page_width - left - right, 1].max
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def section_page_width
|
|
157
|
+
sect_pr = package.document&.body&.section_properties
|
|
158
|
+
sect_pr&.page_size&.width ||
|
|
159
|
+
Wordprocessingml::PageDefaults.default_page_size.width
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def section_side_margins
|
|
163
|
+
margins = section_properties_margins
|
|
164
|
+
defaults = Wordprocessingml::PageDefaults.default_page_margins
|
|
165
|
+
[margins&.left || defaults.left, margins&.right || defaults.right]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def section_properties_margins
|
|
169
|
+
package.document&.body&.section_properties&.page_margins
|
|
170
|
+
end
|
|
171
|
+
|
|
114
172
|
def reconcile_table_cell_order(cell)
|
|
115
173
|
order = cell.element_order
|
|
116
174
|
return unless order && !order.empty?
|
|
@@ -124,7 +182,9 @@ module Uniword
|
|
|
124
182
|
tcPr_entry = order.delete_at(tcPr_idx)
|
|
125
183
|
order.insert(first_p_idx, tcPr_entry)
|
|
126
184
|
|
|
127
|
-
record_fix(FixCodes::TABLE_CELL_PR_REORDERED,
|
|
185
|
+
record_fix(FixCodes::TABLE_CELL_PR_REORDERED,
|
|
186
|
+
"Moved tcPr before p in table cell",
|
|
187
|
+
part: "word/document.xml")
|
|
128
188
|
end
|
|
129
189
|
|
|
130
190
|
def reconcile_grid_after(tbl)
|
|
@@ -153,7 +213,9 @@ module Uniword
|
|
|
153
213
|
row_props.grid_after ||= Wordprocessingml::GridAfter.new
|
|
154
214
|
row_props.grid_after.value = missing
|
|
155
215
|
end
|
|
156
|
-
record_fix(FixCodes::TABLE_ROW_GRID_AFTER,
|
|
216
|
+
record_fix(FixCodes::TABLE_ROW_GRID_AFTER,
|
|
217
|
+
"Added gridAfter=#{missing} to table row (grid has #{grid_col_count} cols, row covers #{covered})",
|
|
218
|
+
part: "word/document.xml")
|
|
157
219
|
end
|
|
158
220
|
end
|
|
159
221
|
end
|
|
@@ -20,7 +20,9 @@ module Uniword
|
|
|
20
20
|
word_theme = friendly.to_word_theme
|
|
21
21
|
word_theme.name = "Office Theme"
|
|
22
22
|
package.theme = word_theme
|
|
23
|
-
record_fix(FixCodes::THEME_CREATED,
|
|
23
|
+
record_fix(FixCodes::THEME_CREATED,
|
|
24
|
+
"Created default theme with complete fmtScheme",
|
|
25
|
+
part: "word/theme/theme1.xml")
|
|
24
26
|
rescue ArgumentError
|
|
25
27
|
nil
|
|
26
28
|
end
|
|
@@ -58,7 +60,8 @@ module Uniword
|
|
|
58
60
|
return unless repaired
|
|
59
61
|
|
|
60
62
|
record_fix(FixCodes::THEME_CREATED,
|
|
61
|
-
"Repaired theme fmtScheme with minimum required content"
|
|
63
|
+
"Repaired theme fmtScheme with minimum required content",
|
|
64
|
+
part: "word/theme/theme1.xml")
|
|
62
65
|
end
|
|
63
66
|
|
|
64
67
|
private
|
|
@@ -100,7 +103,7 @@ module Uniword
|
|
|
100
103
|
fills = Array(lst.solid_fills).dup
|
|
101
104
|
while fills.size < 2
|
|
102
105
|
fills << Drawingml::SolidFill.new(
|
|
103
|
-
|
|
106
|
+
scheme_clr: Drawingml::SchemeColor.new(
|
|
104
107
|
val: "accent#{fills.size + 1}"
|
|
105
108
|
),
|
|
106
109
|
)
|
|
@@ -118,7 +121,7 @@ module Uniword
|
|
|
118
121
|
lines << Drawingml::LineProperties.new(
|
|
119
122
|
width: widths[idx] || 9525,
|
|
120
123
|
solid_fill: Drawingml::SolidFill.new(
|
|
121
|
-
|
|
124
|
+
scheme_clr: Drawingml::SchemeColor.new(val: "accent#{idx + 1}"),
|
|
122
125
|
),
|
|
123
126
|
)
|
|
124
127
|
end
|
|
@@ -9,7 +9,8 @@ module Uniword
|
|
|
9
9
|
VALID_NOTE_TYPES = %w[separator continuationSeparator
|
|
10
10
|
footnoteSeparator continuationNotice].freeze
|
|
11
11
|
|
|
12
|
-
# Sub-modules autoloaded —
|
|
12
|
+
# Sub-modules autoloaded — registered below, loaded on first use.
|
|
13
|
+
autoload :Fix, "#{__dir__}/reconciler/fix"
|
|
13
14
|
autoload :FixCodes, "#{__dir__}/reconciler/fix_codes"
|
|
14
15
|
autoload :Helpers, "#{__dir__}/reconciler/helpers"
|
|
15
16
|
autoload :Notes, "#{__dir__}/reconciler/notes"
|
|
@@ -29,10 +30,16 @@ module Uniword
|
|
|
29
30
|
include PackageStructure
|
|
30
31
|
include Body
|
|
31
32
|
|
|
32
|
-
def initialize(package, profile: nil, allocator: nil
|
|
33
|
+
def initialize(package, profile: nil, allocator: nil,
|
|
34
|
+
builder_managed: nil)
|
|
33
35
|
@package = package
|
|
34
36
|
@profile = profile
|
|
35
37
|
@allocator = allocator
|
|
38
|
+
@builder_managed = if builder_managed.nil?
|
|
39
|
+
!allocator.nil? || !package.allocator.nil?
|
|
40
|
+
else
|
|
41
|
+
builder_managed
|
|
42
|
+
end
|
|
36
43
|
@applied_fixes = []
|
|
37
44
|
end
|
|
38
45
|
|
|
@@ -71,23 +78,40 @@ module Uniword
|
|
|
71
78
|
reconcile_referential_integrity
|
|
72
79
|
end
|
|
73
80
|
|
|
74
|
-
# Audit trail of
|
|
81
|
+
# Audit trail of repairs applied during reconciliation.
|
|
82
|
+
#
|
|
83
|
+
# @return [Array<Fix>] Fixes recorded by the most recent #reconcile
|
|
75
84
|
attr_reader :applied_fixes
|
|
76
85
|
|
|
77
86
|
private
|
|
78
87
|
|
|
79
88
|
attr_reader :package, :profile
|
|
80
89
|
|
|
90
|
+
# The single rId/ID authority for this run. Falls back to an
|
|
91
|
+
# allocator populated from the package when the package carries
|
|
92
|
+
# none (direct Reconciler use outside Package#to_zip_content).
|
|
81
93
|
def allocator
|
|
82
|
-
@allocator
|
|
94
|
+
@allocator ||= package.allocator ||
|
|
95
|
+
IdAllocator.populate_from_package(package)
|
|
83
96
|
end
|
|
84
97
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
# Whether the document's IDs were managed outside the reconciler
|
|
99
|
+
# (builder-created content, or a package loaded with an explicit
|
|
100
|
+
# allocator). Builder-managed documents get light-touch repairs;
|
|
101
|
+
# legacy documents get the full normalization repertoire (ID
|
|
102
|
+
# backfill, table normalization, note renumbering). rIds flow
|
|
103
|
+
# through the allocator either way. Decided at initialize time.
|
|
104
|
+
def builder_managed?
|
|
105
|
+
@builder_managed
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Record one applied repair as a Fix value object.
|
|
109
|
+
#
|
|
110
|
+
# @param code [String] Fix code (a FixCodes constant value)
|
|
111
|
+
# @param message [String] Human-readable description of the repair
|
|
112
|
+
# @param part [String, nil] Package part the repair applies to
|
|
113
|
+
def record_fix(code, message, part: nil)
|
|
114
|
+
@applied_fixes << Fix.new(code: code, message: message, part: part)
|
|
91
115
|
end
|
|
92
116
|
|
|
93
117
|
def clear_stored_namespace_plans
|
|
@@ -102,15 +126,13 @@ module Uniword
|
|
|
102
126
|
package.app_properties,
|
|
103
127
|
package.footnotes,
|
|
104
128
|
package.endnotes,
|
|
129
|
+
package.comments,
|
|
105
130
|
].compact
|
|
106
131
|
|
|
107
132
|
(package.document&.header_footer_parts || []).each do |part|
|
|
108
|
-
parts << part
|
|
133
|
+
parts << part.content if part.content
|
|
109
134
|
end
|
|
110
135
|
|
|
111
|
-
(package.document&.headers&.values || []).each { |h| parts << h }
|
|
112
|
-
(package.document&.footers&.values || []).each { |f| parts << f }
|
|
113
|
-
|
|
114
136
|
parts.each do |part|
|
|
115
137
|
part.import_declaration_plan = nil
|
|
116
138
|
part.pending_plan_root_element = nil
|
data/lib/uniword/docx.rb
CHANGED
|
@@ -8,8 +8,18 @@ module Uniword
|
|
|
8
8
|
# document-level reconciliation, and user-defined requirements.
|
|
9
9
|
module Docx
|
|
10
10
|
autoload :IdAllocator, "#{__dir__}/docx/id_allocator"
|
|
11
|
+
autoload :Part, "#{__dir__}/docx/part"
|
|
12
|
+
autoload :ChartPart, "#{__dir__}/docx/chart_part"
|
|
13
|
+
autoload :HeaderFooterPart, "#{__dir__}/docx/header_footer_part"
|
|
14
|
+
autoload :CustomXmlItem, "#{__dir__}/docx/custom_xml_item"
|
|
15
|
+
autoload :PartCollection, "#{__dir__}/docx/part_collection"
|
|
16
|
+
autoload :HeaderFooterPartCollection,
|
|
17
|
+
"#{__dir__}/docx/header_footer_part_collection"
|
|
18
|
+
autoload :HeaderFooterView, "#{__dir__}/docx/header_footer_view"
|
|
11
19
|
autoload :Package, "#{__dir__}/docx/package"
|
|
12
20
|
autoload :PackageDefaults, "#{__dir__}/docx/package_defaults"
|
|
21
|
+
autoload :PackageIntegrityChecker,
|
|
22
|
+
"#{__dir__}/docx/package_integrity_checker"
|
|
13
23
|
autoload :PackageSerialization, "#{__dir__}/docx/package_serialization"
|
|
14
24
|
autoload :Profile, "#{__dir__}/docx/profile"
|
|
15
25
|
autoload :DocumentStatistics, "#{__dir__}/docx/document_statistics"
|
|
@@ -399,14 +399,13 @@ module Uniword
|
|
|
399
399
|
|
|
400
400
|
# Duplicate the color scheme
|
|
401
401
|
#
|
|
402
|
+
# Deep copy via XML round-trip — preserves system-color entries
|
|
403
|
+
# (dk1/lt1 as windowText/window) and every color slot, not just
|
|
404
|
+
# the standard hex values.
|
|
405
|
+
#
|
|
402
406
|
# @return [ColorScheme] A deep copy of this color scheme
|
|
403
407
|
def dup
|
|
404
|
-
|
|
405
|
-
THEME_COLORS.each do |color_name|
|
|
406
|
-
value = self[color_name]
|
|
407
|
-
new_scheme[color_name] = value if value
|
|
408
|
-
end
|
|
409
|
-
new_scheme
|
|
408
|
+
self.class.from_xml(to_xml)
|
|
410
409
|
end
|
|
411
410
|
end
|
|
412
411
|
end
|
|
@@ -225,16 +225,12 @@ module Uniword
|
|
|
225
225
|
|
|
226
226
|
# Duplicate the font scheme
|
|
227
227
|
#
|
|
228
|
+
# Deep copy via XML round-trip — preserves per-script font entries
|
|
229
|
+
# (Jpan/Hang/Arab/...) that the previous hand-rolled copy dropped.
|
|
230
|
+
#
|
|
228
231
|
# @return [FontScheme] A deep copy of this font scheme
|
|
229
232
|
def dup
|
|
230
|
-
|
|
231
|
-
new_scheme.major_font = major_font
|
|
232
|
-
new_scheme.minor_font = minor_font
|
|
233
|
-
new_scheme.major_east_asian = major_east_asian if major_east_asian
|
|
234
|
-
new_scheme.major_complex_script = major_complex_script if major_complex_script
|
|
235
|
-
new_scheme.minor_east_asian = minor_east_asian if minor_east_asian
|
|
236
|
-
new_scheme.minor_complex_script = minor_complex_script if minor_complex_script
|
|
237
|
-
new_scheme
|
|
233
|
+
self.class.from_xml(to_xml)
|
|
238
234
|
end
|
|
239
235
|
end
|
|
240
236
|
end
|
|
@@ -127,14 +127,20 @@ module Uniword
|
|
|
127
127
|
|
|
128
128
|
# Duplicate the theme
|
|
129
129
|
#
|
|
130
|
+
# Deep copy via XML round-trip: copies color scheme, font scheme,
|
|
131
|
+
# format scheme (fill/line/effect lists), and every other mapped
|
|
132
|
+
# part. The previous hand-rolled copy dropped fmt_scheme (and
|
|
133
|
+
# object_defaults/ext_lst/media_files), producing themes with
|
|
134
|
+
# empty fillStyleLst that Word rejects.
|
|
135
|
+
#
|
|
130
136
|
# @return [Theme] A deep copy of this theme
|
|
131
137
|
def dup
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
self.class.from_xml(to_xml).tap do |copy|
|
|
139
|
+
copy.variants = @variants&.dup
|
|
140
|
+
copy.source_file = @source_file
|
|
141
|
+
copy.media_files = @media_files
|
|
142
|
+
copy.raw_xml = @raw_xml
|
|
143
|
+
end
|
|
138
144
|
end
|
|
139
145
|
|
|
140
146
|
# Check if theme is valid
|
data/lib/uniword/errors.rb
CHANGED
|
@@ -54,11 +54,14 @@ module Uniword
|
|
|
54
54
|
class ValidationError < Error
|
|
55
55
|
# @param element [Element] The element that failed validation
|
|
56
56
|
# @param errors [Array<String>] Array of validation error messages
|
|
57
|
-
|
|
57
|
+
# @param issues [Array] Structured issues (e.g.
|
|
58
|
+
# Validation::Report::ValidationIssue with code, part, message)
|
|
59
|
+
def initialize(element, errors, issues: [])
|
|
58
60
|
element_name = element.class.name.split("::").last
|
|
59
61
|
super("Validation failed for #{element_name}: #{errors.join(', ')}")
|
|
60
62
|
@element = element
|
|
61
63
|
@errors = errors
|
|
64
|
+
@issues = issues
|
|
62
65
|
end
|
|
63
66
|
|
|
64
67
|
# @return [Element] The element that failed
|
|
@@ -66,6 +69,10 @@ module Uniword
|
|
|
66
69
|
|
|
67
70
|
# @return [Array<String>] The validation errors
|
|
68
71
|
attr_reader :errors
|
|
72
|
+
|
|
73
|
+
# @return [Array] Structured issues (code, part, message) — populated
|
|
74
|
+
# by the write-time package integrity gate
|
|
75
|
+
attr_reader :issues
|
|
69
76
|
end
|
|
70
77
|
|
|
71
78
|
# Raised when trying to write to a read-only document
|
data/lib/uniword/footer.rb
CHANGED
|
@@ -12,6 +12,10 @@ module Uniword
|
|
|
12
12
|
attribute :tables, Wordprocessingml::Table, collection: true,
|
|
13
13
|
initialize_empty: true
|
|
14
14
|
|
|
15
|
+
# mc:Ignorable prefix list carried to the serialized
|
|
16
|
+
# Wordprocessingml footer part (set by the reconciler).
|
|
17
|
+
attr_accessor :mc_ignorable
|
|
18
|
+
|
|
15
19
|
# Valid footer types
|
|
16
20
|
TYPES = %w[default first even].freeze
|
|
17
21
|
|
data/lib/uniword/header.rb
CHANGED
|
@@ -12,6 +12,10 @@ module Uniword
|
|
|
12
12
|
attribute :tables, Wordprocessingml::Table, collection: true,
|
|
13
13
|
initialize_empty: true
|
|
14
14
|
|
|
15
|
+
# mc:Ignorable prefix list carried to the serialized
|
|
16
|
+
# Wordprocessingml header part (set by the reconciler).
|
|
17
|
+
attr_accessor :mc_ignorable
|
|
18
|
+
|
|
15
19
|
# Valid header types
|
|
16
20
|
TYPES = %w[default first even].freeze
|
|
17
21
|
|
data/lib/uniword/hyperlink.rb
CHANGED
|
@@ -15,7 +15,7 @@ module Uniword
|
|
|
15
15
|
# link = Hyperlink.new(anchor: 'section1', text: 'Go to section')
|
|
16
16
|
# link.internal? # => true
|
|
17
17
|
class Hyperlink
|
|
18
|
-
REL_TYPE =
|
|
18
|
+
REL_TYPE = Ooxml::PartRegistry.find_by_key(:hyperlink).rel_type
|
|
19
19
|
|
|
20
20
|
attr_reader :url, :anchor, :text, :tooltip
|
|
21
21
|
|
data/lib/uniword/image.rb
CHANGED
|
@@ -15,12 +15,6 @@ module Uniword
|
|
|
15
15
|
class Image < Element
|
|
16
16
|
extend LazyLoader
|
|
17
17
|
|
|
18
|
-
# OOXML namespace configuration for images (drawing elements)
|
|
19
|
-
xml do
|
|
20
|
-
element "drawing"
|
|
21
|
-
namespace Ooxml::Namespaces::WordProcessingML
|
|
22
|
-
end
|
|
23
|
-
|
|
24
18
|
# Image relationship ID (reference to the image file in the package)
|
|
25
19
|
attribute :relationship_id, :string
|
|
26
20
|
|
|
@@ -52,6 +46,12 @@ module Uniword
|
|
|
52
46
|
# Text wrapping style (:square, :tight, :through, :none, etc.)
|
|
53
47
|
attribute :text_wrapping, :string
|
|
54
48
|
|
|
49
|
+
# OOXML namespace configuration for images (drawing elements)
|
|
50
|
+
xml do
|
|
51
|
+
element "drawing"
|
|
52
|
+
namespace Ooxml::Namespaces::WordProcessingML
|
|
53
|
+
end
|
|
54
|
+
|
|
55
55
|
# Image data loader (proc that loads the actual binary data)
|
|
56
56
|
# This is not serialized - it's used for lazy loading
|
|
57
57
|
attr_accessor :data_loader
|
data/lib/uniword/lazy_loader.rb
CHANGED
|
@@ -7,8 +7,7 @@ module Uniword
|
|
|
7
7
|
# which helps reduce memory usage for large documents.
|
|
8
8
|
#
|
|
9
9
|
# All cached values are stored in a single Hash (`@_lazy_cache`)
|
|
10
|
-
# accessed via direct ivar reference
|
|
11
|
-
# no public_send, no remove_instance_variable.
|
|
10
|
+
# accessed via direct ivar reference inside the generated methods.
|
|
12
11
|
#
|
|
13
12
|
# @example Using lazy attributes
|
|
14
13
|
# class Document
|
|
@@ -7,21 +7,37 @@ module Uniword
|
|
|
7
7
|
# getters/setters generated by lutaml-model for declared attributes.
|
|
8
8
|
#
|
|
9
9
|
# Validates that the attribute name exists in the model's schema before
|
|
10
|
-
# dispatching
|
|
11
|
-
#
|
|
10
|
+
# dispatching through the model's public method table — only attributes
|
|
11
|
+
# that are part of the model's declared public API can be accessed.
|
|
12
12
|
module ModelAttributeAccess
|
|
13
13
|
# Read a declared attribute's value by name.
|
|
14
14
|
# @param name [Symbol] Declared attribute name
|
|
15
15
|
# @return [Object] The attribute value
|
|
16
|
+
# @raise [ArgumentError] if the attribute is not declared on the model
|
|
16
17
|
def read_attribute(name)
|
|
17
|
-
|
|
18
|
+
validate_declared_attribute!(name)
|
|
19
|
+
method(name).call
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
# Write a declared attribute's value by name.
|
|
21
23
|
# @param name [Symbol] Declared attribute name
|
|
22
24
|
# @param value [Object] The value to assign
|
|
25
|
+
# @raise [ArgumentError] if the attribute is not declared on the model
|
|
23
26
|
def write_attribute(name, value)
|
|
24
|
-
|
|
27
|
+
validate_declared_attribute!(name)
|
|
28
|
+
method(:"#{name}=").call(value)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Validate that the attribute is declared in the model's schema.
|
|
34
|
+
# @param name [Symbol, String] Attribute name
|
|
35
|
+
# @raise [ArgumentError] if the attribute is not declared
|
|
36
|
+
def validate_declared_attribute!(name)
|
|
37
|
+
return if self.class.attributes.key?(name.to_sym)
|
|
38
|
+
|
|
39
|
+
raise ArgumentError,
|
|
40
|
+
"undeclared attribute #{name.inspect} for #{self.class}"
|
|
25
41
|
end
|
|
26
42
|
end
|
|
27
43
|
end
|
|
@@ -195,7 +195,13 @@ module Uniword
|
|
|
195
195
|
#
|
|
196
196
|
# @param document [Document] The document to save (Generated::Wordprocessingml::DocumentRoot)
|
|
197
197
|
# @param path [String] Output path
|
|
198
|
-
|
|
198
|
+
# @param profile [Object, nil] Unused (DOCX reconciliation profile)
|
|
199
|
+
# @param validate [Boolean, nil] Run the package integrity gate before
|
|
200
|
+
# writing; nil falls back to Uniword.configuration.validate_on_save
|
|
201
|
+
# @return [void]
|
|
202
|
+
# @raise [Uniword::ValidationError] when the gate is enabled and the
|
|
203
|
+
# generated package content is invalid
|
|
204
|
+
def self.to_file(document, path, profile: nil, validate: nil)
|
|
199
205
|
# Create package
|
|
200
206
|
package = new
|
|
201
207
|
|
|
@@ -217,6 +223,9 @@ module Uniword
|
|
|
217
223
|
# Add required OOXML infrastructure files
|
|
218
224
|
add_required_files(zip_content)
|
|
219
225
|
|
|
226
|
+
# Refuse invalid output before packaging
|
|
227
|
+
package.enforce_package_integrity(zip_content, validate)
|
|
228
|
+
|
|
220
229
|
# Package and save
|
|
221
230
|
packager = Infrastructure::ZipPackager.new
|
|
222
231
|
packager.package(zip_content, path)
|
|
@@ -225,13 +234,44 @@ module Uniword
|
|
|
225
234
|
# Save package to file
|
|
226
235
|
#
|
|
227
236
|
# @param path [String] Output path
|
|
228
|
-
|
|
237
|
+
# @param validate [Boolean, nil] Run the package integrity gate before
|
|
238
|
+
# writing; nil falls back to Uniword.configuration.validate_on_save
|
|
239
|
+
# @return [void]
|
|
240
|
+
# @raise [Uniword::ValidationError] when the gate is enabled and the
|
|
241
|
+
# generated package content is invalid
|
|
242
|
+
def to_file(path, validate: nil)
|
|
229
243
|
zip_content = to_zip_content
|
|
230
244
|
|
|
245
|
+
enforce_package_integrity(zip_content, validate)
|
|
246
|
+
|
|
231
247
|
packager = Infrastructure::ZipPackager.new
|
|
232
248
|
packager.package(zip_content, path)
|
|
233
249
|
end
|
|
234
250
|
|
|
251
|
+
# Write-time integrity gate: refuse invalid package content.
|
|
252
|
+
#
|
|
253
|
+
# DOTX is an OPC package like DOCX, so the same
|
|
254
|
+
# Docx::PackageIntegrityChecker invariants apply.
|
|
255
|
+
#
|
|
256
|
+
# @api private
|
|
257
|
+
# @param zip_content [Hash] File paths => content
|
|
258
|
+
# @param validate [Boolean, nil] explicit override; nil reads policy
|
|
259
|
+
# @return [void]
|
|
260
|
+
# @raise [Uniword::ValidationError] listing all integrity issues
|
|
261
|
+
def enforce_package_integrity(zip_content, validate)
|
|
262
|
+
validate = Uniword.configuration.validate_on_save if validate.nil?
|
|
263
|
+
return unless validate
|
|
264
|
+
|
|
265
|
+
issues = Docx::PackageIntegrityChecker.new.check(zip_content)
|
|
266
|
+
return if issues.empty?
|
|
267
|
+
|
|
268
|
+
raise Uniword::ValidationError.new(
|
|
269
|
+
self,
|
|
270
|
+
issues.map { |issue| "#{issue.code} (#{issue.part}): #{issue.message}" },
|
|
271
|
+
issues: issues,
|
|
272
|
+
)
|
|
273
|
+
end
|
|
274
|
+
|
|
235
275
|
# Generate ZIP content hash
|
|
236
276
|
#
|
|
237
277
|
# @return [Hash] File paths => content
|