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
|
@@ -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
|
|
@@ -91,6 +91,8 @@ module Uniword
|
|
|
91
91
|
# Add the SDT to element_order so it serializes correctly
|
|
92
92
|
return unless body.element_order
|
|
93
93
|
|
|
94
|
+
# element_order from the parser may be frozen; never mutate it.
|
|
95
|
+
body.element_order = body.element_order.dup
|
|
94
96
|
insert_element = Lutaml::Xml::Element.new("Element", "sdt")
|
|
95
97
|
if position.positive? && body.element_order.size >= position
|
|
96
98
|
body.element_order.insert(position, insert_element)
|
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
# In-memory validation context: wraps a document model.
|
|
7
|
+
#
|
|
8
|
+
# Counterpart to DocumentContext (which wraps an on-disk DOCX
|
|
9
|
+
# package). Model-level rules consume this context via the Engine.
|
|
10
|
+
#
|
|
11
|
+
# @example Validate a built document
|
|
12
|
+
# context = ModelContext.new(document)
|
|
13
|
+
# issues = Validation::Engine.run(context)
|
|
14
|
+
class ModelContext
|
|
15
|
+
# @return [Wordprocessingml::DocumentRoot] the document model
|
|
16
|
+
attr_reader :document
|
|
17
|
+
|
|
18
|
+
# @param document [Wordprocessingml::DocumentRoot] in-memory model
|
|
19
|
+
def initialize(document)
|
|
20
|
+
@document = document
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Context type used by the Engine to select rules.
|
|
24
|
+
#
|
|
25
|
+
# @return [Symbol] :model — this context wraps an in-memory model
|
|
26
|
+
def context_type
|
|
27
|
+
:model
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Validation
|
|
5
|
+
module Rules
|
|
6
|
+
# Base class for in-memory (model-level) validation rules.
|
|
7
|
+
#
|
|
8
|
+
# Model rules run against a Rules::ModelContext — the pre-save
|
|
9
|
+
# front-end of the validation engine — and inspect the document
|
|
10
|
+
# model directly instead of parsing package XML.
|
|
11
|
+
#
|
|
12
|
+
# @example Implement a model rule
|
|
13
|
+
# class MyModelRule < ModelRule
|
|
14
|
+
# def code = "DOC-900"
|
|
15
|
+
#
|
|
16
|
+
# def check(context)
|
|
17
|
+
# return [] if context.document.body
|
|
18
|
+
#
|
|
19
|
+
# [issue("Document body is missing")]
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# Rules::Registry.register(MyModelRule)
|
|
24
|
+
class ModelRule < Base
|
|
25
|
+
# Model rules consume the in-memory ModelContext.
|
|
26
|
+
#
|
|
27
|
+
# @return [Symbol] :model
|
|
28
|
+
def context_type
|
|
29
|
+
:model
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|