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
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
module Ooxml
|
|
7
|
+
# Single source of truth for OOXML package part metadata.
|
|
8
|
+
#
|
|
9
|
+
# Maps every part kind the library reads or writes to its package
|
|
10
|
+
# path, content type, relationship type, [Content_Types].xml entry
|
|
11
|
+
# form (Default vs Override), loader strategy, and document/package
|
|
12
|
+
# attributes. Content type and relationship type literals live ONLY
|
|
13
|
+
# here; consumers (Uniword::ContentTypes, Docx::PackageDefaults,
|
|
14
|
+
# Docx::Reconciler::PackageStructure, Docx::PartLoader,
|
|
15
|
+
# DocumentFactory, and the Docx::PackageSerialization inject_*
|
|
16
|
+
# methods) derive from this registry instead of holding their own
|
|
17
|
+
# literals.
|
|
18
|
+
#
|
|
19
|
+
# Open/closed: a new part kind is added by registering a
|
|
20
|
+
# PartDefinition — consumers need no further changes.
|
|
21
|
+
#
|
|
22
|
+
# @example Look up a part by key
|
|
23
|
+
# defn = PartRegistry.find_by_key(:styles)
|
|
24
|
+
# defn.part_name # => "/word/styles.xml"
|
|
25
|
+
# defn.rel_type # => ".../relationships/styles"
|
|
26
|
+
#
|
|
27
|
+
# @example Register a custom part kind
|
|
28
|
+
# PartRegistry.register(
|
|
29
|
+
# PartDefinition.new(key: :glossary, path: "word/glossary/document.xml",
|
|
30
|
+
# target: "glossary/document.xml",
|
|
31
|
+
# content_type: "...document.glossary+xml",
|
|
32
|
+
# rel_type: ".../glossaryDocument",
|
|
33
|
+
# kind: :override, rels_scope: :document)
|
|
34
|
+
# )
|
|
35
|
+
module PartRegistry
|
|
36
|
+
# Relationship type base URIs. These literals appear only here.
|
|
37
|
+
OFFICE_REL_BASE =
|
|
38
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
39
|
+
PACKAGE_REL_BASE =
|
|
40
|
+
"http://schemas.openxmlformats.org/package/2006/relationships"
|
|
41
|
+
|
|
42
|
+
# Content type prefixes (literals appear only here).
|
|
43
|
+
CT_OFFICE = "application/vnd.openxmlformats-officedocument"
|
|
44
|
+
CT_WML = "#{CT_OFFICE}.wordprocessingml".freeze
|
|
45
|
+
CT_PACKAGE = "application/vnd.openxmlformats-package"
|
|
46
|
+
|
|
47
|
+
# Built-in registrations. Registration order is observable in
|
|
48
|
+
# emitted output: the :standard entries reproduce the historic
|
|
49
|
+
# ContentTypes.generate ordering exactly (defaults first —
|
|
50
|
+
# jpeg, png, gif, rels, xml — then overrides in the historic
|
|
51
|
+
# order). Non-standard parts follow; consumers select their own
|
|
52
|
+
# ordered subsets by key.
|
|
53
|
+
#
|
|
54
|
+
# Loader metadata: +loader+ selects the Docx::PartLoader
|
|
55
|
+
# strategy, +loader_model+ the parsing model class,
|
|
56
|
+
# +load_priority+ orders the load (content types and package
|
|
57
|
+
# rels first; the main document before the parts its rels
|
|
58
|
+
# describe), and +document_attribute+/+package_attribute+ name
|
|
59
|
+
# where the part lives on Wordprocessingml::DocumentRoot and
|
|
60
|
+
# Docx::Package for the two registry-driven copy loops.
|
|
61
|
+
BUILT_INS = [
|
|
62
|
+
# -- Content-type Defaults (extension → content type) --
|
|
63
|
+
{ key: :jpeg, kind: :default, extension: "jpeg",
|
|
64
|
+
content_type: "image/jpeg", standard: true },
|
|
65
|
+
{ key: :png, kind: :default, extension: "png",
|
|
66
|
+
content_type: "image/png", standard: true },
|
|
67
|
+
{ key: :gif, kind: :default, extension: "gif",
|
|
68
|
+
content_type: "image/gif", standard: true },
|
|
69
|
+
{ key: :rels, kind: :default, extension: "rels",
|
|
70
|
+
content_type: "#{CT_PACKAGE}.relationships+xml",
|
|
71
|
+
required: true, standard: true },
|
|
72
|
+
{ key: :xml, kind: :default, extension: "xml",
|
|
73
|
+
content_type: "application/xml",
|
|
74
|
+
required: true, standard: true },
|
|
75
|
+
|
|
76
|
+
# -- Standard Override parts (historic generate order) --
|
|
77
|
+
{ key: :document, kind: :override, path: "word/document.xml",
|
|
78
|
+
content_type: "#{CT_WML}.document.main+xml",
|
|
79
|
+
rel_type: "#{OFFICE_REL_BASE}/officeDocument",
|
|
80
|
+
required: true, standard: true, rels_scope: :package,
|
|
81
|
+
loader: :xml_model,
|
|
82
|
+
loader_model: Uniword::Wordprocessingml::DocumentRoot,
|
|
83
|
+
path_resolution: :office_document, load_priority: 40,
|
|
84
|
+
package_attribute: :document },
|
|
85
|
+
{ key: :numbering, kind: :override, path: "word/numbering.xml",
|
|
86
|
+
target: "numbering.xml", content_type: "#{CT_WML}.numbering+xml",
|
|
87
|
+
rel_type: "#{OFFICE_REL_BASE}/numbering",
|
|
88
|
+
standard: true, rels_scope: :document,
|
|
89
|
+
loader: :xml_model,
|
|
90
|
+
loader_model: Uniword::Wordprocessingml::NumberingConfiguration,
|
|
91
|
+
load_priority: 50,
|
|
92
|
+
package_attribute: :numbering,
|
|
93
|
+
document_attribute: :numbering_configuration,
|
|
94
|
+
to_package_guard: :numbering_configuration_loaded? },
|
|
95
|
+
{ key: :styles, kind: :override, path: "word/styles.xml",
|
|
96
|
+
target: "styles.xml", content_type: "#{CT_WML}.styles+xml",
|
|
97
|
+
rel_type: "#{OFFICE_REL_BASE}/styles",
|
|
98
|
+
required: true, standard: true, rels_scope: :document,
|
|
99
|
+
loader: :xml_model,
|
|
100
|
+
loader_model: Uniword::Wordprocessingml::StylesConfiguration,
|
|
101
|
+
load_priority: 50,
|
|
102
|
+
package_attribute: :styles,
|
|
103
|
+
document_attribute: :styles_configuration },
|
|
104
|
+
{ key: :settings, kind: :override, path: "word/settings.xml",
|
|
105
|
+
target: "settings.xml", content_type: "#{CT_WML}.settings+xml",
|
|
106
|
+
rel_type: "#{OFFICE_REL_BASE}/settings",
|
|
107
|
+
required: true, standard: true, rels_scope: :document,
|
|
108
|
+
loader: :xml_model,
|
|
109
|
+
loader_model: Uniword::Wordprocessingml::Settings,
|
|
110
|
+
load_priority: 50,
|
|
111
|
+
package_attribute: :settings, document_attribute: :settings },
|
|
112
|
+
{ key: :web_settings, kind: :override, path: "word/webSettings.xml",
|
|
113
|
+
target: "webSettings.xml",
|
|
114
|
+
content_type: "#{CT_WML}.webSettings+xml",
|
|
115
|
+
rel_type: "#{OFFICE_REL_BASE}/webSettings",
|
|
116
|
+
required: true, standard: true, rels_scope: :document,
|
|
117
|
+
loader: :xml_model,
|
|
118
|
+
loader_model: Uniword::Wordprocessingml::WebSettings,
|
|
119
|
+
load_priority: 50,
|
|
120
|
+
package_attribute: :web_settings,
|
|
121
|
+
document_attribute: :web_settings },
|
|
122
|
+
{ key: :font_table, kind: :override, path: "word/fontTable.xml",
|
|
123
|
+
target: "fontTable.xml",
|
|
124
|
+
content_type: "#{CT_WML}.fontTable+xml",
|
|
125
|
+
rel_type: "#{OFFICE_REL_BASE}/fontTable",
|
|
126
|
+
required: true, standard: true, rels_scope: :document,
|
|
127
|
+
loader: :xml_model,
|
|
128
|
+
loader_model: Uniword::Wordprocessingml::FontTable,
|
|
129
|
+
load_priority: 50,
|
|
130
|
+
package_attribute: :font_table,
|
|
131
|
+
document_attribute: :font_table },
|
|
132
|
+
{ key: :theme, kind: :override, path: "word/theme/theme1.xml",
|
|
133
|
+
target: "theme/theme1.xml",
|
|
134
|
+
content_type: "#{CT_OFFICE}.theme+xml",
|
|
135
|
+
rel_type: "#{OFFICE_REL_BASE}/theme",
|
|
136
|
+
standard: true, rels_scope: :document,
|
|
137
|
+
loader: :xml_model, loader_model: Uniword::Drawingml::Theme,
|
|
138
|
+
load_priority: 70,
|
|
139
|
+
package_attribute: :theme, document_attribute: :theme },
|
|
140
|
+
{ key: :core_properties, kind: :override, path: "docProps/core.xml",
|
|
141
|
+
content_type: "#{CT_PACKAGE}.core-properties+xml",
|
|
142
|
+
rel_type: "#{PACKAGE_REL_BASE}/metadata/core-properties",
|
|
143
|
+
required: true, standard: true, rels_scope: :package,
|
|
144
|
+
loader: :xml_model, loader_model: CoreProperties,
|
|
145
|
+
load_priority: 30,
|
|
146
|
+
package_attribute: :core_properties,
|
|
147
|
+
document_attribute: :core_properties },
|
|
148
|
+
{ key: :app_properties, kind: :override, path: "docProps/app.xml",
|
|
149
|
+
content_type: "#{CT_OFFICE}.extended-properties+xml",
|
|
150
|
+
rel_type: "#{OFFICE_REL_BASE}/extended-properties",
|
|
151
|
+
required: true, standard: true, rels_scope: :package,
|
|
152
|
+
loader: :xml_model, loader_model: AppProperties,
|
|
153
|
+
load_priority: 30,
|
|
154
|
+
package_attribute: :app_properties,
|
|
155
|
+
document_attribute: :app_properties },
|
|
156
|
+
|
|
157
|
+
# -- Optional document parts --
|
|
158
|
+
{ key: :footnotes, kind: :override, path: "word/footnotes.xml",
|
|
159
|
+
target: "footnotes.xml",
|
|
160
|
+
content_type: "#{CT_WML}.footnotes+xml",
|
|
161
|
+
rel_type: "#{OFFICE_REL_BASE}/footnotes",
|
|
162
|
+
rels_scope: :document,
|
|
163
|
+
loader: :xml_model,
|
|
164
|
+
loader_model: Uniword::Wordprocessingml::Footnotes,
|
|
165
|
+
load_priority: 80,
|
|
166
|
+
package_attribute: :footnotes, document_attribute: :footnotes },
|
|
167
|
+
{ key: :endnotes, kind: :override, path: "word/endnotes.xml",
|
|
168
|
+
target: "endnotes.xml",
|
|
169
|
+
content_type: "#{CT_WML}.endnotes+xml",
|
|
170
|
+
rel_type: "#{OFFICE_REL_BASE}/endnotes",
|
|
171
|
+
rels_scope: :document,
|
|
172
|
+
loader: :xml_model,
|
|
173
|
+
loader_model: Uniword::Wordprocessingml::Endnotes,
|
|
174
|
+
load_priority: 80,
|
|
175
|
+
package_attribute: :endnotes, document_attribute: :endnotes },
|
|
176
|
+
{ key: :comments, kind: :override, path: "word/comments.xml",
|
|
177
|
+
target: "comments.xml",
|
|
178
|
+
content_type: "#{CT_WML}.comments+xml",
|
|
179
|
+
rel_type: "#{OFFICE_REL_BASE}/comments",
|
|
180
|
+
rels_scope: :document,
|
|
181
|
+
loader: :xml_model, loader_model: Uniword::CommentsPart,
|
|
182
|
+
load_priority: 80,
|
|
183
|
+
package_attribute: :comments, document_attribute: :comments,
|
|
184
|
+
to_package_type: Uniword::CommentsPart },
|
|
185
|
+
# Bibliography sources live only on the document (single home:
|
|
186
|
+
# DocumentRoot#bibliography_sources); nothing reads
|
|
187
|
+
# word/sources.xml back, so there is no loader.
|
|
188
|
+
{ key: :bibliography, kind: :override, path: "word/sources.xml",
|
|
189
|
+
target: "sources.xml",
|
|
190
|
+
content_type: "#{CT_OFFICE}.bibliography+xml",
|
|
191
|
+
rel_type: "#{OFFICE_REL_BASE}/bibliography",
|
|
192
|
+
rels_scope: :document,
|
|
193
|
+
document_attribute: :bibliography_sources },
|
|
194
|
+
{ key: :header, kind: :override,
|
|
195
|
+
path_pattern: "word/header%<counter>d.xml",
|
|
196
|
+
target_pattern: "header%<counter>d.xml",
|
|
197
|
+
content_type: "#{CT_WML}.header+xml",
|
|
198
|
+
rel_type: "#{OFFICE_REL_BASE}/header",
|
|
199
|
+
rels_scope: :document,
|
|
200
|
+
loader: :header_footer,
|
|
201
|
+
loader_model: Uniword::Wordprocessingml::Header,
|
|
202
|
+
load_priority: 90,
|
|
203
|
+
document_attribute: :header_footer_parts },
|
|
204
|
+
{ key: :footer, kind: :override,
|
|
205
|
+
path_pattern: "word/footer%<counter>d.xml",
|
|
206
|
+
target_pattern: "footer%<counter>d.xml",
|
|
207
|
+
content_type: "#{CT_WML}.footer+xml",
|
|
208
|
+
rel_type: "#{OFFICE_REL_BASE}/footer",
|
|
209
|
+
rels_scope: :document,
|
|
210
|
+
loader: :header_footer,
|
|
211
|
+
loader_model: Uniword::Wordprocessingml::Footer,
|
|
212
|
+
load_priority: 90,
|
|
213
|
+
document_attribute: :header_footer_parts },
|
|
214
|
+
{ key: :custom_properties, kind: :override,
|
|
215
|
+
path: "docProps/custom.xml",
|
|
216
|
+
content_type: "#{CT_OFFICE}.custom-properties+xml",
|
|
217
|
+
rel_type: "#{OFFICE_REL_BASE}/custom-properties",
|
|
218
|
+
rels_scope: :package,
|
|
219
|
+
loader: :xml_model, loader_model: CustomProperties,
|
|
220
|
+
load_priority: 30,
|
|
221
|
+
package_attribute: :custom_properties,
|
|
222
|
+
document_attribute: :custom_properties },
|
|
223
|
+
# The chart loader keys parts by document relationship id, so
|
|
224
|
+
# they land directly on the document — the single home for
|
|
225
|
+
# chart parts (DocumentRoot#chart_parts).
|
|
226
|
+
{ key: :chart, kind: :override,
|
|
227
|
+
path_pattern: "word/charts/chart%<n>d.xml",
|
|
228
|
+
target_pattern: "charts/chart%<n>d.xml",
|
|
229
|
+
content_type: "#{CT_OFFICE}.drawingml.chart+xml",
|
|
230
|
+
rel_type: "#{OFFICE_REL_BASE}/chart",
|
|
231
|
+
rels_scope: :document,
|
|
232
|
+
loader: :chart, load_priority: 100,
|
|
233
|
+
document_attribute: :chart_parts },
|
|
234
|
+
# Images carry a per-extension Default content type resolved
|
|
235
|
+
# from the image data at save time, so content_type is nil.
|
|
236
|
+
{ key: :image, kind: :default,
|
|
237
|
+
path_pattern: "word/media/%<name>s",
|
|
238
|
+
target_pattern: "media/%<name>s",
|
|
239
|
+
rel_type: "#{OFFICE_REL_BASE}/image",
|
|
240
|
+
rels_scope: :document,
|
|
241
|
+
loader: :image, load_priority: 110,
|
|
242
|
+
document_attribute: :image_parts },
|
|
243
|
+
# The embedding loader keys parts by target on the package's
|
|
244
|
+
# embeddings collection; no package→document copy.
|
|
245
|
+
{ key: :ole_object, kind: :override,
|
|
246
|
+
path_pattern: "word/embeddings/%<name>s",
|
|
247
|
+
target_pattern: "embeddings/%<name>s",
|
|
248
|
+
content_type: "#{CT_OFFICE}.oleObject",
|
|
249
|
+
rel_type: "#{OFFICE_REL_BASE}/oleObject",
|
|
250
|
+
rels_scope: :document,
|
|
251
|
+
loader: :embedding, load_priority: 120,
|
|
252
|
+
package_attribute: :embeddings,
|
|
253
|
+
document_attribute: :embeddings,
|
|
254
|
+
copy_to_document: false },
|
|
255
|
+
# customXml items themselves fall under the "xml" Default;
|
|
256
|
+
# only their itemProps parts get an Override. The :custom_xml
|
|
257
|
+
# loader also pulls each item's itemProps and .rels sidecars.
|
|
258
|
+
{ key: :custom_xml_item, kind: :none,
|
|
259
|
+
path_pattern: "customXml/item%<index>d.xml",
|
|
260
|
+
loader: :custom_xml, load_priority: 30,
|
|
261
|
+
package_attribute: :custom_xml_items,
|
|
262
|
+
document_attribute: :custom_xml_items },
|
|
263
|
+
{ key: :custom_xml_item_props, kind: :override,
|
|
264
|
+
path_pattern: "customXml/itemProps%<index>d.xml",
|
|
265
|
+
content_type: "#{CT_OFFICE}.customXmlProperties+xml" },
|
|
266
|
+
# Hyperlink relationships target external URLs; no part, no
|
|
267
|
+
# content type.
|
|
268
|
+
{ key: :hyperlink, kind: :none,
|
|
269
|
+
rel_type: "#{OFFICE_REL_BASE}/hyperlink",
|
|
270
|
+
rels_scope: :document },
|
|
271
|
+
# Theme part of THMX (theme) packages, rooted at /theme.
|
|
272
|
+
{ key: :thmx_theme, kind: :override, path: "theme/theme1.xml",
|
|
273
|
+
content_type: "#{CT_OFFICE}.theme+xml", required: true },
|
|
274
|
+
|
|
275
|
+
# -- Read-side infrastructure parts (no content-type entry
|
|
276
|
+
# of their own; kind :none keeps emission untouched) --
|
|
277
|
+
{ key: :content_types, kind: :none, path: "[Content_Types].xml",
|
|
278
|
+
loader: :xml_model, loader_model: Uniword::ContentTypes::Types,
|
|
279
|
+
load_priority: 10,
|
|
280
|
+
package_attribute: :content_types,
|
|
281
|
+
document_attribute: :content_types },
|
|
282
|
+
{ key: :package_rels, kind: :none, path: "_rels/.rels",
|
|
283
|
+
loader: :xml_model,
|
|
284
|
+
loader_model: Relationships::PackageRelationships,
|
|
285
|
+
load_priority: 20,
|
|
286
|
+
package_attribute: :package_rels,
|
|
287
|
+
document_attribute: :package_rels },
|
|
288
|
+
{ key: :document_rels, kind: :none,
|
|
289
|
+
path: "word/_rels/document.xml.rels",
|
|
290
|
+
loader: :xml_model,
|
|
291
|
+
loader_model: Relationships::PackageRelationships,
|
|
292
|
+
path_resolution: :office_document_rels, load_priority: 60,
|
|
293
|
+
package_attribute: :document_rels,
|
|
294
|
+
document_attribute: :document_rels },
|
|
295
|
+
{ key: :settings_rels, kind: :none,
|
|
296
|
+
path: "word/_rels/settings.xml.rels",
|
|
297
|
+
loader: :xml_model,
|
|
298
|
+
loader_model: Relationships::PackageRelationships,
|
|
299
|
+
load_priority: 50,
|
|
300
|
+
package_attribute: :settings_rels,
|
|
301
|
+
document_attribute: :settings_rels },
|
|
302
|
+
{ key: :theme_rels, kind: :none,
|
|
303
|
+
path: "word/theme/_rels/theme1.xml.rels",
|
|
304
|
+
loader: :xml_model,
|
|
305
|
+
loader_model: Relationships::PackageRelationships,
|
|
306
|
+
load_priority: 75,
|
|
307
|
+
package_attribute: :theme_rels,
|
|
308
|
+
document_attribute: :theme_rels },
|
|
309
|
+
{ key: :footnotes_rels, kind: :none,
|
|
310
|
+
path: "word/_rels/footnotes.xml.rels",
|
|
311
|
+
loader: :xml_model,
|
|
312
|
+
loader_model: Relationships::PackageRelationships,
|
|
313
|
+
load_priority: 85,
|
|
314
|
+
package_attribute: :footnotes_rels,
|
|
315
|
+
document_attribute: :footnotes_rels },
|
|
316
|
+
{ key: :endnotes_rels, kind: :none,
|
|
317
|
+
path: "word/_rels/endnotes.xml.rels",
|
|
318
|
+
loader: :xml_model,
|
|
319
|
+
loader_model: Relationships::PackageRelationships,
|
|
320
|
+
load_priority: 85,
|
|
321
|
+
package_attribute: :endnotes_rels,
|
|
322
|
+
document_attribute: :endnotes_rels },
|
|
323
|
+
# Theme media files live on the theme's media_files, not on a
|
|
324
|
+
# package/document attribute; loads only when a theme part was
|
|
325
|
+
# parsed (see PartLoader::ThemeMediaLoader).
|
|
326
|
+
{ key: :theme_media, kind: :none,
|
|
327
|
+
path_pattern: "word/theme/media/%<name>s",
|
|
328
|
+
loader: :theme_media, load_priority: 72 },
|
|
329
|
+
# Fallback passthrough for parts no definition claims
|
|
330
|
+
# (unmodelled parts such as docProps/meta.xml, glossary
|
|
331
|
+
# documents, VBA projects): the :raw loader runs last and
|
|
332
|
+
# carries the remainder byte-for-byte on the package's
|
|
333
|
+
# raw_parts collection. Emission is derived from the stored
|
|
334
|
+
# parts like any collection family, so emitted_paths — and
|
|
335
|
+
# with it the reconciler's dangling-relationship sweep — sees
|
|
336
|
+
# raw-carried parts as first-class.
|
|
337
|
+
{ key: :raw_parts, kind: :none,
|
|
338
|
+
loader: :raw, load_priority: 130,
|
|
339
|
+
package_attribute: :raw_parts,
|
|
340
|
+
document_attribute: :raw_parts },
|
|
341
|
+
].freeze
|
|
342
|
+
|
|
343
|
+
class << self
|
|
344
|
+
# Register a part definition. Re-registering an existing key
|
|
345
|
+
# replaces it in place (preserving position); new keys append.
|
|
346
|
+
#
|
|
347
|
+
# @param definition [PartDefinition] definition to register
|
|
348
|
+
# @return [PartDefinition] the registered definition
|
|
349
|
+
# @raise [ArgumentError] when not given a PartDefinition
|
|
350
|
+
def register(definition)
|
|
351
|
+
unless definition.is_a?(PartDefinition)
|
|
352
|
+
raise ArgumentError,
|
|
353
|
+
"expected PartDefinition, got #{definition.class}"
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
index = definitions.index { |d| d.key == definition.key }
|
|
357
|
+
if index
|
|
358
|
+
definitions[index] = definition
|
|
359
|
+
else
|
|
360
|
+
definitions << definition
|
|
361
|
+
end
|
|
362
|
+
definition
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Remove a registered definition (primarily for tests).
|
|
366
|
+
#
|
|
367
|
+
# @param key [Symbol] registry key
|
|
368
|
+
# @return [Array<PartDefinition>, nil] the definitions list if
|
|
369
|
+
# a definition was removed, nil when the key was absent
|
|
370
|
+
def unregister(key)
|
|
371
|
+
definitions.reject! { |d| d.key == key.to_sym }
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# @return [Array<PartDefinition>] all definitions, in
|
|
375
|
+
# registration order (a copy; safe to mutate)
|
|
376
|
+
def all
|
|
377
|
+
definitions.dup
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# @param key [Symbol] registry key (e.g. :styles)
|
|
381
|
+
# @return [PartDefinition, nil]
|
|
382
|
+
def find_by_key(key)
|
|
383
|
+
definitions.find { |d| d.key == key.to_sym }
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Find by package path; accepts both "word/styles.xml" and
|
|
387
|
+
# "/word/styles.xml" forms, and matches numbered paths against
|
|
388
|
+
# registered patterns ("word/header2.xml" → :header).
|
|
389
|
+
#
|
|
390
|
+
# @param path [String] package-relative path
|
|
391
|
+
# @return [PartDefinition, nil]
|
|
392
|
+
def find_by_path(path)
|
|
393
|
+
normalized = path.to_s.delete_prefix("/")
|
|
394
|
+
definitions.find { |d| path_match?(d, normalized) }
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# @param content_type [String] MIME content type
|
|
398
|
+
# @return [PartDefinition, nil] first definition with this type
|
|
399
|
+
def find_by_content_type(content_type)
|
|
400
|
+
definitions.find { |d| d.content_type == content_type }
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Find an Override definition by content-type part name.
|
|
404
|
+
#
|
|
405
|
+
# @param part_name [String] e.g. "/word/styles.xml"
|
|
406
|
+
# @return [PartDefinition, nil]
|
|
407
|
+
def override_for(part_name)
|
|
408
|
+
normalized = part_name.to_s.delete_prefix("/")
|
|
409
|
+
definitions.find do |d|
|
|
410
|
+
d.override? && path_match?(d, normalized)
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Find a Default definition by file extension.
|
|
415
|
+
#
|
|
416
|
+
# @param extension [String] e.g. "png"
|
|
417
|
+
# @return [PartDefinition, nil]
|
|
418
|
+
def default_for(extension)
|
|
419
|
+
definitions.find do |d|
|
|
420
|
+
d.default? && d.extension == extension.to_s
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Definitions included in the comprehensive [Content_Types].xml
|
|
425
|
+
# that ContentTypes.generate emits for new DOCX packages.
|
|
426
|
+
#
|
|
427
|
+
# @return [Array<PartDefinition>] in registration order
|
|
428
|
+
def standard_defaults
|
|
429
|
+
definitions.select { |d| d.standard? && d.default? }
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# @return [Array<PartDefinition>] in registration order
|
|
433
|
+
def standard_overrides
|
|
434
|
+
definitions.select { |d| d.standard? && d.override? }
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# Relationship type URIs whose relationships belong in the
|
|
438
|
+
# package-level _rels/.rels part.
|
|
439
|
+
#
|
|
440
|
+
# @return [Array<String>] in registration order
|
|
441
|
+
def package_rel_types
|
|
442
|
+
definitions.filter_map { |d| d.rel_type if d.package_rel? }
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Definitions loadable from extracted ZIP content, in load
|
|
446
|
+
# order: ascending PartDefinition#load_priority, registration
|
|
447
|
+
# order within a priority (explicit index tiebreak — Ruby's
|
|
448
|
+
# sort_by is not stable).
|
|
449
|
+
#
|
|
450
|
+
# @return [Array<PartDefinition>]
|
|
451
|
+
def loadable
|
|
452
|
+
definitions.select(&:loadable?).each_with_index
|
|
453
|
+
.sort_by { |d, i| [d.load_priority || 100, i] }
|
|
454
|
+
.map(&:first)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Definitions mirrored package→document by
|
|
458
|
+
# DocumentFactory.copy_package_parts_to_document.
|
|
459
|
+
#
|
|
460
|
+
# @return [Array<PartDefinition>] in registration order
|
|
461
|
+
def copied_to_document
|
|
462
|
+
definitions.select do |d|
|
|
463
|
+
d.document_attribute && d.package_attribute &&
|
|
464
|
+
d.copy_to_document?
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Definitions mirrored document→package by
|
|
469
|
+
# Docx::PackageDefaults.copy_document_parts_to_package.
|
|
470
|
+
#
|
|
471
|
+
# @return [Array<PartDefinition>] in registration order
|
|
472
|
+
def copied_to_package
|
|
473
|
+
definitions.select { |d| d.document_attribute && d.package_attribute }
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Package paths the given package will emit, derived from the
|
|
477
|
+
# definitions: single-instance kinds by model presence,
|
|
478
|
+
# collection families by their stored parts.
|
|
479
|
+
#
|
|
480
|
+
# Single authority for "will this part be emitted?" — used by
|
|
481
|
+
# the reconciler's dangling-relationship sweep.
|
|
482
|
+
#
|
|
483
|
+
# @param package [Docx::Package] Package to enumerate for
|
|
484
|
+
# @return [Set<String>] emitted package paths
|
|
485
|
+
def emitted_paths(package)
|
|
486
|
+
all.flat_map { |definition| definition.emitted_paths(package) }
|
|
487
|
+
.compact.to_set
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
private
|
|
491
|
+
|
|
492
|
+
def definitions
|
|
493
|
+
@definitions ||= []
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def path_match?(definition, normalized_path)
|
|
497
|
+
definition.match_path?(normalized_path)
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
BUILT_INS.each { |attrs| register(PartDefinition.new(**attrs)) }
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
end
|
|
@@ -9,10 +9,13 @@ module Uniword
|
|
|
9
9
|
#
|
|
10
10
|
# Used for image relationships in documents
|
|
11
11
|
class ImageRelationship < Relationship
|
|
12
|
-
|
|
12
|
+
# @param id [String] relationship ID (allocated by
|
|
13
|
+
# Docx::IdAllocator — the single rId authority)
|
|
14
|
+
# @param target [String] image part target (e.g. "media/image1.png")
|
|
15
|
+
def initialize(id:, target:)
|
|
13
16
|
super(
|
|
14
|
-
id:
|
|
15
|
-
type:
|
|
17
|
+
id: id,
|
|
18
|
+
type: PartRegistry.find_by_key(:image).rel_type,
|
|
16
19
|
target: target
|
|
17
20
|
)
|
|
18
21
|
end
|
|
@@ -23,35 +23,33 @@ module Uniword
|
|
|
23
23
|
#
|
|
24
24
|
# Generate package-level .rels file
|
|
25
25
|
#
|
|
26
|
+
# Historical rId assignment: rId3 = app, rId2 = core,
|
|
27
|
+
# rId1 = document (kept for byte-identical DOTX output).
|
|
28
|
+
#
|
|
26
29
|
# @return [PackageRelationships] Relationships object for _rels/.rels
|
|
27
|
-
def self.next_available_rid(relationships)
|
|
28
|
-
max = relationships.relationships.filter_map do |r|
|
|
29
|
-
r.id[/\ArId(\d+)\z/, 1]&.to_i
|
|
30
|
-
end.max || 0
|
|
31
|
-
"rId#{max + 1}"
|
|
32
|
-
end
|
|
33
|
-
|
|
34
30
|
def self.generate_package_rels
|
|
35
31
|
new(
|
|
36
32
|
relationships: [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
target: "docProps/app.xml",
|
|
41
|
-
),
|
|
42
|
-
PackageRelationship.new(
|
|
43
|
-
id: "rId2",
|
|
44
|
-
type: "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
|
|
45
|
-
target: "docProps/core.xml",
|
|
46
|
-
),
|
|
47
|
-
PackageRelationship.new(
|
|
48
|
-
id: "rId1",
|
|
49
|
-
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
|
|
50
|
-
target: "word/document.xml",
|
|
51
|
-
),
|
|
33
|
+
package_relationship("rId3", :app_properties),
|
|
34
|
+
package_relationship("rId2", :core_properties),
|
|
35
|
+
package_relationship("rId1", :document),
|
|
52
36
|
],
|
|
53
37
|
)
|
|
54
38
|
end
|
|
39
|
+
|
|
40
|
+
# Build one package-level relationship from the registry.
|
|
41
|
+
#
|
|
42
|
+
# @param rid [String] relationship ID
|
|
43
|
+
# @param key [Symbol] PartRegistry key
|
|
44
|
+
# @return [PackageRelationship]
|
|
45
|
+
def self.package_relationship(rid, key)
|
|
46
|
+
definition = PartRegistry.find_by_key(key)
|
|
47
|
+
PackageRelationship.new(
|
|
48
|
+
id: rid,
|
|
49
|
+
type: definition.rel_type,
|
|
50
|
+
target: definition.target,
|
|
51
|
+
)
|
|
52
|
+
end
|
|
55
53
|
end
|
|
56
54
|
end
|
|
57
55
|
end
|
|
@@ -301,12 +301,12 @@ module Uniword
|
|
|
301
301
|
def get_attribute_value(element, attr_def)
|
|
302
302
|
property_name = attr_def.property_name
|
|
303
303
|
|
|
304
|
-
#
|
|
305
|
-
#
|
|
306
|
-
#
|
|
307
|
-
#
|
|
304
|
+
# Schema-driven read: the model's declared attributes are the
|
|
305
|
+
# allowlist, and the value is fetched through the model's
|
|
306
|
+
# public method table. The property_name comes from the schema
|
|
307
|
+
# definition, not user input.
|
|
308
308
|
if element.is_a?(Lutaml::Model::Serializable) && element.class.attributes.key?(property_name)
|
|
309
|
-
element.
|
|
309
|
+
element.method(property_name).call
|
|
310
310
|
elsif element.is_a?(Hash) && element.key?(property_name)
|
|
311
311
|
element[property_name]
|
|
312
312
|
end
|
|
@@ -320,9 +320,10 @@ module Uniword
|
|
|
320
320
|
def get_child_elements(element, child_def)
|
|
321
321
|
property_name = child_def.property_name
|
|
322
322
|
|
|
323
|
-
#
|
|
323
|
+
# Schema-driven read via the model's public method table,
|
|
324
|
+
# guarded by the declared attribute names.
|
|
324
325
|
if element.is_a?(Lutaml::Model::Serializable) && element.class.attributes.key?(property_name)
|
|
325
|
-
element.
|
|
326
|
+
element.method(property_name).call
|
|
326
327
|
elsif element.is_a?(Hash) && element.key?(property_name)
|
|
327
328
|
element[property_name]
|
|
328
329
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
module Ooxml
|
|
7
|
+
module Types
|
|
8
|
+
# ST_HexColor (ECMA-376): RGB hex color or "auto"
|
|
9
|
+
#
|
|
10
|
+
# Constrained string type: six hexadecimal digits (case-insensitive,
|
|
11
|
+
# ST_HexColorRGB) or the literal "auto" (ST_HexColorAuto). Invalid
|
|
12
|
+
# values raise Lutaml::Model::Type::InvalidValueError at cast time
|
|
13
|
+
# (attribute assignment and XML parsing).
|
|
14
|
+
class HexColorValue < Lutaml::Model::Type::String
|
|
15
|
+
# ST_HexColor value shape: "auto" or RRGGBB hex digits
|
|
16
|
+
HEX_COLOR_PATTERN = /\A(?:auto|[0-9A-Fa-f]{6})\z/
|
|
17
|
+
|
|
18
|
+
# Cast a value to a valid ST_HexColor string
|
|
19
|
+
#
|
|
20
|
+
# @param value [Object] the raw value
|
|
21
|
+
# @param options [Hash] cast options (unused)
|
|
22
|
+
# @return [String, nil] the validated color string
|
|
23
|
+
# @raise [Lutaml::Model::Type::InvalidValueError] when the value
|
|
24
|
+
# is neither "auto" nor six hex digits
|
|
25
|
+
def self.cast(value, options = {})
|
|
26
|
+
casted = super
|
|
27
|
+
return casted if casted.nil? ||
|
|
28
|
+
Lutaml::Model::Utils.uninitialized?(casted)
|
|
29
|
+
return casted if HEX_COLOR_PATTERN.match?(casted)
|
|
30
|
+
|
|
31
|
+
raise Lutaml::Model::Type::InvalidValueError.new(
|
|
32
|
+
value, ["auto", "RRGGBB (6 hexadecimal digits)"]
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|