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
|
@@ -1,8 +1,100 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Uniword
|
|
4
|
-
|
|
4
|
+
# Global runtime configuration for Uniword.
|
|
5
|
+
#
|
|
6
|
+
# Holds save-path policy as plain, explicitly typed boolean attributes.
|
|
7
|
+
# This object is runtime policy only: it has no file-loading behavior
|
|
8
|
+
# (see ConfigurationLoader for external YAML config files) and no
|
|
9
|
+
# serialization behavior.
|
|
10
|
+
#
|
|
11
|
+
# This class also serves as the namespace for configuration-related
|
|
12
|
+
# classes (ConfigurationLoader, ConfigurationError).
|
|
13
|
+
#
|
|
14
|
+
# @example Read current policy
|
|
15
|
+
# Uniword.configuration.validate_on_save # => true
|
|
16
|
+
#
|
|
17
|
+
# @example Change policy via Uniword.configure
|
|
18
|
+
# Uniword.configure do |config|
|
|
19
|
+
# config.xsd_validation = true
|
|
20
|
+
# end
|
|
21
|
+
class Configuration
|
|
5
22
|
autoload :ConfigurationLoader,
|
|
6
23
|
"#{__dir__}/configuration/configuration_loader"
|
|
24
|
+
|
|
25
|
+
# Whether documents are validated when saved.
|
|
26
|
+
#
|
|
27
|
+
# @return [Boolean]
|
|
28
|
+
attr_reader :validate_on_save
|
|
29
|
+
|
|
30
|
+
# Whether XSD schema validation runs on save (slower, stricter).
|
|
31
|
+
#
|
|
32
|
+
# @return [Boolean]
|
|
33
|
+
attr_reader :xsd_validation
|
|
34
|
+
|
|
35
|
+
# Whether automatic fixes applied during save are logged.
|
|
36
|
+
#
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
attr_reader :log_save_fixes
|
|
39
|
+
|
|
40
|
+
# Create a configuration with default policy values.
|
|
41
|
+
#
|
|
42
|
+
# Defaults: validate_on_save: true, xsd_validation: false,
|
|
43
|
+
# log_save_fixes: true.
|
|
44
|
+
def initialize
|
|
45
|
+
reset!
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Restore all attributes to their default values.
|
|
49
|
+
#
|
|
50
|
+
# @return [Configuration] self
|
|
51
|
+
def reset!
|
|
52
|
+
@validate_on_save = true
|
|
53
|
+
@xsd_validation = false
|
|
54
|
+
@log_save_fixes = true
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Set the validate-on-save policy.
|
|
59
|
+
#
|
|
60
|
+
# @param value [Boolean] new value
|
|
61
|
+
# @return [Boolean] the value set
|
|
62
|
+
# @raise [ArgumentError] if value is not true or false
|
|
63
|
+
def validate_on_save=(value)
|
|
64
|
+
@validate_on_save = typed_boolean(value, :validate_on_save)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Set the XSD validation policy.
|
|
68
|
+
#
|
|
69
|
+
# @param value [Boolean] new value
|
|
70
|
+
# @return [Boolean] the value set
|
|
71
|
+
# @raise [ArgumentError] if value is not true or false
|
|
72
|
+
def xsd_validation=(value)
|
|
73
|
+
@xsd_validation = typed_boolean(value, :xsd_validation)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Set the save-fix logging policy.
|
|
77
|
+
#
|
|
78
|
+
# @param value [Boolean] new value
|
|
79
|
+
# @return [Boolean] the value set
|
|
80
|
+
# @raise [ArgumentError] if value is not true or false
|
|
81
|
+
def log_save_fixes=(value)
|
|
82
|
+
@log_save_fixes = typed_boolean(value, :log_save_fixes)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# Validate that a value is strictly boolean.
|
|
88
|
+
#
|
|
89
|
+
# @param value [Object] value to check
|
|
90
|
+
# @param name [Symbol] attribute name used in the error message
|
|
91
|
+
# @return [Boolean] the value
|
|
92
|
+
# @raise [ArgumentError] if value is not true or false
|
|
93
|
+
def typed_boolean(value, name)
|
|
94
|
+
return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
95
|
+
|
|
96
|
+
raise ArgumentError,
|
|
97
|
+
"#{name} must be true or false, got #{value.inspect}"
|
|
98
|
+
end
|
|
7
99
|
end
|
|
8
100
|
end
|
|
@@ -16,41 +16,20 @@ module Uniword
|
|
|
16
16
|
#
|
|
17
17
|
# Generate comprehensive [Content_Types].xml
|
|
18
18
|
#
|
|
19
|
+
# Derived from Ooxml::PartRegistry (:standard definitions, in
|
|
20
|
+
# registration order — historically the literal order below).
|
|
21
|
+
#
|
|
19
22
|
# @return [Types] Content types object
|
|
20
23
|
def generate
|
|
21
24
|
Types.new(
|
|
22
|
-
defaults:
|
|
23
|
-
Default.new(extension:
|
|
24
|
-
content_type:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
content_type: "application/vnd.openxmlformats-package.relationships+xml"),
|
|
31
|
-
Default.new(extension: "xml",
|
|
32
|
-
content_type: "application/xml"),
|
|
33
|
-
],
|
|
34
|
-
overrides: [
|
|
35
|
-
Override.new(part_name: "/word/document.xml",
|
|
36
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"),
|
|
37
|
-
Override.new(part_name: "/word/numbering.xml",
|
|
38
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"),
|
|
39
|
-
Override.new(part_name: "/word/styles.xml",
|
|
40
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"),
|
|
41
|
-
Override.new(part_name: "/word/settings.xml",
|
|
42
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"),
|
|
43
|
-
Override.new(part_name: "/word/webSettings.xml",
|
|
44
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml"),
|
|
45
|
-
Override.new(part_name: "/word/fontTable.xml",
|
|
46
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml"),
|
|
47
|
-
Override.new(part_name: "/word/theme/theme1.xml",
|
|
48
|
-
content_type: "application/vnd.openxmlformats-officedocument.theme+xml"),
|
|
49
|
-
Override.new(part_name: "/docProps/core.xml",
|
|
50
|
-
content_type: "application/vnd.openxmlformats-package.core-properties+xml"),
|
|
51
|
-
Override.new(part_name: "/docProps/app.xml",
|
|
52
|
-
content_type: "application/vnd.openxmlformats-officedocument.extended-properties+xml"),
|
|
53
|
-
],
|
|
25
|
+
defaults: Ooxml::PartRegistry.standard_defaults.map do |defn|
|
|
26
|
+
Default.new(extension: defn.extension,
|
|
27
|
+
content_type: defn.content_type)
|
|
28
|
+
end,
|
|
29
|
+
overrides: Ooxml::PartRegistry.standard_overrides.map do |defn|
|
|
30
|
+
Override.new(part_name: defn.part_name,
|
|
31
|
+
content_type: defn.content_type)
|
|
32
|
+
end,
|
|
54
33
|
)
|
|
55
34
|
end
|
|
56
35
|
module_function :generate
|
|
@@ -59,15 +38,16 @@ module Uniword
|
|
|
59
38
|
#
|
|
60
39
|
# @return [Types] Content types object for theme package
|
|
61
40
|
def generate_for_theme
|
|
41
|
+
theme = Ooxml::PartRegistry.find_by_key(:thmx_theme)
|
|
62
42
|
Types.new(
|
|
63
|
-
defaults: [
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
43
|
+
defaults: %i[rels xml].map do |key|
|
|
44
|
+
defn = Ooxml::PartRegistry.find_by_key(key)
|
|
45
|
+
Default.new(extension: defn.extension,
|
|
46
|
+
content_type: defn.content_type)
|
|
47
|
+
end,
|
|
68
48
|
overrides: [
|
|
69
|
-
Override.new(part_name:
|
|
70
|
-
content_type:
|
|
49
|
+
Override.new(part_name: theme.part_name,
|
|
50
|
+
content_type: theme.content_type),
|
|
71
51
|
],
|
|
72
52
|
)
|
|
73
53
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# All classes autoloaded via lib/uniword.rb
|
|
4
|
-
#
|
|
4
|
+
# Lazy loading for internal library code is handled by autoload.
|
|
5
5
|
|
|
6
6
|
module Uniword
|
|
7
7
|
# Factory for creating Document instances.
|
|
@@ -194,6 +194,14 @@ module Uniword
|
|
|
194
194
|
document.custom_xml_items = package.custom_xml_items if package.custom_xml_items
|
|
195
195
|
document.footnotes = package.footnotes if package.footnotes
|
|
196
196
|
document.endnotes = package.endnotes if package.endnotes
|
|
197
|
+
document.comments = package.comments if package.comments
|
|
198
|
+
document.settings_rels = package.settings_rels if package.settings_rels
|
|
199
|
+
if package.footnotes_rels
|
|
200
|
+
document.footnotes_rels = package.footnotes_rels
|
|
201
|
+
end
|
|
202
|
+
if package.endnotes_rels
|
|
203
|
+
document.endnotes_rels = package.endnotes_rels
|
|
204
|
+
end
|
|
197
205
|
end
|
|
198
206
|
|
|
199
207
|
private
|
|
@@ -33,25 +33,34 @@ module Uniword
|
|
|
33
33
|
#
|
|
34
34
|
# @param path [String] The output file path
|
|
35
35
|
# @param format [Symbol] The format (:auto, :docx, :mhtml)
|
|
36
|
+
# @param profile [Docx::Profile, nil] Profile for reconciliation
|
|
37
|
+
# @param validate [Boolean, nil] Run the package integrity gate before
|
|
38
|
+
# writing; nil falls back to Uniword.configuration.validate_on_save.
|
|
39
|
+
# Not applicable to :mhtml (MIME, not an OPC/ZIP package — there are
|
|
40
|
+
# no content types, relationships parts, or ZIP entries to check).
|
|
36
41
|
# @return [void]
|
|
37
42
|
# @raise [ArgumentError] if path is invalid
|
|
38
43
|
# @raise [ArgumentError] if format is not supported
|
|
44
|
+
# @raise [Uniword::ValidationError] when the gate is enabled and the
|
|
45
|
+
# generated package content is invalid
|
|
39
46
|
#
|
|
40
47
|
# @example Save as DOCX
|
|
41
48
|
# writer.save("output.docx")
|
|
42
49
|
#
|
|
43
50
|
# @example Save with explicit format
|
|
44
51
|
# writer.save("output.mht", format: :mhtml)
|
|
45
|
-
def save(path, format: :auto, profile: nil)
|
|
52
|
+
def save(path, format: :auto, profile: nil, validate: nil)
|
|
46
53
|
validate_path(path)
|
|
47
54
|
|
|
48
55
|
format = infer_format(path) if format == :auto
|
|
49
56
|
|
|
50
57
|
case format
|
|
51
58
|
when :docx, :docm
|
|
52
|
-
Docx::Package.to_file(document, path, profile: profile
|
|
59
|
+
Docx::Package.to_file(document, path, profile: profile,
|
|
60
|
+
validate: validate)
|
|
53
61
|
when :dotx, :dotm
|
|
54
|
-
Ooxml::DotxPackage.to_file(document, path, profile: profile
|
|
62
|
+
Ooxml::DotxPackage.to_file(document, path, profile: profile,
|
|
63
|
+
validate: validate)
|
|
55
64
|
when :mhtml
|
|
56
65
|
Mhtml::MhtmlPackage.to_file(document, path)
|
|
57
66
|
else
|
|
@@ -99,6 +108,8 @@ module Uniword
|
|
|
99
108
|
#
|
|
100
109
|
# @param stream [IO, StringIO] The output stream
|
|
101
110
|
# @param format [Symbol] The format (:docx, :mhtml)
|
|
111
|
+
# @param validate [Boolean, nil] Run the package integrity gate before
|
|
112
|
+
# writing; nil falls back to Uniword.configuration.validate_on_save
|
|
102
113
|
# @return [void]
|
|
103
114
|
#
|
|
104
115
|
# @example Write to StringIO
|
|
@@ -106,14 +117,14 @@ module Uniword
|
|
|
106
117
|
# writer.write_to_stream(io)
|
|
107
118
|
# io.rewind
|
|
108
119
|
# content = io.read
|
|
109
|
-
def write_to_stream(stream, format: :docx)
|
|
120
|
+
def write_to_stream(stream, format: :docx, validate: nil)
|
|
110
121
|
require "tempfile"
|
|
111
122
|
|
|
112
123
|
# Use a temporary file to generate the document
|
|
113
124
|
temp_file = Tempfile.new(["uniword_stream", ".#{format}"], binmode: true)
|
|
114
125
|
begin
|
|
115
126
|
# Save to temp file
|
|
116
|
-
save(temp_file.path, format: format)
|
|
127
|
+
save(temp_file.path, format: format, validate: validate)
|
|
117
128
|
|
|
118
129
|
# Read and write to stream in binary mode
|
|
119
130
|
temp_file.rewind
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Docx
|
|
5
|
+
# A chart part (word/charts/chartN.xml) held by the package.
|
|
6
|
+
#
|
|
7
|
+
# The content is the raw chart XML String. Replaces the former
|
|
8
|
+
# +{ xml:, target: }+ hash entries in +chart_parts+.
|
|
9
|
+
class ChartPart < Part
|
|
10
|
+
# @param r_id [String, nil] relationship id
|
|
11
|
+
# @param target [String, nil] e.g. "charts/chart1.xml"
|
|
12
|
+
# @param content [String, nil] raw chart XML
|
|
13
|
+
def initialize(r_id: nil, target: nil, content: nil, **rest)
|
|
14
|
+
super(
|
|
15
|
+
definition: Ooxml::PartRegistry.find_by_key(:chart),
|
|
16
|
+
r_id: r_id, target: target, content: content, **rest
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Raw chart XML (alias for content).
|
|
21
|
+
#
|
|
22
|
+
# @return [String, nil]
|
|
23
|
+
def xml
|
|
24
|
+
content
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Hash-style read compatibility (+:xml+ in addition to the
|
|
28
|
+
# Part keys).
|
|
29
|
+
def [](key)
|
|
30
|
+
key.to_sym == :xml ? content : super
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Docx
|
|
5
|
+
# One customXml data item (customXml/itemN.xml) with its optional
|
|
6
|
+
# properties part and relationships part.
|
|
7
|
+
#
|
|
8
|
+
# Replaces the former +{ index:, xml_content:, props_xml:,
|
|
9
|
+
# rels_xml: }+ hash entries in +custom_xml_items+.
|
|
10
|
+
class CustomXmlItem
|
|
11
|
+
# @return [Integer] item number (customXml/item%<index>d.xml)
|
|
12
|
+
attr_accessor :index
|
|
13
|
+
|
|
14
|
+
# @return [String] raw XML of the item part
|
|
15
|
+
attr_accessor :xml_content
|
|
16
|
+
|
|
17
|
+
# @return [String, nil] raw XML of the itemProps part
|
|
18
|
+
attr_accessor :props_xml
|
|
19
|
+
|
|
20
|
+
# @return [String, nil] raw XML of the item relationships part
|
|
21
|
+
attr_accessor :rels_xml
|
|
22
|
+
|
|
23
|
+
# @param index [Integer] item number
|
|
24
|
+
# @param xml_content [String] raw item XML
|
|
25
|
+
# @param props_xml [String, nil] raw itemProps XML
|
|
26
|
+
# @param rels_xml [String, nil] raw item relationships XML
|
|
27
|
+
def initialize(index:, xml_content:, props_xml: nil, rels_xml: nil)
|
|
28
|
+
@index = index
|
|
29
|
+
@xml_content = xml_content
|
|
30
|
+
@props_xml = props_xml
|
|
31
|
+
@rels_xml = rels_xml
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Normalize a legacy hash entry (or an existing CustomXmlItem)
|
|
35
|
+
# into a CustomXmlItem.
|
|
36
|
+
#
|
|
37
|
+
# @param value [CustomXmlItem, Hash]
|
|
38
|
+
# @return [CustomXmlItem]
|
|
39
|
+
def self.wrap(value)
|
|
40
|
+
return value if value.is_a?(CustomXmlItem)
|
|
41
|
+
|
|
42
|
+
new(
|
|
43
|
+
index: value[:index],
|
|
44
|
+
xml_content: value[:xml_content],
|
|
45
|
+
props_xml: value[:props_xml],
|
|
46
|
+
rels_xml: value[:rels_xml],
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Package path of the item part.
|
|
51
|
+
#
|
|
52
|
+
# @return [String] e.g. "customXml/item1.xml"
|
|
53
|
+
def path
|
|
54
|
+
Ooxml::PartRegistry.find_by_key(:custom_xml_item)
|
|
55
|
+
.path_for(index: index)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Package path of the properties part.
|
|
59
|
+
#
|
|
60
|
+
# @return [String, nil] e.g. "customXml/itemProps1.xml"
|
|
61
|
+
def props_path
|
|
62
|
+
return nil unless props_xml
|
|
63
|
+
|
|
64
|
+
Ooxml::PartRegistry.find_by_key(:custom_xml_item_props)
|
|
65
|
+
.path_for(index: index)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Hash-style read compatibility.
|
|
69
|
+
#
|
|
70
|
+
# @param key [Symbol, String] one of :index, :xml_content,
|
|
71
|
+
# :props_xml, :rels_xml
|
|
72
|
+
# @return [Object, nil]
|
|
73
|
+
def [](key)
|
|
74
|
+
case key.to_sym
|
|
75
|
+
when :index then index
|
|
76
|
+
when :xml_content then xml_content
|
|
77
|
+
when :props_xml then props_xml
|
|
78
|
+
when :rels_xml then rels_xml
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Docx
|
|
5
|
+
# A header or footer part (word/headerN.xml / word/footerN.xml)
|
|
6
|
+
# held by the package — the single storage form for both
|
|
7
|
+
# round-tripped and builder-created headers/footers.
|
|
8
|
+
#
|
|
9
|
+
# Parts loaded from an existing DOCX carry their original +r_id+,
|
|
10
|
+
# +target+ and +rel_type+ verbatim and are flagged +loaded+ so the
|
|
11
|
+
# reconciler leaves their relationships and section references
|
|
12
|
+
# untouched. Parts added programmatically (via the Builder or the
|
|
13
|
+
# +headers+/+footers+ views) are flagged fresh: the reconciler
|
|
14
|
+
# assigns them a relationship and wires the sectPr reference.
|
|
15
|
+
#
|
|
16
|
+
# The +type+ ("default"/"first"/"even") is the sectPr reference
|
|
17
|
+
# type — derived from the section properties on load, set by the
|
|
18
|
+
# caller on insert. Content is usually a
|
|
19
|
+
# Wordprocessingml::Header or Wordprocessingml::Footer model;
|
|
20
|
+
# legacy Uniword::Header/Uniword::Footer convenience models are
|
|
21
|
+
# stored as given and converted at serialization time so callers
|
|
22
|
+
# can keep mutating the object they inserted.
|
|
23
|
+
class HeaderFooterPart < Part
|
|
24
|
+
# Valid section reference types.
|
|
25
|
+
TYPES = %w[default first even].freeze
|
|
26
|
+
|
|
27
|
+
# @return [String, nil] sectPr reference type
|
|
28
|
+
# ("default"/"first"/"even")
|
|
29
|
+
attr_accessor :type
|
|
30
|
+
|
|
31
|
+
# @param kind [Symbol, nil] :header or :footer (derived from
|
|
32
|
+
# rel_type, target or content when omitted)
|
|
33
|
+
# @param type [String, nil] sectPr reference type
|
|
34
|
+
# @param loaded [Boolean] true when extracted verbatim from an
|
|
35
|
+
# existing package
|
|
36
|
+
def initialize(kind: nil, type: nil, loaded: false, **rest)
|
|
37
|
+
super(**rest)
|
|
38
|
+
@kind = kind
|
|
39
|
+
@type = type&.to_s
|
|
40
|
+
@loaded = loaded
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Symbol, nil] :header or :footer
|
|
44
|
+
def kind
|
|
45
|
+
@kind ||= derive_kind
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Boolean] true for header parts
|
|
49
|
+
def header?
|
|
50
|
+
kind == :header
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Boolean] true for footer parts
|
|
54
|
+
def footer?
|
|
55
|
+
kind == :footer
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Boolean] true when extracted from an existing package
|
|
59
|
+
# (relationships and section references are already in place)
|
|
60
|
+
def loaded?
|
|
61
|
+
!!@loaded
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Mark whether the part originates from a loaded package.
|
|
65
|
+
attr_writer :loaded
|
|
66
|
+
|
|
67
|
+
# Registry definition for this part kind.
|
|
68
|
+
#
|
|
69
|
+
# @return [Ooxml::PartDefinition, nil]
|
|
70
|
+
def definition
|
|
71
|
+
@definition ||= kind && Ooxml::PartRegistry.find_by_key(kind)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# -- Content delegation (quacks like the content model) --
|
|
75
|
+
|
|
76
|
+
# @return [Array<Wordprocessingml::Paragraph>] content paragraphs
|
|
77
|
+
def paragraphs
|
|
78
|
+
content&.paragraphs
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @return [Array<Wordprocessingml::Table>] content tables
|
|
82
|
+
def tables
|
|
83
|
+
content&.tables
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @return [Boolean] true when the content has no paragraphs/tables
|
|
87
|
+
def empty?
|
|
88
|
+
return true unless content
|
|
89
|
+
|
|
90
|
+
paragraphs.empty? && tables.empty?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Hash-style read compatibility (+:type+ in addition to the
|
|
94
|
+
# Part keys).
|
|
95
|
+
def [](key)
|
|
96
|
+
key.to_sym == :type ? type : super
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# The content model to serialize into the part file. Legacy
|
|
100
|
+
# Uniword::Header/Uniword::Footer models are converted to their
|
|
101
|
+
# Wordprocessingml counterparts (paragraph and table elements are
|
|
102
|
+
# shared, mc:Ignorable carried over); other content passes
|
|
103
|
+
# through unchanged.
|
|
104
|
+
#
|
|
105
|
+
# @return [Object] serializable content model
|
|
106
|
+
def serializable_content
|
|
107
|
+
case content
|
|
108
|
+
when Uniword::Header
|
|
109
|
+
convert_legacy(Wordprocessingml::Header.new)
|
|
110
|
+
when Uniword::Footer
|
|
111
|
+
convert_legacy(Wordprocessingml::Footer.new)
|
|
112
|
+
else
|
|
113
|
+
content
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def convert_legacy(wml)
|
|
120
|
+
wml.paragraphs.concat(content.paragraphs)
|
|
121
|
+
wml.tables.concat(content.tables)
|
|
122
|
+
wml.mc_ignorable = content.mc_ignorable if content.mc_ignorable
|
|
123
|
+
wml
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def derive_kind
|
|
127
|
+
# rel_type (".../relationships/header") and target
|
|
128
|
+
# ("header1.xml") both embed the kind word.
|
|
129
|
+
carrier = "#{rel_type} #{target}"
|
|
130
|
+
return :header if carrier.include?("header")
|
|
131
|
+
return :footer if carrier.include?("footer")
|
|
132
|
+
|
|
133
|
+
case content
|
|
134
|
+
when Wordprocessingml::Header, Uniword::Header then :header
|
|
135
|
+
when Wordprocessingml::Footer, Uniword::Footer then :footer
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Docx
|
|
5
|
+
# Ordered collection of HeaderFooterPart objects — the single
|
|
6
|
+
# storage path for all header/footer parts of a document, whether
|
|
7
|
+
# loaded from an existing DOCX or added programmatically.
|
|
8
|
+
#
|
|
9
|
+
# The +headers+/+footers+ views (HeaderFooterView) and the
|
|
10
|
+
# serializer both read from this store, so a part exists exactly
|
|
11
|
+
# once: one part file, one relationship, one content-type
|
|
12
|
+
# override, one sectPr reference.
|
|
13
|
+
class HeaderFooterPartCollection
|
|
14
|
+
include Enumerable
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@parts = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Iterate over all parts (both kinds) in insertion order.
|
|
21
|
+
def each(&block)
|
|
22
|
+
@parts.each(&block)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [Integer]
|
|
26
|
+
def size
|
|
27
|
+
@parts.size
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
alias length size
|
|
31
|
+
|
|
32
|
+
# @return [Boolean]
|
|
33
|
+
def empty?
|
|
34
|
+
@parts.empty?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Array<HeaderFooterPart>]
|
|
38
|
+
def to_a
|
|
39
|
+
@parts.dup
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Append a part.
|
|
43
|
+
#
|
|
44
|
+
# @param part [HeaderFooterPart]
|
|
45
|
+
# @return [HeaderFooterPart] the added part
|
|
46
|
+
def <<(part)
|
|
47
|
+
@parts << part
|
|
48
|
+
part
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
alias add <<
|
|
52
|
+
|
|
53
|
+
# Remove a part (by identity).
|
|
54
|
+
#
|
|
55
|
+
# @param part [HeaderFooterPart]
|
|
56
|
+
# @return [HeaderFooterPart, nil] the removed part
|
|
57
|
+
def delete(part)
|
|
58
|
+
@parts.delete(part)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param kind [Symbol] :header or :footer
|
|
62
|
+
# @return [Array<HeaderFooterPart>] parts of that kind, in order
|
|
63
|
+
def of_kind(kind)
|
|
64
|
+
@parts.select { |part| part.kind == kind }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @param kind [Symbol] :header or :footer
|
|
68
|
+
# @param type [String, nil] sectPr reference type
|
|
69
|
+
# @return [HeaderFooterPart, nil]
|
|
70
|
+
def find_part(kind, type)
|
|
71
|
+
type = type&.to_s
|
|
72
|
+
@parts.find { |part| part.kind == kind && part.type == type }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# All non-nil relationship targets in the store.
|
|
76
|
+
#
|
|
77
|
+
# @return [Array<String>]
|
|
78
|
+
def targets
|
|
79
|
+
@parts.filter_map(&:target)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# First unused numbered target for a kind
|
|
83
|
+
# ("header3.xml" when header1/header2 exist).
|
|
84
|
+
#
|
|
85
|
+
# @param kind [Symbol] :header or :footer
|
|
86
|
+
# @return [String]
|
|
87
|
+
def next_target(kind)
|
|
88
|
+
numbers = of_kind(kind).filter_map do |part|
|
|
89
|
+
part.target&.[](/\A#{kind}(\d+)\.xml\z/, 1)&.to_i
|
|
90
|
+
end
|
|
91
|
+
"#{kind}#{(numbers.max || 0) + 1}.xml"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Replace the entire store (legacy +header_footer_parts=+
|
|
95
|
+
# assignment). Accepts HeaderFooterPart objects and legacy
|
|
96
|
+
# part hashes ({ r_id:, target:, rel_type:, content_type:,
|
|
97
|
+
# content: }).
|
|
98
|
+
#
|
|
99
|
+
# @param parts [Array<HeaderFooterPart, Hash>]
|
|
100
|
+
# @return [void]
|
|
101
|
+
def replace_all(parts)
|
|
102
|
+
@parts = Array(parts).map { |part| self.class.wrap(part) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Replace all parts of one kind, leaving the other untouched.
|
|
106
|
+
#
|
|
107
|
+
# @param kind [Symbol] :header or :footer
|
|
108
|
+
# @param parts [Array<HeaderFooterPart>]
|
|
109
|
+
# @return [void]
|
|
110
|
+
def replace_kind(kind, parts)
|
|
111
|
+
@parts.reject! { |part| part.kind == kind }
|
|
112
|
+
parts.each { |part| @parts << part }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Normalize one entry into a HeaderFooterPart. Legacy hashes
|
|
116
|
+
# describe loaded parts (they carry original rIds/targets).
|
|
117
|
+
#
|
|
118
|
+
# @param value [HeaderFooterPart, Hash]
|
|
119
|
+
# @return [HeaderFooterPart]
|
|
120
|
+
def self.wrap(value)
|
|
121
|
+
return value if value.is_a?(HeaderFooterPart)
|
|
122
|
+
|
|
123
|
+
HeaderFooterPart.new(
|
|
124
|
+
r_id: value[:r_id],
|
|
125
|
+
target: value[:target],
|
|
126
|
+
rel_type: value[:rel_type],
|
|
127
|
+
content_type: value[:content_type],
|
|
128
|
+
type: value[:type],
|
|
129
|
+
content: value[:content],
|
|
130
|
+
loaded: true,
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|