uniword 1.2.5 → 1.4.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 +313 -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 +22 -14
- 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 +11 -10
- 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 +12 -18
- 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 +91 -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/image_part.rb +83 -0
- data/lib/uniword/docx/package.rb +141 -333
- data/lib/uniword/docx/package_defaults.rb +83 -99
- data/lib/uniword/docx/package_integrity_checker.rb +312 -0
- data/lib/uniword/docx/package_serialization.rb +144 -233
- data/lib/uniword/docx/part.rb +125 -0
- data/lib/uniword/docx/part_collection.rb +131 -0
- data/lib/uniword/docx/part_loader/chart_loader.rb +50 -0
- data/lib/uniword/docx/part_loader/custom_xml_loader.rb +53 -0
- data/lib/uniword/docx/part_loader/embedding_loader.rb +26 -0
- data/lib/uniword/docx/part_loader/header_footer_loader.rb +101 -0
- data/lib/uniword/docx/part_loader/image_loader.rb +105 -0
- data/lib/uniword/docx/part_loader/load_context.rb +85 -0
- data/lib/uniword/docx/part_loader/raw_part_loader.rb +183 -0
- data/lib/uniword/docx/part_loader/theme_media_loader.rb +39 -0
- data/lib/uniword/docx/part_loader/xml_model_loader.rb +56 -0
- data/lib/uniword/docx/part_loader.rb +92 -0
- data/lib/uniword/docx/raw_part.rb +75 -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 +18 -31
- data/lib/uniword/docx/reconciler/notes.rb +17 -8
- data/lib/uniword/docx/reconciler/package_structure.rb +168 -221
- data/lib/uniword/docx/reconciler/parts.rb +36 -13
- data/lib/uniword/docx/reconciler/referential_integrity.rb +222 -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 +13 -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/images/image_manager.rb +13 -13
- 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/element_order.rb +94 -0
- data/lib/uniword/ooxml/part_definition.rb +329 -0
- data/lib/uniword/ooxml/part_registry.rb +504 -0
- data/lib/uniword/ooxml/relationships/image_relationship.rb +6 -3
- 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 +3 -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 +6 -5
- data/lib/uniword/transformation/mhtml_element_renderer.rb +1 -5
- data/lib/uniword/transformation/mhtml_metadata_builder.rb +6 -6
- data/lib/uniword/validation/engine.rb +35 -0
- data/lib/uniword/validation/link_checker.rb +1 -1
- data/lib/uniword/validation/opc_validator.rb +16 -2
- 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 +46 -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 +121 -122
- data/lib/uniword/wordprocessingml/document_styling.rb +246 -0
- data/lib/uniword/wordprocessingml/endnotes.rb +4 -1
- data/lib/uniword/wordprocessingml/font_replacer.rb +147 -0
- data/lib/uniword/wordprocessingml/footnotes.rb +4 -1
- 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/numbering_elements.rb +0 -15
- 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 +5 -2
- data/lib/uniword.rb +27 -2
- metadata +52 -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
|
@@ -8,8 +8,11 @@ module Uniword
|
|
|
8
8
|
#
|
|
9
9
|
# Generated from OOXML schema: shared_types.yml
|
|
10
10
|
# Element: <st:pixel_measure>
|
|
11
|
+
#
|
|
12
|
+
# Value constrained to ST_PixelsMeasure (ECMA-376): non-negative
|
|
13
|
+
# integer; see Ooxml::Types::UnsignedDecimalNumber.
|
|
11
14
|
class PixelMeasure < Lutaml::Model::Serializable
|
|
12
|
-
attribute :val,
|
|
15
|
+
attribute :val, Uniword::Ooxml::Types::UnsignedDecimalNumber
|
|
13
16
|
|
|
14
17
|
xml do
|
|
15
18
|
element "pixel_measure"
|
|
@@ -8,8 +8,11 @@ module Uniword
|
|
|
8
8
|
#
|
|
9
9
|
# Generated from OOXML schema: shared_types.yml
|
|
10
10
|
# Element: <st:point_measure>
|
|
11
|
+
#
|
|
12
|
+
# Value constrained to ST_PointMeasure (ECMA-376): non-negative
|
|
13
|
+
# integer; see Ooxml::Types::UnsignedDecimalNumber.
|
|
11
14
|
class PointMeasure < Lutaml::Model::Serializable
|
|
12
|
-
attribute :val,
|
|
15
|
+
attribute :val, Uniword::Ooxml::Types::UnsignedDecimalNumber
|
|
13
16
|
|
|
14
17
|
xml do
|
|
15
18
|
element "point_measure"
|
|
@@ -8,8 +8,13 @@ module Uniword
|
|
|
8
8
|
#
|
|
9
9
|
# Generated from OOXML schema: shared_types.yml
|
|
10
10
|
# Element: <st:text_alignment>
|
|
11
|
+
#
|
|
12
|
+
# Value constrained to ST_TextAlignment (ECMA-376, wml.xsd).
|
|
11
13
|
class TextAlignment < Lutaml::Model::Serializable
|
|
12
|
-
|
|
14
|
+
# Full ST_TextAlignment enumeration from ECMA-376 (wml.xsd)
|
|
15
|
+
VALUES = %w[top center baseline bottom auto].freeze
|
|
16
|
+
|
|
17
|
+
attribute :val, :string, values: VALUES
|
|
13
18
|
|
|
14
19
|
xml do
|
|
15
20
|
element "text_alignment"
|
|
@@ -8,8 +8,11 @@ module Uniword
|
|
|
8
8
|
#
|
|
9
9
|
# Generated from OOXML schema: shared_types.yml
|
|
10
10
|
# Element: <st:twips_measure>
|
|
11
|
+
#
|
|
12
|
+
# Value constrained to ST_TwipsMeasure (ECMA-376): non-negative
|
|
13
|
+
# integer; see Ooxml::Types::UnsignedDecimalNumber.
|
|
11
14
|
class TwipsMeasure < Lutaml::Model::Serializable
|
|
12
|
-
attribute :val,
|
|
15
|
+
attribute :val, Uniword::Ooxml::Types::UnsignedDecimalNumber
|
|
13
16
|
|
|
14
17
|
xml do
|
|
15
18
|
element "twips_measure"
|
|
@@ -26,7 +26,7 @@ module Uniword
|
|
|
26
26
|
map_attribute "panose", to: :panose, render_nil: false
|
|
27
27
|
map_attribute "pitchFamily", to: :pitch_family, render_nil: false
|
|
28
28
|
map_attribute "charset", to: :charset, render_nil: false
|
|
29
|
-
map_attribute "altText", to: :
|
|
29
|
+
map_attribute "altText", to: :alt_text, render_nil: false
|
|
30
30
|
map_attribute "combinationId", to: :combination_id, render_nil: false
|
|
31
31
|
map_attribute "growingEditing", to: :growing_editing, render_nil: false
|
|
32
32
|
end
|
|
@@ -11,8 +11,8 @@ module Uniword
|
|
|
11
11
|
class SharedStringTable < Lutaml::Model::Serializable
|
|
12
12
|
attribute :count, :integer
|
|
13
13
|
attribute :unique_count, :integer
|
|
14
|
-
attribute :si_entries,
|
|
15
|
-
|
|
14
|
+
attribute :si_entries, StringItem, collection: true,
|
|
15
|
+
initialize_empty: true
|
|
16
16
|
|
|
17
17
|
xml do
|
|
18
18
|
element "sst"
|
|
@@ -101,22 +101,55 @@ module Uniword
|
|
|
101
101
|
|
|
102
102
|
# Navigate to a property on an object
|
|
103
103
|
#
|
|
104
|
+
# Dispatches on the object's class: Hash keys, declared attributes
|
|
105
|
+
# of Lutaml::Model objects, or public zero-arity readers on plain
|
|
106
|
+
# Ruby objects.
|
|
107
|
+
#
|
|
104
108
|
# @param object [Object] Object to navigate
|
|
105
109
|
# @param property [String] Property name
|
|
106
110
|
# @return [Object] Property value or nil
|
|
107
111
|
def navigate_property(object, property)
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
case object
|
|
113
|
+
when Hash
|
|
110
114
|
object[property.to_sym] || object[property]
|
|
115
|
+
when Lutaml::Model::Serializable
|
|
116
|
+
read_model_attribute(object, property)
|
|
111
117
|
else
|
|
112
|
-
|
|
113
|
-
# property access on arbitrary user objects. The property name
|
|
114
|
-
# comes from template syntax and may reference any public
|
|
115
|
-
# reader method on the model.
|
|
116
|
-
object.public_send(property.to_sym)
|
|
118
|
+
read_object_property(object, property)
|
|
117
119
|
end
|
|
118
120
|
end
|
|
119
121
|
|
|
122
|
+
# Read a declared attribute from a Lutaml::Model object
|
|
123
|
+
#
|
|
124
|
+
# Only attributes declared in the model's schema are readable from
|
|
125
|
+
# templates; arbitrary public methods are not exposed. The schema
|
|
126
|
+
# acts as the allowlist, the model's public method table provides
|
|
127
|
+
# the value.
|
|
128
|
+
#
|
|
129
|
+
# @param object [Lutaml::Model::Serializable] Model instance
|
|
130
|
+
# @param property [String] Property name
|
|
131
|
+
# @return [Object] Attribute value or nil when not declared
|
|
132
|
+
def read_model_attribute(object, property)
|
|
133
|
+
name = property.to_sym
|
|
134
|
+
return unless object.class.attributes.key?(name)
|
|
135
|
+
|
|
136
|
+
object.method(name).call
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Read a property from a plain Ruby object
|
|
140
|
+
#
|
|
141
|
+
# Looks the property up in the object's public method table.
|
|
142
|
+
# Returns nil when the object has no such public method.
|
|
143
|
+
#
|
|
144
|
+
# @param object [Object] Object to read from
|
|
145
|
+
# @param property [String] Property name
|
|
146
|
+
# @return [Object] Property value or nil
|
|
147
|
+
def read_object_property(object, property)
|
|
148
|
+
object.method(property.to_sym).call
|
|
149
|
+
rescue NameError
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
120
153
|
# Parse literal value from string
|
|
121
154
|
#
|
|
122
155
|
# @param value [String] Literal value string
|
|
@@ -52,34 +52,10 @@ module Uniword
|
|
|
52
52
|
)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
OFFICE_THEME_PATH = File.expand_path(
|
|
58
|
-
"../../../data/themes/office_theme.xml", __dir__
|
|
59
|
-
).freeze
|
|
60
|
-
|
|
61
|
-
def default_format_scheme
|
|
62
|
-
@default_format_scheme ||= begin
|
|
63
|
-
theme = Drawingml::Theme.from_xml(File.read(OFFICE_THEME_PATH))
|
|
64
|
-
theme.theme_elements.fmt_scheme
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Build OOXML ThemeElements from friendly theme
|
|
55
|
+
# Build OOXML ColorScheme from friendly color scheme.
|
|
69
56
|
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
def build_theme_elements(friendly)
|
|
73
|
-
return nil unless friendly
|
|
74
|
-
|
|
75
|
-
Drawingml::ThemeElements.new(
|
|
76
|
-
clr_scheme: build_color_scheme(friendly.color_scheme),
|
|
77
|
-
font_scheme: build_font_scheme(friendly.font_scheme),
|
|
78
|
-
fmt_scheme: default_format_scheme,
|
|
79
|
-
)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Build OOXML ColorScheme from friendly color scheme
|
|
57
|
+
# Public converter, used by partial theme edits (Design → Colors
|
|
58
|
+
# style scheme swaps that keep fonts and formats untouched).
|
|
83
59
|
#
|
|
84
60
|
# @param friendly_colors [Themes::ColorScheme, nil] Friendly color scheme
|
|
85
61
|
# @return [Drawingml::ColorScheme, nil] OOXML color scheme
|
|
@@ -103,6 +79,58 @@ module Uniword
|
|
|
103
79
|
)
|
|
104
80
|
end
|
|
105
81
|
|
|
82
|
+
# Build OOXML FontScheme from friendly font scheme.
|
|
83
|
+
#
|
|
84
|
+
# Public converter, used by partial theme edits (Design → Fonts
|
|
85
|
+
# style scheme swaps that keep colors and formats untouched).
|
|
86
|
+
#
|
|
87
|
+
# @param friendly_fonts [Themes::FontScheme, nil] Friendly font scheme
|
|
88
|
+
# @return [Drawingml::FontScheme, nil] OOXML font scheme
|
|
89
|
+
def build_font_scheme(friendly_fonts)
|
|
90
|
+
return nil unless friendly_fonts
|
|
91
|
+
|
|
92
|
+
Drawingml::FontScheme.new(
|
|
93
|
+
name: friendly_fonts.name,
|
|
94
|
+
major_font_obj: build_major_font(friendly_fonts),
|
|
95
|
+
minor_font_obj: build_minor_font(friendly_fonts),
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A fresh parse of the bundled default Office theme.
|
|
100
|
+
#
|
|
101
|
+
# Each call re-parses the bundled XML so every returned theme (and
|
|
102
|
+
# its elements) is independent — callers may mutate it freely.
|
|
103
|
+
#
|
|
104
|
+
# @return [Drawingml::Theme] Default Office theme
|
|
105
|
+
def default_office_theme
|
|
106
|
+
Drawingml::Theme.from_xml(File.read(OFFICE_THEME_PATH))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
OFFICE_THEME_PATH = File.expand_path(
|
|
112
|
+
"../../../data/themes/office_theme.xml", __dir__
|
|
113
|
+
).freeze
|
|
114
|
+
|
|
115
|
+
def default_format_scheme
|
|
116
|
+
@default_format_scheme ||=
|
|
117
|
+
default_office_theme.theme_elements.fmt_scheme
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Build OOXML ThemeElements from friendly theme
|
|
121
|
+
#
|
|
122
|
+
# @param friendly [Themes::Theme] Friendly theme
|
|
123
|
+
# @return [Drawingml::ThemeElements] OOXML theme elements
|
|
124
|
+
def build_theme_elements(friendly)
|
|
125
|
+
return nil unless friendly
|
|
126
|
+
|
|
127
|
+
Drawingml::ThemeElements.new(
|
|
128
|
+
clr_scheme: build_color_scheme(friendly.color_scheme),
|
|
129
|
+
font_scheme: build_font_scheme(friendly.font_scheme),
|
|
130
|
+
fmt_scheme: default_format_scheme,
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
106
134
|
# Build OOXML color reference from hex value
|
|
107
135
|
#
|
|
108
136
|
# Creates the appropriate color type (Dk1Color, Lt1Color, etc.)
|
|
@@ -118,20 +146,6 @@ module Uniword
|
|
|
118
146
|
color_class.new.tap { |c| c.rgb = hex }
|
|
119
147
|
end
|
|
120
148
|
|
|
121
|
-
# Build OOXML FontScheme from friendly font scheme
|
|
122
|
-
#
|
|
123
|
-
# @param friendly_fonts [Themes::FontScheme, nil] Friendly font scheme
|
|
124
|
-
# @return [Drawingml::FontScheme, nil] OOXML font scheme
|
|
125
|
-
def build_font_scheme(friendly_fonts)
|
|
126
|
-
return nil unless friendly_fonts
|
|
127
|
-
|
|
128
|
-
Drawingml::FontScheme.new(
|
|
129
|
-
name: friendly_fonts.name,
|
|
130
|
-
major_font_obj: build_major_font(friendly_fonts),
|
|
131
|
-
minor_font_obj: build_minor_font(friendly_fonts),
|
|
132
|
-
)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
149
|
# Build OOXML MajorFont from friendly font scheme
|
|
136
150
|
#
|
|
137
151
|
# @param friendly [Themes::FontScheme] Friendly font scheme
|
|
@@ -92,11 +92,12 @@ module Uniword
|
|
|
92
92
|
return unless body.element_order
|
|
93
93
|
|
|
94
94
|
insert_element = Lutaml::Xml::Element.new("Element", "sdt")
|
|
95
|
-
if position.positive? && body.element_order.size >= position
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
index = if position.positive? && body.element_order.size >= position
|
|
96
|
+
position
|
|
97
|
+
else
|
|
98
|
+
0
|
|
99
|
+
end
|
|
100
|
+
Ooxml::ElementOrder.insert_at(body, index, insert_element)
|
|
100
101
|
end
|
|
101
102
|
|
|
102
103
|
# Update an existing TOC in the document.
|
|
@@ -219,11 +219,7 @@ module Uniword
|
|
|
219
219
|
def resolve_image_target(embed_id)
|
|
220
220
|
return nil unless @image_parts
|
|
221
221
|
|
|
222
|
-
|
|
223
|
-
return nil unless entry
|
|
224
|
-
|
|
225
|
-
image_data = entry[1]
|
|
226
|
-
image_data[:target]
|
|
222
|
+
@image_parts[embed_id]&.target
|
|
227
223
|
end
|
|
228
224
|
|
|
229
225
|
# Apply run formatting (text must already be HTML-escaped)
|
|
@@ -63,8 +63,8 @@ module Uniword
|
|
|
63
63
|
# Build filelist.xml part
|
|
64
64
|
def build_filelist_part
|
|
65
65
|
image_entries = if @document.image_parts && !@document.image_parts.empty?
|
|
66
|
-
@document.image_parts.map do |_r_id,
|
|
67
|
-
%(<o:File HRef="#{
|
|
66
|
+
@document.image_parts.map do |_r_id, part|
|
|
67
|
+
%(<o:File HRef="#{part.target}"/>)
|
|
68
68
|
end.join("\n ")
|
|
69
69
|
else
|
|
70
70
|
""
|
|
@@ -91,12 +91,12 @@ module Uniword
|
|
|
91
91
|
def build_image_parts
|
|
92
92
|
return [] unless @document.image_parts && !@document.image_parts.empty?
|
|
93
93
|
|
|
94
|
-
@document.image_parts.map do |_r_id,
|
|
94
|
+
@document.image_parts.map do |_r_id, image_part|
|
|
95
95
|
part = Uniword::Mhtml::ImagePart.new
|
|
96
|
-
part.content_type =
|
|
96
|
+
part.content_type = image_part.content_type || "image/png"
|
|
97
97
|
part.content_transfer_encoding = "base64"
|
|
98
|
-
part.raw_content =
|
|
99
|
-
part.content_location = "file:///C:/D057922B/#{document_name}.fld/#{
|
|
98
|
+
part.raw_content = image_part.data
|
|
99
|
+
part.content_location = "file:///C:/D057922B/#{document_name}.fld/#{image_part.target}"
|
|
100
100
|
part
|
|
101
101
|
end
|
|
102
102
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Validation
|
|
5
|
+
# The single validation engine.
|
|
6
|
+
#
|
|
7
|
+
# Runs the rules registered in Rules::Registry against a validation
|
|
8
|
+
# context and returns Report::ValidationIssue results. One engine, two
|
|
9
|
+
# front-ends:
|
|
10
|
+
#
|
|
11
|
+
# - On-disk (post-save): Rules::DocumentContext wraps the DOCX package;
|
|
12
|
+
# driven by Validators::DocumentSemanticsValidator (verify layer 3).
|
|
13
|
+
# - In-memory (pre-save): Rules::ModelContext wraps the document model;
|
|
14
|
+
# driven by Wordprocessingml::DocumentRoot#valid? and CLI `validate`.
|
|
15
|
+
#
|
|
16
|
+
# Rules declare which context they consume via Rules::Base#context_type
|
|
17
|
+
# (:package or :model); only matching rules run.
|
|
18
|
+
module Engine
|
|
19
|
+
# Run every rule matching the context's type.
|
|
20
|
+
#
|
|
21
|
+
# @param context [Rules::DocumentContext, Rules::ModelContext]
|
|
22
|
+
# validation context; its #context_type selects the rule set
|
|
23
|
+
# @param rules [Array<Rules::Base>] candidate rules
|
|
24
|
+
# @return [Array<Report::ValidationIssue>] issues found
|
|
25
|
+
def self.run(context, rules: Rules::Registry.all)
|
|
26
|
+
rules.each_with_object([]) do |rule, issues|
|
|
27
|
+
next unless rule.context_type == context.context_type
|
|
28
|
+
next unless rule.applicable?(context)
|
|
29
|
+
|
|
30
|
+
issues.concat(rule.check(context))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "zip"
|
|
4
4
|
require "nokogiri"
|
|
5
|
-
require_relative "report/validation_issue"
|
|
6
5
|
|
|
7
6
|
module Uniword
|
|
8
7
|
module Validation
|
|
@@ -222,7 +221,22 @@ module Uniword
|
|
|
222
221
|
def resolve_target(base_dir, target)
|
|
223
222
|
return target[1..] if target.start_with?("/")
|
|
224
223
|
|
|
225
|
-
|
|
224
|
+
normalize_package_path(
|
|
225
|
+
base_dir.empty? ? target : File.join(base_dir, target),
|
|
226
|
+
)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Lexically normalize a package-relative path (resolve "." and
|
|
230
|
+
# ".." segments). Deliberately avoids File.expand_path, which
|
|
231
|
+
# prepends a drive letter on Windows.
|
|
232
|
+
def normalize_package_path(path)
|
|
233
|
+
segments = []
|
|
234
|
+
path.split("/").each do |segment|
|
|
235
|
+
next if segment.empty? || segment == "."
|
|
236
|
+
|
|
237
|
+
segment == ".." ? segments.pop : segments << segment
|
|
238
|
+
end
|
|
239
|
+
segments.join("/")
|
|
226
240
|
end
|
|
227
241
|
end
|
|
228
242
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Validation
|
|
5
|
+
# Reporting value objects for validation and verification results.
|
|
6
|
+
#
|
|
7
|
+
# Registered here via autoload so any namespace can reference the
|
|
8
|
+
# report classes without load-order coupling.
|
|
9
|
+
module Report
|
|
10
|
+
autoload :ValidationIssue,
|
|
11
|
+
"uniword/validation/report/validation_issue"
|
|
12
|
+
autoload :LayerResult,
|
|
13
|
+
"uniword/validation/report/layer_result"
|
|
14
|
+
autoload :VerificationReport,
|
|
15
|
+
"uniword/validation/report/verification_report"
|
|
16
|
+
autoload :TerminalFormatter,
|
|
17
|
+
"uniword/validation/report/terminal_formatter"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../report/validation_issue"
|
|
4
|
-
|
|
5
3
|
module Uniword
|
|
6
4
|
module Validation
|
|
7
5
|
module Rules
|
|
@@ -64,6 +62,16 @@ module Uniword
|
|
|
64
62
|
"error"
|
|
65
63
|
end
|
|
66
64
|
|
|
65
|
+
# The context type this rule consumes. The Engine runs a rule
|
|
66
|
+
# only when the context's type matches: :package rules validate
|
|
67
|
+
# an on-disk DOCX via DocumentContext; :model rules validate an
|
|
68
|
+
# in-memory document model via ModelContext (see ModelRule).
|
|
69
|
+
#
|
|
70
|
+
# @return [Symbol] :package or :model
|
|
71
|
+
def context_type
|
|
72
|
+
:package
|
|
73
|
+
end
|
|
74
|
+
|
|
67
75
|
# Check if this rule applies to the given document context.
|
|
68
76
|
#
|
|
69
77
|
# @param context [DocumentContext] The document being validated
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
# DOC-201: every bookmarkStart must have a matching bookmarkEnd.
|
|
7
|
+
#
|
|
8
|
+
# Unpaired bookmark starts produce corrupt documents that Word
|
|
9
|
+
# refuses to open.
|
|
10
|
+
class BookmarkPairingRule < ModelRule
|
|
11
|
+
def code = "DOC-201"
|
|
12
|
+
def category = :bookmarks
|
|
13
|
+
def severity = "error"
|
|
14
|
+
|
|
15
|
+
def check(context)
|
|
16
|
+
body = context.document.body
|
|
17
|
+
return [] unless body
|
|
18
|
+
|
|
19
|
+
end_ids = (body.bookmark_ends || []).filter_map(&:id).to_set
|
|
20
|
+
(body.bookmark_starts || []).filter_map do |start|
|
|
21
|
+
unpaired_issue(start, end_ids)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# @return [Report::ValidationIssue, nil] issue when unpaired
|
|
28
|
+
def unpaired_issue(start, end_ids)
|
|
29
|
+
return if end_ids.include?(start.id)
|
|
30
|
+
|
|
31
|
+
issue("bookmarkStart id='#{start.id}' (name='#{start.name}') " \
|
|
32
|
+
"has no matching bookmarkEnd",
|
|
33
|
+
part: "word/document.xml")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
# DOC-202: bookmark names should be unique.
|
|
7
|
+
#
|
|
8
|
+
# Duplicate names confuse cross-references and GoTo navigation.
|
|
9
|
+
# The Word-internal "_GoBack" bookmark is exempt.
|
|
10
|
+
class BookmarkUniquenessRule < ModelRule
|
|
11
|
+
def code = "DOC-202"
|
|
12
|
+
def category = :bookmarks
|
|
13
|
+
def severity = "warning"
|
|
14
|
+
|
|
15
|
+
def check(context)
|
|
16
|
+
body = context.document.body
|
|
17
|
+
return [] unless body
|
|
18
|
+
|
|
19
|
+
duplicates(body.bookmark_starts || []).map do |name|
|
|
20
|
+
issue("Duplicate bookmark name '#{name}'",
|
|
21
|
+
part: "word/document.xml")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# @param starts [Array] bookmarkStart models
|
|
28
|
+
# @return [Array<String>] names seen more than once
|
|
29
|
+
def duplicates(starts)
|
|
30
|
+
seen = {}
|
|
31
|
+
starts.each_with_object([]) do |start, dups|
|
|
32
|
+
name = tracked_name(start)
|
|
33
|
+
next unless name
|
|
34
|
+
|
|
35
|
+
dups << name if seen[name]
|
|
36
|
+
seen[name] = true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [String, nil] name worth tracking, nil when exempt
|
|
41
|
+
def tracked_name(start)
|
|
42
|
+
name = start.name.to_s
|
|
43
|
+
name unless name.empty? || name == "_GoBack"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
# DOC-200: the document must have a body.
|
|
7
|
+
#
|
|
8
|
+
# A w:document without w:body carries no content; every document
|
|
9
|
+
# must contain exactly one body element.
|
|
10
|
+
class DocumentBodyRule < ModelRule
|
|
11
|
+
def code = "DOC-200"
|
|
12
|
+
def category = :structure
|
|
13
|
+
def severity = "error"
|
|
14
|
+
|
|
15
|
+
def check(context)
|
|
16
|
+
return [] if context.document.body
|
|
17
|
+
|
|
18
|
+
[issue("Document body is missing",
|
|
19
|
+
part: "word/document.xml",
|
|
20
|
+
suggestion: "Add a w:body element to the document.")]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -32,6 +32,13 @@ module Uniword
|
|
|
32
32
|
@moxml = Moxml.new(:nokogiri)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# Context type used by the Engine to select rules.
|
|
36
|
+
#
|
|
37
|
+
# @return [Symbol] :package — this context wraps an on-disk DOCX
|
|
38
|
+
def context_type
|
|
39
|
+
:package
|
|
40
|
+
end
|
|
41
|
+
|
|
35
42
|
# Open the ZIP archive.
|
|
36
43
|
#
|
|
37
44
|
# @return [Zip::File]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
# DOC-203: paragraphs should not be empty.
|
|
7
|
+
#
|
|
8
|
+
# Empty paragraphs are usually leftover scaffolding; reported as
|
|
9
|
+
# warnings only.
|
|
10
|
+
class EmptyParagraphsRule < ModelRule
|
|
11
|
+
def code = "DOC-203"
|
|
12
|
+
def category = :structure
|
|
13
|
+
def severity = "warning"
|
|
14
|
+
|
|
15
|
+
def check(context)
|
|
16
|
+
paragraphs = context.document.paragraphs
|
|
17
|
+
paragraphs.each_with_index.filter_map do |para, idx|
|
|
18
|
+
next unless para.runs.nil? || para.runs.empty?
|
|
19
|
+
|
|
20
|
+
issue("Empty paragraph at index #{idx}",
|
|
21
|
+
part: "word/document.xml")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|