uniword 1.0.10 → 1.2.4
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/config/font_metadata.yml +56 -0
- data/lib/uniword/accessibility/rules/descriptive_headings_rule.rb +1 -1
- data/lib/uniword/assembly/toc.rb +4 -14
- data/lib/uniword/assembly/variable_substitutor.rb +3 -4
- data/lib/uniword/batch/stages/normalize_styles_stage.rb +4 -2
- data/lib/uniword/builder/base_builder.rb +25 -0
- data/lib/uniword/builder/chart_builder.rb +33 -14
- data/lib/uniword/builder/deterministic_id.rb +17 -0
- data/lib/uniword/builder/document_builder.rb +129 -34
- data/lib/uniword/builder/footnote_builder.rb +22 -23
- data/lib/uniword/builder/header_footer_builder.rb +10 -10
- data/lib/uniword/builder/image_builder.rb +24 -16
- data/lib/uniword/builder/list_builder.rb +2 -1
- data/lib/uniword/builder/paragraph_builder.rb +95 -20
- data/lib/uniword/builder/run_utils.rb +97 -0
- data/lib/uniword/builder/section_builder.rb +40 -67
- data/lib/uniword/builder/table_builder.rb +32 -13
- data/lib/uniword/builder/table_cell_builder.rb +19 -0
- data/lib/uniword/builder.rb +27 -19
- data/lib/uniword/cli/diff_cli.rb +0 -1
- data/lib/uniword/cli/generate_cli.rb +0 -1
- data/lib/uniword/cli/headers_cli.rb +0 -1
- data/lib/uniword/cli/images_cli.rb +0 -1
- data/lib/uniword/cli/main.rb +6 -7
- data/lib/uniword/cli/protect_cli.rb +0 -1
- data/lib/uniword/cli/resources_cli.rb +0 -1
- data/lib/uniword/cli/review_cli.rb +0 -1
- data/lib/uniword/cli/spellcheck_cli.rb +0 -1
- data/lib/uniword/cli/styleset_cli.rb +0 -1
- data/lib/uniword/cli/template_cli.rb +0 -1
- data/lib/uniword/cli/theme_cli.rb +0 -1
- data/lib/uniword/cli/toc_cli.rb +0 -1
- data/lib/uniword/cli/watermark_cli.rb +0 -1
- data/lib/uniword/cli.rb +6 -17
- data/lib/uniword/diff/document_differ.rb +23 -5
- data/lib/uniword/diff/package_differ.rb +0 -1
- data/lib/uniword/diff.rb +4 -0
- data/lib/uniword/document_factory.rb +16 -24
- data/lib/uniword/docx/document_statistics.rb +42 -54
- data/lib/uniword/docx/id_allocator.rb +143 -0
- data/lib/uniword/docx/package.rb +42 -6
- data/lib/uniword/docx/package_defaults.rb +16 -25
- data/lib/uniword/docx/package_serialization.rb +131 -127
- data/lib/uniword/docx/reconciler/body.rb +197 -0
- data/lib/uniword/docx/reconciler/fix_codes.rb +74 -0
- data/lib/uniword/docx/reconciler/helpers.rb +337 -0
- data/lib/uniword/docx/reconciler/notes.rb +301 -0
- data/lib/uniword/docx/reconciler/package_structure.rb +322 -0
- data/lib/uniword/docx/reconciler/parts.rb +484 -0
- data/lib/uniword/docx/reconciler/referential_integrity.rb +449 -0
- data/lib/uniword/docx/reconciler/tables.rb +162 -0
- data/lib/uniword/docx/reconciler/theme.rb +141 -0
- data/lib/uniword/docx/reconciler.rb +44 -1069
- data/lib/uniword/docx.rb +3 -0
- data/lib/uniword/drawingml/color_scheme.rb +38 -5
- data/lib/uniword/hyperlink.rb +27 -26
- data/lib/uniword/infrastructure/zip_packager.rb +41 -38
- data/lib/uniword/lazy_loader.rb +36 -62
- data/lib/uniword/math.rb +3 -86
- data/lib/uniword/mhtml/document.rb +1 -3
- data/lib/uniword/model_attribute_access.rb +27 -0
- data/lib/uniword/ooxml/namespaces.rb +62 -2
- data/lib/uniword/ooxml/relationships/package_relationships.rb +7 -0
- data/lib/uniword/presentationml/slide_id.rb +0 -1
- data/lib/uniword/presentationml/slide_master_id.rb +0 -1
- data/lib/uniword/properties/bold.rb +0 -2
- data/lib/uniword/properties/boolean_formatting.rb +0 -2
- data/lib/uniword/properties/borders.rb +1 -1
- data/lib/uniword/properties/italic.rb +0 -2
- data/lib/uniword/properties/numbering_properties.rb +1 -1
- data/lib/uniword/properties/style_reference.rb +13 -0
- data/lib/uniword/quality/rules/heading_hierarchy_rule.rb +1 -1
- data/lib/uniword/quality/rules/style_consistency_rule.rb +1 -1
- data/lib/uniword/review/review_manager.rb +0 -2
- data/lib/uniword/template/helpers/loop_helper.rb +2 -2
- data/lib/uniword/template/variable_resolver.rb +14 -1
- data/lib/uniword/theme/theme_xml_parser.rb +1 -1
- data/lib/uniword/toc/toc_generator.rb +3 -14
- data/lib/uniword/transformation/mhtml_element_renderer.rb +94 -37
- data/lib/uniword/transformation/mhtml_metadata_builder.rb +1 -1
- data/lib/uniword/transformation/mhtml_style_builder.rb +9 -2
- data/lib/uniword/transformation/ooxml_to_html_converter.rb +3 -5
- data/lib/uniword/transformation/ooxml_to_mhtml_converter.rb +7 -15
- data/lib/uniword/transformation/yaml_css_generator.rb +815 -0
- data/lib/uniword/transformation.rb +2 -0
- data/lib/uniword/validation/checkers/internal_link_checker.rb +2 -3
- data/lib/uniword/version.rb +1 -1
- data/lib/uniword/visitor/text_extractor.rb +2 -1
- data/lib/uniword/watermark/manager.rb +2 -2
- data/lib/uniword/wordprocessingml/allow_png.rb +14 -0
- data/lib/uniword/wordprocessingml/body.rb +53 -18
- data/lib/uniword/wordprocessingml/body_div.rb +17 -0
- data/lib/uniword/wordprocessingml/div_border.rb +23 -0
- data/lib/uniword/wordprocessingml/div_borders.rb +24 -0
- data/lib/uniword/wordprocessingml/divs_child.rb +17 -0
- data/lib/uniword/wordprocessingml/document_root.rb +6 -3
- data/lib/uniword/wordprocessingml/endnote.rb +1 -1
- data/lib/uniword/wordprocessingml/endnotes.rb +4 -0
- data/lib/uniword/wordprocessingml/footer.rb +4 -0
- data/lib/uniword/wordprocessingml/footer_reference.rb +0 -1
- data/lib/uniword/wordprocessingml/footnote.rb +1 -1
- data/lib/uniword/wordprocessingml/footnotes.rb +4 -0
- data/lib/uniword/wordprocessingml/header.rb +4 -0
- data/lib/uniword/wordprocessingml/header_reference.rb +0 -1
- data/lib/uniword/wordprocessingml/hyperlink.rb +2 -2
- data/lib/uniword/wordprocessingml/instr_text.rb +20 -5
- data/lib/uniword/wordprocessingml/level.rb +2 -0
- data/lib/uniword/wordprocessingml/mar_bottom.rb +17 -0
- data/lib/uniword/wordprocessingml/mar_left.rb +17 -0
- data/lib/uniword/wordprocessingml/mar_right.rb +17 -0
- data/lib/uniword/wordprocessingml/mar_top.rb +17 -0
- data/lib/uniword/wordprocessingml/num.rb +4 -0
- data/lib/uniword/wordprocessingml/numbering.rb +1 -22
- data/lib/uniword/wordprocessingml/optimize_for_browser.rb +14 -0
- data/lib/uniword/wordprocessingml/page_defaults.rb +39 -0
- data/lib/uniword/wordprocessingml/paragraph.rb +16 -13
- data/lib/uniword/wordprocessingml/paragraph_properties.rb +12 -8
- data/lib/uniword/wordprocessingml/run.rb +32 -36
- data/lib/uniword/wordprocessingml/run_properties/merging.rb +45 -0
- data/lib/uniword/wordprocessingml/run_properties.rb +8 -5
- data/lib/uniword/wordprocessingml/section_properties.rb +3 -3
- data/lib/uniword/wordprocessingml/separator_char.rb +21 -0
- data/lib/uniword/wordprocessingml/settings.rb +19 -17
- data/lib/uniword/wordprocessingml/styles/style_definition.rb +1 -1
- data/lib/uniword/wordprocessingml/styles/style_library.rb +16 -0
- data/lib/uniword/wordprocessingml/table_defaults.rb +44 -0
- data/lib/uniword/wordprocessingml/text.rb +8 -2
- data/lib/uniword/wordprocessingml/web_div.rb +41 -0
- data/lib/uniword/wordprocessingml/web_divs.rb +18 -0
- data/lib/uniword/wordprocessingml/web_encoding.rb +17 -0
- data/lib/uniword/wordprocessingml/web_settings.rb +5 -21
- data/lib/uniword/wordprocessingml/zoom.rb +2 -0
- data/lib/uniword/wordprocessingml.rb +23 -0
- data/lib/uniword.rb +27 -0
- metadata +47 -70
- data/lib/uniword/math/accent.rb +0 -25
- data/lib/uniword/math/accent_properties.rb +0 -25
- data/lib/uniword/math/argument_properties.rb +0 -22
- data/lib/uniword/math/bar.rb +0 -25
- data/lib/uniword/math/bar_properties.rb +0 -25
- data/lib/uniword/math/begin_char.rb +0 -22
- data/lib/uniword/math/border_box.rb +0 -25
- data/lib/uniword/math/border_box_properties.rb +0 -39
- data/lib/uniword/math/box.rb +0 -25
- data/lib/uniword/math/box_properties.rb +0 -33
- data/lib/uniword/math/char.rb +0 -22
- data/lib/uniword/math/control_properties.rb +0 -27
- data/lib/uniword/math/degree.rb +0 -26
- data/lib/uniword/math/delimiter.rb +0 -25
- data/lib/uniword/math/delimiter_properties.rb +0 -33
- data/lib/uniword/math/denominator.rb +0 -26
- data/lib/uniword/math/element.rb +0 -61
- data/lib/uniword/math/end_char.rb +0 -22
- data/lib/uniword/math/equation_array.rb +0 -25
- data/lib/uniword/math/equation_array_properties.rb +0 -33
- data/lib/uniword/math/fraction.rb +0 -27
- data/lib/uniword/math/fraction_properties.rb +0 -25
- data/lib/uniword/math/function.rb +0 -27
- data/lib/uniword/math/function_name.rb +0 -26
- data/lib/uniword/math/function_properties.rb +0 -23
- data/lib/uniword/math/group_char.rb +0 -25
- data/lib/uniword/math/group_char_properties.rb +0 -29
- data/lib/uniword/math/lim.rb +0 -26
- data/lib/uniword/math/lower_limit.rb +0 -27
- data/lib/uniword/math/lower_limit_properties.rb +0 -23
- data/lib/uniword/math/math_break.rb +0 -22
- data/lib/uniword/math/math_font.rb +0 -22
- data/lib/uniword/math/math_properties.rb +0 -53
- data/lib/uniword/math/math_run.rb +0 -42
- data/lib/uniword/math/math_run_properties.rb +0 -37
- data/lib/uniword/math/math_simple_int_val.rb +0 -24
- data/lib/uniword/math/math_simple_val.rb +0 -24
- data/lib/uniword/math/math_style.rb +0 -22
- data/lib/uniword/math/math_text.rb +0 -23
- data/lib/uniword/math/matrix.rb +0 -25
- data/lib/uniword/math/matrix_column.rb +0 -23
- data/lib/uniword/math/matrix_column_properties.rb +0 -24
- data/lib/uniword/math/matrix_columns.rb +0 -23
- data/lib/uniword/math/matrix_properties.rb +0 -37
- data/lib/uniword/math/matrix_row.rb +0 -23
- data/lib/uniword/math/nary.rb +0 -29
- data/lib/uniword/math/nary_properties.rb +0 -33
- data/lib/uniword/math/numerator.rb +0 -26
- data/lib/uniword/math/o_math.rb +0 -65
- data/lib/uniword/math/o_math_para.rb +0 -25
- data/lib/uniword/math/o_math_para_properties.rb +0 -22
- data/lib/uniword/math/phantom.rb +0 -25
- data/lib/uniword/math/phantom_properties.rb +0 -33
- data/lib/uniword/math/pre_sub_superscript.rb +0 -29
- data/lib/uniword/math/pre_sub_superscript_properties.rb +0 -23
- data/lib/uniword/math/radical.rb +0 -27
- data/lib/uniword/math/radical_properties.rb +0 -25
- data/lib/uniword/math/separator_char.rb +0 -22
- data/lib/uniword/math/sub.rb +0 -26
- data/lib/uniword/math/sub_superscript.rb +0 -29
- data/lib/uniword/math/sub_superscript_properties.rb +0 -25
- data/lib/uniword/math/subscript.rb +0 -27
- data/lib/uniword/math/subscript_properties.rb +0 -23
- data/lib/uniword/math/sup.rb +0 -26
- data/lib/uniword/math/superscript.rb +0 -27
- data/lib/uniword/math/superscript_properties.rb +0 -23
- data/lib/uniword/math/upper_limit.rb +0 -27
- data/lib/uniword/math/upper_limit_properties.rb +0 -23
|
@@ -1,25 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "yaml"
|
|
4
|
-
require "securerandom"
|
|
5
|
-
|
|
6
3
|
module Uniword
|
|
7
4
|
module Docx
|
|
8
|
-
# Reconciles DOCX-level invariants before serialization.
|
|
9
|
-
#
|
|
10
|
-
# Ensures that the document's model state is internally consistent so that
|
|
11
|
-
# the serialized output is always a valid DOCX file. Called from
|
|
12
|
-
# Docx::Package#to_zip_content before the serialization phase.
|
|
13
|
-
#
|
|
14
|
-
# This is not an extension point -- it enforces built-in invariants.
|
|
15
|
-
# For customizable validation, use Uniword::Validation::Rules instead.
|
|
16
|
-
# For user-defined requirements, use Docx::Requirement (future).
|
|
17
5
|
class Reconciler
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
6
|
+
EXTENSION_PREFIXES = Ooxml::Namespaces::EXTENSION_PREFIXES
|
|
7
|
+
FULL_IGNORABLE = Ooxml::Namespaces::FULL_IGNORABLE_PREFIXES
|
|
8
|
+
|
|
9
|
+
VALID_NOTE_TYPES = %w[separator continuationSeparator
|
|
10
|
+
footnoteSeparator continuationNotice].freeze
|
|
11
|
+
|
|
12
|
+
# Sub-modules autoloaded — no require_relative for internal code.
|
|
13
|
+
autoload :FixCodes, "#{__dir__}/reconciler/fix_codes"
|
|
14
|
+
autoload :Helpers, "#{__dir__}/reconciler/helpers"
|
|
15
|
+
autoload :Notes, "#{__dir__}/reconciler/notes"
|
|
16
|
+
autoload :ReferentialIntegrity, "#{__dir__}/reconciler/referential_integrity"
|
|
17
|
+
autoload :Tables, "#{__dir__}/reconciler/tables"
|
|
18
|
+
autoload :Theme, "#{__dir__}/reconciler/theme"
|
|
19
|
+
autoload :Parts, "#{__dir__}/reconciler/parts"
|
|
20
|
+
autoload :PackageStructure, "#{__dir__}/reconciler/package_structure"
|
|
21
|
+
autoload :Body, "#{__dir__}/reconciler/body"
|
|
22
|
+
|
|
23
|
+
include Helpers
|
|
24
|
+
include Notes
|
|
25
|
+
include ReferentialIntegrity
|
|
26
|
+
include Tables
|
|
27
|
+
include Theme
|
|
28
|
+
include Parts
|
|
29
|
+
include PackageStructure
|
|
30
|
+
include Body
|
|
31
|
+
|
|
32
|
+
def initialize(package, profile: nil, allocator: nil)
|
|
21
33
|
@package = package
|
|
22
34
|
@profile = profile
|
|
35
|
+
@allocator = allocator
|
|
23
36
|
@applied_fixes = []
|
|
24
37
|
end
|
|
25
38
|
|
|
@@ -29,6 +42,8 @@ module Uniword
|
|
|
29
42
|
reconcile_footnotes
|
|
30
43
|
reconcile_endnotes
|
|
31
44
|
reconcile_note_references
|
|
45
|
+
reconcile_headers_footers
|
|
46
|
+
reconcile_tables
|
|
32
47
|
repair_theme
|
|
33
48
|
|
|
34
49
|
# Group 2: Support parts (profile-dependent)
|
|
@@ -51,16 +66,22 @@ module Uniword
|
|
|
51
66
|
reconcile_content_types
|
|
52
67
|
reconcile_package_rels
|
|
53
68
|
reconcile_document_rels
|
|
69
|
+
|
|
70
|
+
# Group 4: Integrity checks (after rels are assembled)
|
|
71
|
+
reconcile_referential_integrity
|
|
54
72
|
end
|
|
55
73
|
|
|
56
74
|
# Audit trail of fixes applied during reconciliation.
|
|
57
|
-
# Each entry is { validity_rule:, message:, timestamp: }.
|
|
58
75
|
attr_reader :applied_fixes
|
|
59
76
|
|
|
60
77
|
private
|
|
61
78
|
|
|
62
79
|
attr_reader :package, :profile
|
|
63
80
|
|
|
81
|
+
def allocator
|
|
82
|
+
@allocator || package.allocator
|
|
83
|
+
end
|
|
84
|
+
|
|
64
85
|
def record_fix(validity_rule, message)
|
|
65
86
|
@applied_fixes << {
|
|
66
87
|
validity_rule: validity_rule,
|
|
@@ -69,10 +90,6 @@ module Uniword
|
|
|
69
90
|
}
|
|
70
91
|
end
|
|
71
92
|
|
|
72
|
-
# Clear stored namespace plans from parsed XML so that
|
|
73
|
-
# declare: :always namespace_scopes take full effect during
|
|
74
|
-
# serialization. Without this, parsed objects limit namespace
|
|
75
|
-
# declarations to only those present in the source XML.
|
|
76
93
|
def clear_stored_namespace_plans
|
|
77
94
|
parts = [
|
|
78
95
|
package.document,
|
|
@@ -83,1060 +100,18 @@ module Uniword
|
|
|
83
100
|
package.numbering,
|
|
84
101
|
package.core_properties,
|
|
85
102
|
package.app_properties,
|
|
103
|
+
package.footnotes,
|
|
104
|
+
package.endnotes,
|
|
86
105
|
].compact
|
|
87
106
|
|
|
88
|
-
|
|
89
|
-
part
|
|
90
|
-
part.pending_plan_root_element = nil
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
parts.each(&:clear_xml_parse_state!)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def reconcile_section_properties
|
|
97
|
-
return unless package.document&.body
|
|
98
|
-
|
|
99
|
-
body = package.document.body
|
|
100
|
-
|
|
101
|
-
unless body.section_properties
|
|
102
|
-
body.section_properties = Wordprocessingml::SectionProperties.new(
|
|
103
|
-
page_size: Wordprocessingml::PageSize.new(width: 12_240,
|
|
104
|
-
height: 15_840),
|
|
105
|
-
page_margins: Wordprocessingml::PageMargins.new(
|
|
106
|
-
top: 1440, right: 1440, bottom: 1440, left: 1440,
|
|
107
|
-
header: 720, footer: 720, gutter: 0
|
|
108
|
-
),
|
|
109
|
-
columns: Wordprocessingml::Columns.new(space: 720),
|
|
110
|
-
doc_grid: Wordprocessingml::DocGrid.new(line_pitch: 360),
|
|
111
|
-
)
|
|
112
|
-
record_fix("R11",
|
|
113
|
-
"Added default section properties with US Letter page size")
|
|
114
|
-
return
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
sect_pr = body.section_properties
|
|
118
|
-
fixed = false
|
|
119
|
-
unless sect_pr.page_size
|
|
120
|
-
sect_pr.page_size = Wordprocessingml::PageSize.new(width: 12_240,
|
|
121
|
-
height: 15_840)
|
|
122
|
-
fixed = true
|
|
123
|
-
end
|
|
124
|
-
unless sect_pr.page_margins
|
|
125
|
-
sect_pr.page_margins = Wordprocessingml::PageMargins.new(
|
|
126
|
-
top: 1440, right: 1440, bottom: 1440, left: 1440,
|
|
127
|
-
header: 720, footer: 720, gutter: 0
|
|
128
|
-
)
|
|
129
|
-
fixed = true
|
|
130
|
-
end
|
|
131
|
-
if fixed
|
|
132
|
-
record_fix("R11",
|
|
133
|
-
"Filled missing pgSz/pgMar in existing section properties")
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# -- Footnotes --
|
|
138
|
-
|
|
139
|
-
def reconcile_footnotes
|
|
140
|
-
has_fn_pr = package.settings&.footnote_pr
|
|
141
|
-
has_footnotes = package.footnotes
|
|
142
|
-
|
|
143
|
-
if has_fn_pr && !has_footnotes
|
|
144
|
-
package.footnotes = minimal_footnotes
|
|
145
|
-
record_fix("R9",
|
|
146
|
-
"Created footnotes.xml to match footnotePr in settings")
|
|
147
|
-
elsif has_footnotes && !has_fn_pr
|
|
148
|
-
package.settings ||= Wordprocessingml::Settings.new
|
|
149
|
-
package.settings.footnote_pr = Wordprocessingml::FootnotePr.new
|
|
150
|
-
record_fix("R9",
|
|
151
|
-
"Added footnotePr to settings to match footnotes.xml")
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
ensure_separators(package.footnotes, :footnote) if package.footnotes
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# -- Endnotes --
|
|
158
|
-
|
|
159
|
-
def reconcile_endnotes
|
|
160
|
-
has_en_pr = package.settings&.endnote_pr
|
|
161
|
-
has_endnotes = package.endnotes
|
|
162
|
-
|
|
163
|
-
if has_en_pr && !has_endnotes
|
|
164
|
-
package.endnotes = minimal_endnotes
|
|
165
|
-
record_fix("R9",
|
|
166
|
-
"Created endnotes.xml to match endnotePr in settings")
|
|
167
|
-
elsif has_endnotes && !has_en_pr
|
|
168
|
-
package.settings ||= Wordprocessingml::Settings.new
|
|
169
|
-
package.settings.endnote_pr = Wordprocessingml::EndnotePr.new
|
|
170
|
-
record_fix("R9", "Added endnotePr to settings to match endnotes.xml")
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
ensure_separators(package.endnotes, :endnote) if package.endnotes
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# -- Note reference consistency --
|
|
177
|
-
|
|
178
|
-
def reconcile_note_references
|
|
179
|
-
reconcile_footnote_references
|
|
180
|
-
reconcile_endnote_references
|
|
181
|
-
reconcile_note_definition_integrity
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def reconcile_footnote_references
|
|
185
|
-
return unless package.document&.body
|
|
186
|
-
|
|
187
|
-
referenced = collect_note_ids(:footnote)
|
|
188
|
-
return if referenced.empty?
|
|
189
|
-
|
|
190
|
-
ensure_footnotes_part
|
|
191
|
-
defined = package.footnotes.footnote_entries.each_with_object(Set.new) do |fn, s|
|
|
192
|
-
s << fn.id
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
missing = referenced.to_set - defined
|
|
196
|
-
return if missing.empty?
|
|
197
|
-
|
|
198
|
-
missing.each do |id|
|
|
199
|
-
package.footnotes.footnote_entries << Wordprocessingml::Footnote.new(
|
|
200
|
-
id: id, paragraphs: [Wordprocessingml::Paragraph.new],
|
|
201
|
-
)
|
|
202
|
-
end
|
|
203
|
-
record_fix("R10",
|
|
204
|
-
"Added #{missing.size} missing footnote definition(s) " \
|
|
205
|
-
"for orphaned footnoteReference IDs: #{missing.sort.join(', ')}")
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
def reconcile_endnote_references
|
|
209
|
-
return unless package.document&.body
|
|
210
|
-
|
|
211
|
-
referenced = collect_note_ids(:endnote)
|
|
212
|
-
return if referenced.empty?
|
|
213
|
-
|
|
214
|
-
ensure_endnotes_part
|
|
215
|
-
defined = package.endnotes.endnote_entries.each_with_object(Set.new) do |en, s|
|
|
216
|
-
s << en.id
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
missing = referenced.to_set - defined
|
|
220
|
-
return if missing.empty?
|
|
221
|
-
|
|
222
|
-
missing.each do |id|
|
|
223
|
-
package.endnotes.endnote_entries << Wordprocessingml::Endnote.new(
|
|
224
|
-
id: id, paragraphs: [Wordprocessingml::Paragraph.new],
|
|
225
|
-
)
|
|
226
|
-
end
|
|
227
|
-
record_fix("R10",
|
|
228
|
-
"Added #{missing.size} missing endnote definition(s) " \
|
|
229
|
-
"for orphaned endnoteReference IDs: #{missing.sort.join(', ')}")
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
# Validates structural integrity of footnote/endnote definitions.
|
|
233
|
-
def reconcile_note_definition_integrity
|
|
234
|
-
strip_invalid_note_types(:footnote)
|
|
235
|
-
strip_invalid_note_types(:endnote)
|
|
236
|
-
deduplicate_note_ids(:footnote)
|
|
237
|
-
deduplicate_note_ids(:endnote)
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
# R15: Regular footnotes/endnotes must NOT have w:type.
|
|
241
|
-
# Only separator (id=-1) and continuationSeparator (id=0) use w:type.
|
|
242
|
-
def strip_invalid_note_types(type)
|
|
243
|
-
notes = package.public_send(:"#{type}s")
|
|
244
|
-
return unless notes
|
|
245
|
-
|
|
246
|
-
entries = notes.public_send(:"#{type}_entries")
|
|
247
|
-
invalid = entries.select do |e|
|
|
248
|
-
e.type && e.id != "-1" && e.id != "0"
|
|
249
|
-
end
|
|
250
|
-
return if invalid.empty?
|
|
251
|
-
|
|
252
|
-
invalid.each { |e| e.type = nil }
|
|
253
|
-
record_fix("R15",
|
|
254
|
-
"Removed invalid w:type from #{invalid.size} " \
|
|
255
|
-
"regular #{type} definition(s): " \
|
|
256
|
-
"IDs #{invalid.map(&:id).sort.join(', ')}")
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
# R16: Duplicate IDs in footnotes.xml/endnotes.xml are invalid.
|
|
260
|
-
# Keeps the first occurrence, removes subsequent duplicates.
|
|
261
|
-
def deduplicate_note_ids(type)
|
|
262
|
-
notes = package.public_send(:"#{type}s")
|
|
263
|
-
return unless notes
|
|
264
|
-
|
|
265
|
-
entries = notes.public_send(:"#{type}_entries")
|
|
266
|
-
seen = Set.new
|
|
267
|
-
dupes = []
|
|
268
|
-
entries.reject! do |e|
|
|
269
|
-
if seen.include?(e.id)
|
|
270
|
-
dupes << e
|
|
271
|
-
true
|
|
272
|
-
else
|
|
273
|
-
seen << e.id
|
|
274
|
-
false
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
|
-
return if dupes.empty?
|
|
278
|
-
|
|
279
|
-
record_fix("R16",
|
|
280
|
-
"Removed #{dupes.size} duplicate #{type} ID(s): " \
|
|
281
|
-
"#{dupes.map(&:id).sort.join(', ')}")
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
# -- Builders --
|
|
285
|
-
|
|
286
|
-
def minimal_footnotes
|
|
287
|
-
Wordprocessingml::Footnotes.new(
|
|
288
|
-
footnote_entries: [separator_entry(:footnote),
|
|
289
|
-
continuation_entry(:footnote)],
|
|
290
|
-
)
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
def minimal_endnotes
|
|
294
|
-
Wordprocessingml::Endnotes.new(
|
|
295
|
-
endnote_entries: [separator_entry(:endnote),
|
|
296
|
-
continuation_entry(:endnote)],
|
|
297
|
-
)
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
def separator_entry(type)
|
|
301
|
-
entry_class(type).new(
|
|
302
|
-
id: "-1", type: "separator",
|
|
303
|
-
paragraphs: [Wordprocessingml::Paragraph.new]
|
|
304
|
-
)
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
def continuation_entry(type)
|
|
308
|
-
entry_class(type).new(
|
|
309
|
-
id: "0", type: "continuationSeparator",
|
|
310
|
-
paragraphs: [Wordprocessingml::Paragraph.new]
|
|
311
|
-
)
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
def entry_class(type)
|
|
315
|
-
type == :footnote ? Wordprocessingml::Footnote : Wordprocessingml::Endnote
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
def ensure_separators(notes, type)
|
|
319
|
-
entries = notes.public_send(:"#{type}_entries")
|
|
320
|
-
ids = entries.to_set(&:id)
|
|
321
|
-
|
|
322
|
-
entries.unshift(separator_entry(type)) unless ids.include?("-1")
|
|
323
|
-
entries.unshift(continuation_entry(type)) unless ids.include?("0")
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
def collect_note_ids(type)
|
|
327
|
-
ref_attr = type == :footnote ? :footnote_reference : :endnote_reference
|
|
328
|
-
ids = []
|
|
329
|
-
collect_note_ids_from_element(package.document.body, ref_attr, ids)
|
|
330
|
-
ids
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
def collect_note_ids_from_element(element, ref_attr, ids)
|
|
334
|
-
return unless element
|
|
335
|
-
|
|
336
|
-
collect_note_ids_from_paragraphs(element.paragraphs, ref_attr, ids)
|
|
337
|
-
|
|
338
|
-
tables = element.tables
|
|
339
|
-
return unless tables
|
|
340
|
-
tables.each do |tbl|
|
|
341
|
-
tbl.rows.each do |row|
|
|
342
|
-
row.cells.each do |cell|
|
|
343
|
-
collect_note_ids_from_paragraphs(cell.paragraphs, ref_attr, ids)
|
|
344
|
-
end
|
|
345
|
-
end
|
|
346
|
-
end
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
def collect_note_ids_from_paragraphs(paragraphs, ref_attr, ids)
|
|
350
|
-
return unless paragraphs
|
|
351
|
-
paragraphs.each do |p|
|
|
352
|
-
(p.runs || []).each do |r|
|
|
353
|
-
ref = r.public_send(ref_attr)
|
|
354
|
-
ids << ref.id if ref&.id
|
|
355
|
-
end
|
|
107
|
+
(package.document&.header_footer_parts || []).each do |part|
|
|
108
|
+
parts << part[:content] if part[:content]
|
|
356
109
|
end
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
def ensure_footnotes_part
|
|
360
|
-
return if package.footnotes
|
|
361
|
-
|
|
362
|
-
package.footnotes = minimal_footnotes
|
|
363
|
-
record_fix("R9", "Created footnotes.xml for orphaned references")
|
|
364
|
-
end
|
|
365
110
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
package.endnotes = minimal_endnotes
|
|
370
|
-
record_fix("R9", "Created endnotes.xml for orphaned references")
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
# -- Group 2: Support parts (profile-dependent) --
|
|
374
|
-
|
|
375
|
-
def reconcile_theme
|
|
376
|
-
return unless profile
|
|
377
|
-
return if package.theme
|
|
378
|
-
|
|
379
|
-
theme_name = profile.system.default_theme_name
|
|
380
|
-
return unless theme_name
|
|
381
|
-
|
|
382
|
-
begin
|
|
383
|
-
friendly = Themes::Theme.load(theme_name)
|
|
384
|
-
word_theme = friendly.to_word_theme
|
|
385
|
-
word_theme.name = "Office Theme"
|
|
386
|
-
package.theme = word_theme
|
|
387
|
-
record_fix("R3", "Created default theme with complete fmtScheme")
|
|
388
|
-
rescue ArgumentError
|
|
389
|
-
nil
|
|
390
|
-
end
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
# Repair broken theme fmtScheme even without a profile.
|
|
394
|
-
# Word replaces the entire theme on repair; we fill minimum content.
|
|
395
|
-
def repair_theme
|
|
396
|
-
theme = package.theme
|
|
397
|
-
return unless theme
|
|
398
|
-
|
|
399
|
-
fmt = theme.theme_elements&.fmt_scheme
|
|
400
|
-
return unless fmt
|
|
401
|
-
|
|
402
|
-
repaired = false
|
|
403
|
-
|
|
404
|
-
if count_fill_styles(fmt.fill_style_lst) < 2
|
|
405
|
-
ensure_minimal_fill_list(fmt)
|
|
406
|
-
repaired = true
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
if count_line_styles(fmt.ln_style_lst) < 3
|
|
410
|
-
ensure_minimal_line_list(fmt)
|
|
411
|
-
repaired = true
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
if count_effect_styles(fmt.effect_style_lst) < 3
|
|
415
|
-
ensure_minimal_effect_list(fmt)
|
|
416
|
-
repaired = true
|
|
417
|
-
end
|
|
111
|
+
(package.document&.headers&.values || []).each { |h| parts << h }
|
|
112
|
+
(package.document&.footers&.values || []).each { |f| parts << f }
|
|
418
113
|
|
|
419
|
-
|
|
420
|
-
ensure_minimal_bg_fill_list(fmt)
|
|
421
|
-
repaired = true
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
if repaired
|
|
425
|
-
record_fix("R3",
|
|
426
|
-
"Repaired theme fmtScheme with minimum required content")
|
|
427
|
-
end
|
|
428
|
-
end
|
|
429
|
-
|
|
430
|
-
def count_fill_styles(lst)
|
|
431
|
-
return 0 unless lst
|
|
432
|
-
|
|
433
|
-
(lst.solid_fills || []).size + (lst.gradient_fills || []).size + (lst.blip_fills || []).size
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
def count_line_styles(lst)
|
|
437
|
-
return 0 unless lst
|
|
438
|
-
|
|
439
|
-
(lst.lines || []).size
|
|
440
|
-
end
|
|
441
|
-
|
|
442
|
-
def count_effect_styles(lst)
|
|
443
|
-
return 0 unless lst
|
|
444
|
-
|
|
445
|
-
(lst.effect_styles || []).size
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
def ensure_minimal_fill_list(fmt)
|
|
449
|
-
lst = fmt.fill_style_lst || Drawingml::FillStyleList.new
|
|
450
|
-
fills = Array(lst.solid_fills).dup
|
|
451
|
-
while fills.size < 2
|
|
452
|
-
fills << Drawingml::SolidFill.new(
|
|
453
|
-
scheme_color: Drawingml::SchemeColor.new(val: "accent#{fills.size + 1}"),
|
|
454
|
-
)
|
|
455
|
-
end
|
|
456
|
-
lst.solid_fills = fills
|
|
457
|
-
fmt.fill_style_lst = lst
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
def ensure_minimal_line_list(fmt)
|
|
461
|
-
lst = fmt.ln_style_lst || Drawingml::LineStyleList.new
|
|
462
|
-
lines = Array(lst.lines).dup
|
|
463
|
-
widths = [9525, 25400, 38100]
|
|
464
|
-
while lines.size < 3
|
|
465
|
-
idx = lines.size
|
|
466
|
-
lines << Drawingml::LineProperties.new(
|
|
467
|
-
width: widths[idx] || 9525,
|
|
468
|
-
solid_fill: Drawingml::SolidFill.new(
|
|
469
|
-
scheme_color: Drawingml::SchemeColor.new(val: "accent#{idx + 1}"),
|
|
470
|
-
),
|
|
471
|
-
)
|
|
472
|
-
end
|
|
473
|
-
lst.lines = lines
|
|
474
|
-
fmt.ln_style_lst = lst
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
def ensure_minimal_effect_list(fmt)
|
|
478
|
-
lst = fmt.effect_style_lst || Drawingml::EffectStyleList.new
|
|
479
|
-
styles = Array(lst.effect_styles).dup
|
|
480
|
-
while styles.size < 3
|
|
481
|
-
styles << Drawingml::EffectStyle.new
|
|
482
|
-
end
|
|
483
|
-
lst.effect_styles = styles
|
|
484
|
-
fmt.effect_style_lst = lst
|
|
485
|
-
end
|
|
486
|
-
|
|
487
|
-
def ensure_minimal_bg_fill_list(fmt)
|
|
488
|
-
lst = fmt.bg_fill_style_lst || Drawingml::BackgroundFillStyleList.new
|
|
489
|
-
fills = Array(lst.solid_fills).dup
|
|
490
|
-
while fills.size < 2
|
|
491
|
-
fills << Drawingml::SolidFill.new(
|
|
492
|
-
scheme_color: Drawingml::SchemeColor.new(val: "accent#{fills.size + 1}"),
|
|
493
|
-
)
|
|
494
|
-
end
|
|
495
|
-
lst.solid_fills = fills
|
|
496
|
-
fmt.bg_fill_style_lst = lst
|
|
497
|
-
end
|
|
498
|
-
|
|
499
|
-
def reconcile_settings
|
|
500
|
-
return unless profile
|
|
501
|
-
|
|
502
|
-
settings = package.settings
|
|
503
|
-
settings ||= begin
|
|
504
|
-
package.settings = Wordprocessingml::Settings.new
|
|
505
|
-
package.settings
|
|
506
|
-
end
|
|
507
|
-
|
|
508
|
-
rsid = generate_rsid
|
|
509
|
-
|
|
510
|
-
settings.zoom ||= Wordprocessingml::Zoom.new(percent: 100)
|
|
511
|
-
settings.do_not_display_page_boundaries ||= Wordprocessingml::DoNotDisplayPageBoundaries.new
|
|
512
|
-
settings.proof_state ||= Wordprocessingml::ProofState.new(
|
|
513
|
-
spelling: "clean", grammar: "clean",
|
|
514
|
-
)
|
|
515
|
-
settings.default_tab_stop ||= Wordprocessingml::DefaultTabStop.new(val: "720")
|
|
516
|
-
settings.character_spacing_control ||= Wordprocessingml::CharacterSpacingControl.new(
|
|
517
|
-
val: "doNotCompress",
|
|
518
|
-
)
|
|
519
|
-
|
|
520
|
-
settings.compat ||= build_compat
|
|
521
|
-
settings.rsids ||= build_rsids(rsid)
|
|
522
|
-
settings.math_pr ||= build_math_pr
|
|
523
|
-
settings.theme_font_lang ||= Wordprocessingml::ThemeFontLang.new(
|
|
524
|
-
val: profile.lang,
|
|
525
|
-
east_asia: profile.east_asia_lang,
|
|
526
|
-
)
|
|
527
|
-
settings.clr_scheme_mapping ||= build_clr_scheme_mapping
|
|
528
|
-
settings.decimal_symbol ||= Wordprocessingml::DecimalSymbol.new(
|
|
529
|
-
val: profile.decimal_symbol,
|
|
530
|
-
)
|
|
531
|
-
settings.list_separator ||= Wordprocessingml::ListSeparator.new(
|
|
532
|
-
val: profile.list_separator,
|
|
533
|
-
)
|
|
534
|
-
|
|
535
|
-
unless settings.w14_doc_id
|
|
536
|
-
settings.w14_doc_id = Wordprocessingml::W14DocId.new(
|
|
537
|
-
val: SecureRandom.hex(4).upcase,
|
|
538
|
-
)
|
|
539
|
-
record_fix("R2", "Generated w14:docId")
|
|
540
|
-
end
|
|
541
|
-
unless settings.w15_doc_id
|
|
542
|
-
settings.w15_doc_id = Wordprocessingml::W15DocId.new(
|
|
543
|
-
val: "{#{SecureRandom.uuid.upcase}}",
|
|
544
|
-
)
|
|
545
|
-
record_fix("R2", "Generated w15:docId in GUID format")
|
|
546
|
-
end
|
|
547
|
-
|
|
548
|
-
settings.mc_ignorable = nil
|
|
549
|
-
end
|
|
550
|
-
|
|
551
|
-
def reconcile_font_table
|
|
552
|
-
return unless profile
|
|
553
|
-
|
|
554
|
-
font_table = package.font_table
|
|
555
|
-
font_table ||= begin
|
|
556
|
-
package.font_table = Wordprocessingml::FontTable.new
|
|
557
|
-
record_fix("R13", "Created font table")
|
|
558
|
-
package.font_table
|
|
559
|
-
end
|
|
560
|
-
|
|
561
|
-
return unless font_table.fonts.empty?
|
|
562
|
-
|
|
563
|
-
metadata = load_font_metadata
|
|
564
|
-
return unless metadata
|
|
565
|
-
|
|
566
|
-
font_names = font_names_for_profile
|
|
567
|
-
font_names.each do |name|
|
|
568
|
-
meta = metadata[name]
|
|
569
|
-
next unless meta
|
|
570
|
-
|
|
571
|
-
sig_data = meta["sig"] || {}
|
|
572
|
-
font = Wordprocessingml::Font.new(
|
|
573
|
-
name: name,
|
|
574
|
-
panose1: Wordprocessingml::Panose1.new(val: meta["panose1"]),
|
|
575
|
-
charset: Wordprocessingml::Charset.new(val: meta["charset"]),
|
|
576
|
-
family: Wordprocessingml::Family.new(val: meta["family"]),
|
|
577
|
-
pitch: Wordprocessingml::Pitch.new(val: meta["pitch"]),
|
|
578
|
-
sig: Wordprocessingml::Sig.new(
|
|
579
|
-
usb0: sig_data["usb0"], usb1: sig_data["usb1"],
|
|
580
|
-
usb2: sig_data["usb2"], usb3: sig_data["usb3"],
|
|
581
|
-
csb0: sig_data["csb0"], csb1: sig_data["csb1"]
|
|
582
|
-
),
|
|
583
|
-
)
|
|
584
|
-
|
|
585
|
-
font.alt_name = Wordprocessingml::AltName.new(val: meta["alt_name"]) if meta["alt_name"]
|
|
586
|
-
font_table.fonts << font
|
|
587
|
-
end
|
|
588
|
-
|
|
589
|
-
font_table.mc_ignorable = nil
|
|
590
|
-
record_fix("R13",
|
|
591
|
-
"Populated font table with profile fonts and signatures")
|
|
592
|
-
end
|
|
593
|
-
|
|
594
|
-
def reconcile_styles
|
|
595
|
-
return unless profile
|
|
596
|
-
|
|
597
|
-
styles = package.styles
|
|
598
|
-
styles ||= begin
|
|
599
|
-
package.styles = Wordprocessingml::StylesConfiguration.new(include_defaults: false)
|
|
600
|
-
package.styles
|
|
601
|
-
end
|
|
602
|
-
|
|
603
|
-
styles.doc_defaults ||= build_doc_defaults
|
|
604
|
-
styles.latent_styles ||= build_latent_styles
|
|
605
|
-
|
|
606
|
-
ensure_default_styles(styles)
|
|
607
|
-
|
|
608
|
-
styles.mc_ignorable = nil
|
|
609
|
-
record_fix("R10",
|
|
610
|
-
"Ensured styles have docDefaults, latentStyles, and default styles")
|
|
611
|
-
end
|
|
612
|
-
|
|
613
|
-
def reconcile_numbering
|
|
614
|
-
return unless profile
|
|
615
|
-
return unless package.numbering
|
|
616
|
-
|
|
617
|
-
package.numbering.mc_ignorable = nil
|
|
618
|
-
|
|
619
|
-
# Validate instance → definition references
|
|
620
|
-
package.numbering.instances.each do |inst|
|
|
621
|
-
next unless inst.abstract_num_id
|
|
622
|
-
|
|
623
|
-
abs_id = inst.abstract_num_id.is_a?(Uniword::Wordprocessingml::AbstractNumId) ? inst.abstract_num_id.val : inst.abstract_num_id
|
|
624
|
-
defn = package.numbering.definitions.find do |d|
|
|
625
|
-
d.abstract_num_id == abs_id
|
|
626
|
-
end
|
|
627
|
-
next if defn
|
|
628
|
-
|
|
629
|
-
record_fix("R4", "Numbering instance numId=#{inst.num_id} references " \
|
|
630
|
-
"missing abstractNumId=#{abs_id}")
|
|
631
|
-
end
|
|
632
|
-
end
|
|
633
|
-
|
|
634
|
-
def reconcile_web_settings
|
|
635
|
-
return unless profile
|
|
636
|
-
|
|
637
|
-
ws = package.web_settings
|
|
638
|
-
ws ||= begin
|
|
639
|
-
package.web_settings = Wordprocessingml::WebSettings.new
|
|
640
|
-
package.web_settings
|
|
641
|
-
end
|
|
642
|
-
|
|
643
|
-
ws.mc_ignorable = nil
|
|
644
|
-
record_fix("R1", "Cleared mc:Ignorable on webSettings")
|
|
645
|
-
end
|
|
646
|
-
|
|
647
|
-
def reconcile_app_properties
|
|
648
|
-
return unless profile
|
|
649
|
-
|
|
650
|
-
app = package.app_properties
|
|
651
|
-
app ||= begin
|
|
652
|
-
package.app_properties = Ooxml::AppProperties.new
|
|
653
|
-
package.app_properties
|
|
654
|
-
end
|
|
655
|
-
|
|
656
|
-
# Always assign to override lutaml-model's using_default tracking,
|
|
657
|
-
# which prevents serialization of attributes that equal their defaults.
|
|
658
|
-
app.template = "Normal.dotm"
|
|
659
|
-
app.application = profile.application_name
|
|
660
|
-
app.app_version = profile.app_version
|
|
661
|
-
app.company = profile.user_company if profile.user_company && !profile.user_company.empty?
|
|
662
|
-
|
|
663
|
-
# Calculate document statistics only when missing
|
|
664
|
-
unless app.pages && !app.pages.to_s.empty?
|
|
665
|
-
stats = calculate_document_statistics
|
|
666
|
-
app.pages = stats[:pages].to_s
|
|
667
|
-
app.words = stats[:words].to_s
|
|
668
|
-
app.characters = stats[:characters].to_s
|
|
669
|
-
app.characters_with_spaces = stats[:characters_with_spaces].to_s
|
|
670
|
-
app.paragraphs = stats[:paragraphs].to_s
|
|
671
|
-
app.lines = stats[:lines].to_s
|
|
672
|
-
end
|
|
673
|
-
|
|
674
|
-
# Re-assign to clear lutaml-model's @using_default tracking
|
|
675
|
-
# which otherwise omits these from XML output.
|
|
676
|
-
# Preserve source values when present.
|
|
677
|
-
app.total_time = app.total_time || "0"
|
|
678
|
-
app.scale_crop = app.scale_crop || "false"
|
|
679
|
-
app.doc_security = app.doc_security || "0"
|
|
680
|
-
app.links_up_to_date = app.links_up_to_date || "false"
|
|
681
|
-
app.shared_doc = app.shared_doc || "false"
|
|
682
|
-
app.hyperlinks_changed = app.hyperlinks_changed || "false"
|
|
683
|
-
|
|
684
|
-
# HeadingPairs and TitlesOfParts are NOT generated here.
|
|
685
|
-
# Word repairs files that have incorrect values, so we only
|
|
686
|
-
# preserve what was parsed from the source document.
|
|
687
|
-
record_fix("R8", "Ensured app properties with statistics")
|
|
688
|
-
end
|
|
689
|
-
|
|
690
|
-
def reconcile_core_properties
|
|
691
|
-
return unless profile
|
|
692
|
-
|
|
693
|
-
# Rebuild from parsed state to ensure namespace_scope
|
|
694
|
-
# declarations (e.g. xmlns:dcmitype) are applied on
|
|
695
|
-
# serialization (lutaml-model preserves parsed namespaces)
|
|
696
|
-
old_cp = package.core_properties
|
|
697
|
-
package.core_properties = if old_cp
|
|
698
|
-
Ooxml::CoreProperties.new(
|
|
699
|
-
title: old_cp.title,
|
|
700
|
-
subject: old_cp.subject,
|
|
701
|
-
creator: old_cp.creator,
|
|
702
|
-
keywords: old_cp.keywords,
|
|
703
|
-
description: old_cp.description,
|
|
704
|
-
last_modified_by: old_cp.last_modified_by,
|
|
705
|
-
revision: old_cp.revision,
|
|
706
|
-
created: old_cp.created,
|
|
707
|
-
modified: old_cp.modified,
|
|
708
|
-
)
|
|
709
|
-
else
|
|
710
|
-
Ooxml::CoreProperties.new
|
|
711
|
-
end
|
|
712
|
-
cp = package.core_properties
|
|
713
|
-
|
|
714
|
-
if profile.user_name && !profile.user_name.empty?
|
|
715
|
-
cp.last_modified_by = profile.user_name
|
|
716
|
-
cp.creator ||= profile.user_name
|
|
717
|
-
end
|
|
718
|
-
|
|
719
|
-
cp.last_modified_by ||= profile.application_name
|
|
720
|
-
|
|
721
|
-
now = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
722
|
-
cp.modified = Ooxml::Types::DctermsModifiedType.new(
|
|
723
|
-
value: now, type: "dcterms:W3CDTF",
|
|
724
|
-
)
|
|
725
|
-
cp.created ||= Ooxml::Types::DctermsCreatedType.new(
|
|
726
|
-
value: now, type: "dcterms:W3CDTF",
|
|
727
|
-
)
|
|
728
|
-
|
|
729
|
-
cp.revision = "1" unless cp.revision
|
|
730
|
-
record_fix("R14", "Rebuilt core properties with namespace declarations")
|
|
731
|
-
end
|
|
732
|
-
|
|
733
|
-
def reconcile_document_body
|
|
734
|
-
return unless profile
|
|
735
|
-
return unless package.document&.body
|
|
736
|
-
|
|
737
|
-
doc = package.document
|
|
738
|
-
doc.mc_ignorable = Ooxml::Types::McIgnorable.new("w14")
|
|
739
|
-
|
|
740
|
-
record_fix("R1", "Added mc:Ignorable to document body")
|
|
741
|
-
record_fix("R12", "Assigned rsid and paraId to paragraphs")
|
|
742
|
-
|
|
743
|
-
rsid = generate_rsid
|
|
744
|
-
body = doc.body
|
|
745
|
-
|
|
746
|
-
body.paragraphs.each do |para|
|
|
747
|
-
para.rsid_r ||= rsid
|
|
748
|
-
para.rsid_r_default ||= "00000000"
|
|
749
|
-
para.para_id ||= generate_hex_id
|
|
750
|
-
para.text_id ||= "77777777"
|
|
751
|
-
end
|
|
752
|
-
|
|
753
|
-
sect_pr = body.section_properties
|
|
754
|
-
return unless sect_pr
|
|
755
|
-
|
|
756
|
-
sect_pr.rsid_r ||= rsid
|
|
757
|
-
end
|
|
758
|
-
|
|
759
|
-
# -- Group 3: Package consistency (always) --
|
|
760
|
-
|
|
761
|
-
def reconcile_content_types
|
|
762
|
-
ct = package.content_types
|
|
763
|
-
return unless ct
|
|
764
|
-
|
|
765
|
-
# Defaults: only rels + xml (image types added by inject_image_parts)
|
|
766
|
-
ct.defaults = [
|
|
767
|
-
Uniword::ContentTypes::Default.new(
|
|
768
|
-
extension: "rels",
|
|
769
|
-
content_type: "application/vnd.openxmlformats-package.relationships+xml",
|
|
770
|
-
),
|
|
771
|
-
Uniword::ContentTypes::Default.new(
|
|
772
|
-
extension: "xml",
|
|
773
|
-
content_type: "application/xml",
|
|
774
|
-
),
|
|
775
|
-
]
|
|
776
|
-
|
|
777
|
-
# Overrides: rebuild for standard parts that exist
|
|
778
|
-
standard = content_type_overrides_for_present_parts
|
|
779
|
-
standard_parts = standard.to_set(&:part_name)
|
|
780
|
-
non_standard = ct.overrides.reject do |o|
|
|
781
|
-
standard_parts.include?(o.part_name)
|
|
782
|
-
end
|
|
783
|
-
|
|
784
|
-
ct.overrides = standard + non_standard
|
|
785
|
-
record_fix("R7", "Rebuilt content types for standard parts")
|
|
786
|
-
end
|
|
787
|
-
|
|
788
|
-
def reconcile_package_rels
|
|
789
|
-
rels = package.package_rels
|
|
790
|
-
return unless rels
|
|
791
|
-
|
|
792
|
-
base = "http://schemas.openxmlformats.org"
|
|
793
|
-
standard_defs = [
|
|
794
|
-
["rId1",
|
|
795
|
-
"#{base}/officeDocument/2006/relationships/officeDocument",
|
|
796
|
-
"word/document.xml"],
|
|
797
|
-
["rId2",
|
|
798
|
-
"#{base}/package/2006/relationships/metadata/core-properties",
|
|
799
|
-
"docProps/core.xml"],
|
|
800
|
-
["rId3",
|
|
801
|
-
"#{base}/officeDocument/2006/relationships/extended-properties",
|
|
802
|
-
"docProps/app.xml"],
|
|
803
|
-
]
|
|
804
|
-
|
|
805
|
-
standard_targets = standard_defs.to_set { |_, _, t| t }
|
|
806
|
-
standard_rids = standard_defs.to_set { |rid, _, _| rid }
|
|
807
|
-
non_standard = rels.relationships.reject do |r|
|
|
808
|
-
standard_targets.include?(r.target) || standard_rids.include?(r.id)
|
|
809
|
-
end
|
|
810
|
-
|
|
811
|
-
existing_by_target = rels.relationships.to_h { |r| [r.target, r] }
|
|
812
|
-
standard = standard_defs.map do |rid, type, target|
|
|
813
|
-
existing = existing_by_target[target]
|
|
814
|
-
build_rel(existing ? existing.id : rid, type, target)
|
|
815
|
-
end
|
|
816
|
-
|
|
817
|
-
rels.relationships = standard + non_standard
|
|
818
|
-
record_fix("R6", "Rebuilt package relationships for standard parts")
|
|
819
|
-
end
|
|
820
|
-
|
|
821
|
-
def reconcile_document_rels
|
|
822
|
-
rels = package.document_rels
|
|
823
|
-
return unless rels
|
|
824
|
-
|
|
825
|
-
base = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
826
|
-
defs = [
|
|
827
|
-
["rId1", "styles", "styles.xml", package.styles],
|
|
828
|
-
["rId2", "settings", "settings.xml", package.settings],
|
|
829
|
-
["rId3", "webSettings", "webSettings.xml", package.web_settings],
|
|
830
|
-
["rId4", "fontTable", "fontTable.xml", package.font_table],
|
|
831
|
-
["rId5", "theme", "theme/theme1.xml", package.theme],
|
|
832
|
-
["rId6", "numbering", "numbering.xml", package.numbering],
|
|
833
|
-
]
|
|
834
|
-
|
|
835
|
-
standard_targets = defs.filter_map do |_, _, target, obj|
|
|
836
|
-
target if obj
|
|
837
|
-
end.to_set
|
|
838
|
-
standard_rids = defs.filter_map { |rid, _, _, obj| rid if obj }.to_set
|
|
839
|
-
non_standard = rels.relationships.reject do |r|
|
|
840
|
-
standard_targets.include?(r.target) || standard_rids.include?(r.id)
|
|
841
|
-
end
|
|
842
|
-
|
|
843
|
-
# Reuse existing rIds for matching targets to avoid duplicates
|
|
844
|
-
existing_by_target = rels.relationships.to_h { |r| [r.target, r] }
|
|
845
|
-
standard = defs.filter_map do |_rid, suffix, target, obj|
|
|
846
|
-
next unless obj
|
|
847
|
-
|
|
848
|
-
existing = existing_by_target[target]
|
|
849
|
-
rid = existing ? existing.id : _rid
|
|
850
|
-
build_rel(rid, "#{base}/#{suffix}", target)
|
|
851
|
-
end
|
|
852
|
-
|
|
853
|
-
rels.relationships = standard + non_standard
|
|
854
|
-
record_fix("R6", "Rebuilt document relationships for standard parts")
|
|
855
|
-
end
|
|
856
|
-
|
|
857
|
-
# -- Helpers --
|
|
858
|
-
|
|
859
|
-
def calculate_document_statistics
|
|
860
|
-
DocumentStatistics.new(package).calculate
|
|
861
|
-
end
|
|
862
|
-
|
|
863
|
-
def generate_rsid
|
|
864
|
-
"00#{SecureRandom.hex(3).upcase}"
|
|
865
|
-
end
|
|
866
|
-
|
|
867
|
-
def generate_hex_id
|
|
868
|
-
SecureRandom.hex(4).upcase
|
|
869
|
-
end
|
|
870
|
-
|
|
871
|
-
def content_type_overrides_for_present_parts
|
|
872
|
-
checks = [
|
|
873
|
-
[package.document, "/word/document.xml",
|
|
874
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"],
|
|
875
|
-
[package.styles, "/word/styles.xml",
|
|
876
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"],
|
|
877
|
-
[package.settings, "/word/settings.xml",
|
|
878
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"],
|
|
879
|
-
[package.font_table, "/word/fontTable.xml",
|
|
880
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml"],
|
|
881
|
-
[package.web_settings, "/word/webSettings.xml",
|
|
882
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml"],
|
|
883
|
-
[package.theme, "/word/theme/theme1.xml",
|
|
884
|
-
"application/vnd.openxmlformats-officedocument.theme+xml"],
|
|
885
|
-
[package.core_properties, "/docProps/core.xml",
|
|
886
|
-
"application/vnd.openxmlformats-package.core-properties+xml"],
|
|
887
|
-
[package.app_properties, "/docProps/app.xml",
|
|
888
|
-
"application/vnd.openxmlformats-officedocument.extended-properties+xml"],
|
|
889
|
-
[package.footnotes, "/word/footnotes.xml",
|
|
890
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml"],
|
|
891
|
-
[package.endnotes, "/word/endnotes.xml",
|
|
892
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml"],
|
|
893
|
-
[package.numbering, "/word/numbering.xml",
|
|
894
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"],
|
|
895
|
-
]
|
|
896
|
-
|
|
897
|
-
checks.filter_map do |obj, part_name, content_type|
|
|
898
|
-
next unless obj
|
|
899
|
-
|
|
900
|
-
Uniword::ContentTypes::Override.new(
|
|
901
|
-
part_name: part_name,
|
|
902
|
-
content_type: content_type,
|
|
903
|
-
)
|
|
904
|
-
end
|
|
905
|
-
end
|
|
906
|
-
|
|
907
|
-
def build_rel(id, type, target)
|
|
908
|
-
Ooxml::Relationships::Relationship.new(
|
|
909
|
-
id: id, type: type, target: target,
|
|
910
|
-
)
|
|
911
|
-
end
|
|
912
|
-
|
|
913
|
-
def build_compat
|
|
914
|
-
Wordprocessingml::Compat.new(
|
|
915
|
-
use_fe_layout: Wordprocessingml::UseFELayout.new,
|
|
916
|
-
compatSetting: [
|
|
917
|
-
Wordprocessingml::CompatSetting.new(
|
|
918
|
-
name: "compatibilityMode",
|
|
919
|
-
uri: "http://schemas.microsoft.com/office/word",
|
|
920
|
-
val: profile.compat_mode,
|
|
921
|
-
),
|
|
922
|
-
Wordprocessingml::CompatSetting.new(
|
|
923
|
-
name: "overrideTableStyleFontSizeAndJustification",
|
|
924
|
-
uri: "http://schemas.microsoft.com/office/word",
|
|
925
|
-
val: "1",
|
|
926
|
-
),
|
|
927
|
-
Wordprocessingml::CompatSetting.new(
|
|
928
|
-
name: "enableOpenTypeFeatures",
|
|
929
|
-
uri: "http://schemas.microsoft.com/office/word",
|
|
930
|
-
val: "1",
|
|
931
|
-
),
|
|
932
|
-
Wordprocessingml::CompatSetting.new(
|
|
933
|
-
name: "doNotFlipMirrorIndents",
|
|
934
|
-
uri: "http://schemas.microsoft.com/office/word",
|
|
935
|
-
val: "1",
|
|
936
|
-
),
|
|
937
|
-
Wordprocessingml::CompatSetting.new(
|
|
938
|
-
name: "differentiateMultirowTableHeaders",
|
|
939
|
-
uri: "http://schemas.microsoft.com/office/word",
|
|
940
|
-
val: "1",
|
|
941
|
-
),
|
|
942
|
-
Wordprocessingml::CompatSetting.new(
|
|
943
|
-
name: "useWord2013TrackBottomHyphenation",
|
|
944
|
-
uri: "http://schemas.microsoft.com/office/word",
|
|
945
|
-
val: "0",
|
|
946
|
-
),
|
|
947
|
-
],
|
|
948
|
-
)
|
|
949
|
-
end
|
|
950
|
-
|
|
951
|
-
def build_rsids(rsid)
|
|
952
|
-
root = "00#{SecureRandom.hex(3).upcase}"
|
|
953
|
-
Wordprocessingml::Rsids.new(
|
|
954
|
-
rsid_root: Wordprocessingml::RsidRoot.new(val: root),
|
|
955
|
-
rsid: [Wordprocessingml::Rsid.new(val: rsid)],
|
|
956
|
-
)
|
|
957
|
-
end
|
|
958
|
-
|
|
959
|
-
def build_math_pr
|
|
960
|
-
Wordprocessingml::MathPr.new(
|
|
961
|
-
math_font: Wordprocessingml::MathFont.new(val: "Cambria Math"),
|
|
962
|
-
brk_bin: Wordprocessingml::BrkBin.new(val: "before"),
|
|
963
|
-
brk_bin_sub: Wordprocessingml::BrkBinSub.new(val: "--"),
|
|
964
|
-
small_frac: Wordprocessingml::SmallFrac.new(val: "0"),
|
|
965
|
-
disp_def: Wordprocessingml::DispDef.new,
|
|
966
|
-
l_margin: Wordprocessingml::LMargin.new(val: "0"),
|
|
967
|
-
r_margin: Wordprocessingml::RMargin.new(val: "0"),
|
|
968
|
-
def_jc: Wordprocessingml::DefJc.new(val: "centerGroup"),
|
|
969
|
-
wrap_indent: Wordprocessingml::WrapIndent.new(val: "1440"),
|
|
970
|
-
int_lim: Wordprocessingml::IntLim.new(val: "subSup"),
|
|
971
|
-
nary_lim: Wordprocessingml::NaryLim.new(val: "undOvr"),
|
|
972
|
-
)
|
|
973
|
-
end
|
|
974
|
-
|
|
975
|
-
def build_clr_scheme_mapping
|
|
976
|
-
Wordprocessingml::ClrSchemeMapping.new(
|
|
977
|
-
bg1: "light1", t1: "dark1", bg2: "light2", t2: "dark2",
|
|
978
|
-
accent1: "accent1", accent2: "accent2", accent3: "accent3",
|
|
979
|
-
accent4: "accent4", accent5: "accent5", accent6: "accent6",
|
|
980
|
-
hyperlink: "hyperlink", followed_hyperlink: "followedHyperlink"
|
|
981
|
-
)
|
|
982
|
-
end
|
|
983
|
-
|
|
984
|
-
def build_doc_defaults
|
|
985
|
-
r_pr = Wordprocessingml::RunProperties.new(
|
|
986
|
-
fonts: Properties::RunFonts.new(
|
|
987
|
-
ascii_theme: "minorHAnsi",
|
|
988
|
-
east_asia_theme: "minorEastAsia",
|
|
989
|
-
h_ansi_theme: "minorHAnsi",
|
|
990
|
-
cs_theme: "minorBidi",
|
|
991
|
-
),
|
|
992
|
-
kerning: Properties::Kerning.new(value: 2),
|
|
993
|
-
size: Properties::FontSize.new(value: 24),
|
|
994
|
-
size_cs: Properties::FontSize.new(value: 24),
|
|
995
|
-
language: Properties::Language.new(
|
|
996
|
-
val: profile.lang,
|
|
997
|
-
east_asia: profile.east_asia_lang,
|
|
998
|
-
bidi: profile.bidi_lang,
|
|
999
|
-
),
|
|
1000
|
-
ligatures: Uniword::Wordprocessingml2010::Ligatures.new(
|
|
1001
|
-
val: "standardContextual",
|
|
1002
|
-
),
|
|
1003
|
-
)
|
|
1004
|
-
|
|
1005
|
-
p_pr = Wordprocessingml::ParagraphProperties.new(
|
|
1006
|
-
spacing: Properties::Spacing.new(after: 160, line: 278,
|
|
1007
|
-
line_rule: "auto"),
|
|
1008
|
-
)
|
|
1009
|
-
|
|
1010
|
-
Wordprocessingml::DocDefaults.new(
|
|
1011
|
-
rPrDefault: Wordprocessingml::RPrDefault.new(rPr: r_pr),
|
|
1012
|
-
pPrDefault: Wordprocessingml::PPrDefault.new(pPr: p_pr),
|
|
1013
|
-
)
|
|
1014
|
-
end
|
|
1015
|
-
|
|
1016
|
-
def build_latent_styles
|
|
1017
|
-
config = load_latent_styles_config
|
|
1018
|
-
return Wordprocessingml::LatentStyles.new(count: 0) unless config
|
|
1019
|
-
|
|
1020
|
-
exceptions = (config["exceptions"] || []).map do |ex|
|
|
1021
|
-
attrs = { name: ex["name"] }
|
|
1022
|
-
attrs[:ui_priority] = ex["uiPriority"].to_i if ex["uiPriority"]
|
|
1023
|
-
attrs[:q_format] = ex["qFormat"] if ex["qFormat"]
|
|
1024
|
-
attrs[:semi_hidden] = ex["semiHidden"] if ex["semiHidden"]
|
|
1025
|
-
if ex["unhideWhenUsed"]
|
|
1026
|
-
attrs[:unhide_when_used] =
|
|
1027
|
-
ex["unhideWhenUsed"]
|
|
1028
|
-
end
|
|
1029
|
-
attrs[:locked] = ex["locked"] if ex["locked"]
|
|
1030
|
-
Wordprocessingml::LatentStylesException.new(attrs)
|
|
1031
|
-
end
|
|
1032
|
-
|
|
1033
|
-
Wordprocessingml::LatentStyles.new(
|
|
1034
|
-
def_locked_state: config["defLockedState"],
|
|
1035
|
-
def_ui_priority: config["defUIPriority"].to_i,
|
|
1036
|
-
def_semi_hidden: config["defSemiHidden"],
|
|
1037
|
-
def_unhide_when_used: config["defUnhideWhenUsed"],
|
|
1038
|
-
def_q_format: config["defQFormat"],
|
|
1039
|
-
count: config["count"].to_i,
|
|
1040
|
-
lsd_exception: exceptions,
|
|
1041
|
-
)
|
|
1042
|
-
end
|
|
1043
|
-
|
|
1044
|
-
def ensure_default_styles(styles)
|
|
1045
|
-
style_ids = styles.styles.to_set(&:id)
|
|
1046
|
-
|
|
1047
|
-
unless style_ids.include?("Normal")
|
|
1048
|
-
styles.add_style(Wordprocessingml::Style.new(
|
|
1049
|
-
type: "paragraph", default: true, styleId: "Normal",
|
|
1050
|
-
name: Wordprocessingml::StyleName.new(val: "Normal"),
|
|
1051
|
-
qFormat: Properties::QuickFormat.new
|
|
1052
|
-
))
|
|
1053
|
-
end
|
|
1054
|
-
|
|
1055
|
-
unless style_ids.include?("DefaultParagraphFont")
|
|
1056
|
-
styles.add_style(Wordprocessingml::Style.new(
|
|
1057
|
-
type: "character", default: true, styleId: "DefaultParagraphFont",
|
|
1058
|
-
name: Wordprocessingml::StyleName.new(val: "Default Paragraph Font"),
|
|
1059
|
-
uiPriority: Wordprocessingml::UiPriority.new(val: 1),
|
|
1060
|
-
semiHidden: Wordprocessingml::SemiHidden.new,
|
|
1061
|
-
unhideWhenUsed: Wordprocessingml::UnhideWhenUsed.new
|
|
1062
|
-
))
|
|
1063
|
-
end
|
|
1064
|
-
|
|
1065
|
-
unless style_ids.include?("TableNormal")
|
|
1066
|
-
tbl_pr = Wordprocessingml::TableProperties.new(
|
|
1067
|
-
table_indent: Properties::TableIndent.new(value: 0, type: "dxa"),
|
|
1068
|
-
table_cell_margin: Properties::TableCellMargin.new(
|
|
1069
|
-
top: Properties::Margin.new(w: 0, type: "dxa"),
|
|
1070
|
-
left: Properties::Margin.new(w: 108, type: "dxa"),
|
|
1071
|
-
bottom: Properties::Margin.new(w: 0, type: "dxa"),
|
|
1072
|
-
right: Properties::Margin.new(w: 108, type: "dxa"),
|
|
1073
|
-
),
|
|
1074
|
-
)
|
|
1075
|
-
|
|
1076
|
-
styles.add_style(Wordprocessingml::Style.new(
|
|
1077
|
-
type: "table", default: true, styleId: "TableNormal",
|
|
1078
|
-
name: Wordprocessingml::StyleName.new(val: "Normal Table"),
|
|
1079
|
-
uiPriority: Wordprocessingml::UiPriority.new(val: 99),
|
|
1080
|
-
semiHidden: Wordprocessingml::SemiHidden.new,
|
|
1081
|
-
unhideWhenUsed: Wordprocessingml::UnhideWhenUsed.new,
|
|
1082
|
-
tblPr: tbl_pr
|
|
1083
|
-
))
|
|
1084
|
-
end
|
|
1085
|
-
|
|
1086
|
-
return if style_ids.include?("NoList")
|
|
1087
|
-
|
|
1088
|
-
styles.add_style(Wordprocessingml::Style.new(
|
|
1089
|
-
type: "numbering", default: true, styleId: "NoList",
|
|
1090
|
-
name: Wordprocessingml::StyleName.new(val: "No List"),
|
|
1091
|
-
uiPriority: Wordprocessingml::UiPriority.new(val: 99),
|
|
1092
|
-
semiHidden: Wordprocessingml::SemiHidden.new,
|
|
1093
|
-
unhideWhenUsed: Wordprocessingml::UnhideWhenUsed.new
|
|
1094
|
-
))
|
|
1095
|
-
end
|
|
1096
|
-
|
|
1097
|
-
def load_font_metadata
|
|
1098
|
-
path = File.join(CONFIG_DIR, "font_metadata.yml")
|
|
1099
|
-
YAML.load_file(path)["fonts"]
|
|
1100
|
-
rescue StandardError => e
|
|
1101
|
-
Uniword.logger&.warn { "Font metadata load failed: #{e.message}" }
|
|
1102
|
-
nil
|
|
1103
|
-
end
|
|
1104
|
-
|
|
1105
|
-
def load_latent_styles_config
|
|
1106
|
-
path = File.join(CONFIG_DIR, "latent_styles.yml")
|
|
1107
|
-
YAML.load_file(path)
|
|
1108
|
-
rescue StandardError => e
|
|
1109
|
-
Uniword.logger&.warn do
|
|
1110
|
-
"Latent styles config load failed: #{e.message}"
|
|
1111
|
-
end
|
|
1112
|
-
nil
|
|
1113
|
-
end
|
|
1114
|
-
|
|
1115
|
-
def font_names_for_profile
|
|
1116
|
-
names = []
|
|
1117
|
-
fs = profile.system.font_scheme
|
|
1118
|
-
loc = profile.locale
|
|
1119
|
-
|
|
1120
|
-
# Order matches Word's canonical output:
|
|
1121
|
-
# 1. Minor font (body), 2. East Asian font, 3. Legacy serif,
|
|
1122
|
-
# 4. East Asian light font, 5. Major font (headings)
|
|
1123
|
-
names << fs&.minor_font if fs&.minor_font
|
|
1124
|
-
|
|
1125
|
-
ea_font = loc&.east_asian_font
|
|
1126
|
-
ea_light = loc&.east_asian_light_font
|
|
1127
|
-
|
|
1128
|
-
# Default East Asian fonts for zh-CN when locale profile omits them
|
|
1129
|
-
if loc.east_asia_lang == "zh-CN"
|
|
1130
|
-
ea_font ||= "DengXian"
|
|
1131
|
-
ea_light ||= "DengXian Light"
|
|
1132
|
-
end
|
|
1133
|
-
|
|
1134
|
-
names << ea_font if ea_font
|
|
1135
|
-
names << "Times New Roman"
|
|
1136
|
-
names << ea_light if ea_light
|
|
1137
|
-
names << fs&.major_font if fs&.major_font
|
|
1138
|
-
|
|
1139
|
-
names.uniq
|
|
114
|
+
parts.each(&:clear_xml_parse_state!)
|
|
1140
115
|
end
|
|
1141
116
|
end
|
|
1142
117
|
end
|