uniword 1.0.11 → 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 -25
- 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 +15 -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 -1097
- 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/ooxml_to_html_converter.rb +3 -5
- 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 +51 -34
- 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 +4 -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 +46 -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
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Docx
|
|
5
|
+
class Reconciler
|
|
6
|
+
# Footnote and endnote reconciliation.
|
|
7
|
+
#
|
|
8
|
+
# Ensures notes are structurally valid with separators, proper ordering
|
|
9
|
+
# and sequential IDs. Creates missing notes/settings pairs and strips
|
|
10
|
+
# invalid types.
|
|
11
|
+
module Notes
|
|
12
|
+
def reconcile_note_references
|
|
13
|
+
[:footnote, :endnote].each do |type|
|
|
14
|
+
ref_ids = collect_note_ids(type)
|
|
15
|
+
next if ref_ids.empty?
|
|
16
|
+
|
|
17
|
+
current = notes_collection_for(type)
|
|
18
|
+
|
|
19
|
+
if current.nil?
|
|
20
|
+
ensure_notes_part(type)
|
|
21
|
+
current = notes_collection_for(type)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
defined_set = note_entries_for(current, type).map(&:id).to_set
|
|
25
|
+
|
|
26
|
+
missing = ref_ids.reject { |id| defined_set.include?(id) }
|
|
27
|
+
next if missing.empty?
|
|
28
|
+
|
|
29
|
+
entries = note_entries_for(current, type)
|
|
30
|
+
missing.each do |id|
|
|
31
|
+
entries << entry_class(type).new(
|
|
32
|
+
id: id,
|
|
33
|
+
paragraphs: [Wordprocessingml::Paragraph.new(
|
|
34
|
+
runs: [Wordprocessingml::Run.new(
|
|
35
|
+
text: Wordprocessingml::Text.new(content: " ")
|
|
36
|
+
)]
|
|
37
|
+
)],
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
record_fix(FixCodes::NOTE_DEFINITION_CREATED,
|
|
41
|
+
"Created missing #{type}note definitions for ids=#{missing.join(', ')}")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def reconcile_footnotes
|
|
46
|
+
reconcile_notes_type(
|
|
47
|
+
notes: package.footnotes,
|
|
48
|
+
notes_setter: ->(v) { package.footnotes = v },
|
|
49
|
+
has_pr: package.settings&.footnote_pr,
|
|
50
|
+
type: :footnote,
|
|
51
|
+
create_message: "Created footnotes.xml to match footnotePr in settings",
|
|
52
|
+
pr_message: "Added footnotePr to settings to match footnotes.xml",
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def reconcile_endnotes
|
|
57
|
+
reconcile_notes_type(
|
|
58
|
+
notes: package.endnotes,
|
|
59
|
+
notes_setter: ->(v) { package.endnotes = v },
|
|
60
|
+
has_pr: package.settings&.endnote_pr,
|
|
61
|
+
type: :endnote,
|
|
62
|
+
create_message: "Created endnotes.xml to match endnotePr in settings",
|
|
63
|
+
pr_message: "Added endnotePr to settings to match endnotes.xml",
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def reconcile_notes_type(notes:, notes_setter:, has_pr:, type:,
|
|
70
|
+
create_message:, pr_message:)
|
|
71
|
+
if has_pr && !notes
|
|
72
|
+
notes_setter.call(minimal_notes(type))
|
|
73
|
+
record_fix(FixCodes::NOTE_PAIR_CREATED, create_message)
|
|
74
|
+
elsif notes && !has_pr
|
|
75
|
+
package.settings ||= Wordprocessingml::Settings.new
|
|
76
|
+
assign_note_pr(package.settings, type)
|
|
77
|
+
record_fix(FixCodes::NOTE_PAIR_CREATED, pr_message)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
current = notes_collection_for(type)
|
|
81
|
+
return unless current
|
|
82
|
+
|
|
83
|
+
entries = note_entries_for(current, type)
|
|
84
|
+
|
|
85
|
+
finalize_notes(current, entries, type)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def finalize_notes(notes, entries, type)
|
|
89
|
+
ensure_separators(notes, type, entries)
|
|
90
|
+
strip_invalid_note_types(entries, type)
|
|
91
|
+
deduplicate_note_ids(entries, type)
|
|
92
|
+
strip_empty_runs_from_notes(entries)
|
|
93
|
+
reorder_notes_by_reference(entries, type)
|
|
94
|
+
|
|
95
|
+
unless allocator
|
|
96
|
+
prefix = type == :footnote ? "fn" : "en"
|
|
97
|
+
|
|
98
|
+
entries.each_with_index do |entry, eidx|
|
|
99
|
+
backfill_paragraphs(entry.paragraphs, generate_rsid,
|
|
100
|
+
"#{prefix}:#{eidx}")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
renumber_notes(entries, type)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
set_mc_ignorable(notes, prefixes: FULL_IGNORABLE)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def minimal_notes(type)
|
|
110
|
+
entries = [separator_entry(type), continuation_entry(type)]
|
|
111
|
+
build_notes_collection(type, entries: entries)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def entry_class(type)
|
|
115
|
+
type == :footnote ? Wordprocessingml::Footnote : Wordprocessingml::Endnote
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def separator_entry(type)
|
|
119
|
+
entry_class(type).new(
|
|
120
|
+
id: "-1", type: "separator",
|
|
121
|
+
paragraphs: [separator_paragraph(:separator)]
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def continuation_entry(type)
|
|
126
|
+
entry_class(type).new(
|
|
127
|
+
id: "0", type: "continuationSeparator",
|
|
128
|
+
paragraphs: [separator_paragraph(:continuation)]
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def separator_paragraph(kind = :separator)
|
|
133
|
+
sep_attr = kind == :separator ? :separator_char : :continuation_separator_char
|
|
134
|
+
sep_class = kind == :separator ? Wordprocessingml::SeparatorChar : Wordprocessingml::ContinuationSeparatorChar
|
|
135
|
+
sep_run = Wordprocessingml::Run.new(sep_attr => sep_class.new)
|
|
136
|
+
Wordprocessingml::Paragraph.new(
|
|
137
|
+
properties: Wordprocessingml::ParagraphProperties.new(
|
|
138
|
+
spacing: Properties::Spacing.new(after: 0, line: 240,
|
|
139
|
+
line_rule: "auto"),
|
|
140
|
+
),
|
|
141
|
+
runs: [sep_run]
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def ensure_separators(notes, type, entries)
|
|
146
|
+
entries.each do |entry|
|
|
147
|
+
next unless %w[separator continuationSeparator].include?(entry.type)
|
|
148
|
+
next if entry.paragraphs.empty?
|
|
149
|
+
|
|
150
|
+
entry.paragraphs.each do |p|
|
|
151
|
+
strip_empty_runs(p)
|
|
152
|
+
ensure_separator_run(p, entry.type)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
ids = entries.to_set(&:id)
|
|
157
|
+
entries.unshift(separator_entry(type)) unless ids.include?("-1")
|
|
158
|
+
entries.unshift(continuation_entry(type)) unless ids.include?("0")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def ensure_separator_run(para, type)
|
|
162
|
+
has_sep = para.runs.any? { |r| r.separator_char || r.continuation_separator_char }
|
|
163
|
+
return if has_sep
|
|
164
|
+
|
|
165
|
+
sep = if type == "separator"
|
|
166
|
+
Wordprocessingml::Run.new(separator_char: Wordprocessingml::SeparatorChar.new)
|
|
167
|
+
else
|
|
168
|
+
Wordprocessingml::Run.new(continuation_separator_char: Wordprocessingml::ContinuationSeparatorChar.new)
|
|
169
|
+
end
|
|
170
|
+
para.runs << sep
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def strip_invalid_note_types(entries, type)
|
|
174
|
+
stripped = []
|
|
175
|
+
entries.each do |entry|
|
|
176
|
+
next if VALID_NOTE_TYPES.include?(entry.type)
|
|
177
|
+
next unless entry.type
|
|
178
|
+
|
|
179
|
+
stripped << entry.id
|
|
180
|
+
entry.type = nil
|
|
181
|
+
end
|
|
182
|
+
return if stripped.empty?
|
|
183
|
+
|
|
184
|
+
record_fix(FixCodes::NOTE_INVALID_TYPE_STRIPPED,
|
|
185
|
+
"Stripped invalid w:type from #{type}note ids=#{stripped.join(', ')}")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def deduplicate_note_ids(entries, type)
|
|
189
|
+
seen = {}
|
|
190
|
+
dup_ids = []
|
|
191
|
+
entries.reject! do |entry|
|
|
192
|
+
if seen[entry.id]
|
|
193
|
+
dup_ids << entry.id
|
|
194
|
+
true
|
|
195
|
+
else
|
|
196
|
+
seen[entry.id] = true
|
|
197
|
+
false
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
return if dup_ids.empty?
|
|
201
|
+
|
|
202
|
+
record_fix(FixCodes::NOTE_DUPLICATE_ID_REMOVED,
|
|
203
|
+
"Removed duplicate #{type}note ids=#{dup_ids.join(', ')}")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def reorder_notes_by_reference(entries, type)
|
|
207
|
+
body = package.document&.body
|
|
208
|
+
return unless body
|
|
209
|
+
|
|
210
|
+
ref_order = collect_note_reference_order(body, type)
|
|
211
|
+
return if ref_order.empty?
|
|
212
|
+
|
|
213
|
+
structural = entries.select { |e| VALID_NOTE_TYPES.include?(e.type) }
|
|
214
|
+
user_entries = entries.reject { |e| VALID_NOTE_TYPES.include?(e.type) }
|
|
215
|
+
return if user_entries.size <= 1
|
|
216
|
+
|
|
217
|
+
by_id = user_entries.group_by(&:id)
|
|
218
|
+
reordered = ref_order.filter_map { |id| by_id[id]&.first }
|
|
219
|
+
|
|
220
|
+
referenced_ids = ref_order.to_set
|
|
221
|
+
unreferenced = user_entries.reject { |e| referenced_ids.include?(e.id) }
|
|
222
|
+
reordered.concat(unreferenced)
|
|
223
|
+
|
|
224
|
+
return if reordered == user_entries
|
|
225
|
+
|
|
226
|
+
entries.clear
|
|
227
|
+
structural.each { |e| entries << e }
|
|
228
|
+
reordered.each { |e| entries << e }
|
|
229
|
+
|
|
230
|
+
record_fix(FixCodes::NOTE_PAIR_CREATED, "Reordered #{type}notes by first reference in document body")
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def renumber_notes(entries, type)
|
|
234
|
+
user_entries = entries.reject { |e| VALID_NOTE_TYPES.include?(e.type) }
|
|
235
|
+
return if user_entries.size <= 1
|
|
236
|
+
|
|
237
|
+
id_map = {}
|
|
238
|
+
user_entries.each_with_index do |entry, idx|
|
|
239
|
+
old_id = entry.id
|
|
240
|
+
new_id = (idx + 1).to_s
|
|
241
|
+
next if old_id == new_id
|
|
242
|
+
id_map[old_id] = new_id
|
|
243
|
+
entry.id = new_id
|
|
244
|
+
end
|
|
245
|
+
return if id_map.empty?
|
|
246
|
+
|
|
247
|
+
body = package.document&.body
|
|
248
|
+
return unless body
|
|
249
|
+
|
|
250
|
+
walk_body_paragraphs(body) do |para|
|
|
251
|
+
para.runs.each do |run|
|
|
252
|
+
ref = note_reference_from_run(run, type)
|
|
253
|
+
next unless ref && ref.id && id_map.key?(ref.id)
|
|
254
|
+
ref.id = id_map[ref.id]
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
record_fix(FixCodes::NOTE_PAIR_CREATED, "Renumbered #{type}note IDs sequentially (#{id_map.size} changed)")
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def collect_note_reference_order(body, type)
|
|
262
|
+
seen = []
|
|
263
|
+
|
|
264
|
+
walk_body_paragraphs(body) do |para|
|
|
265
|
+
para.runs.each do |run|
|
|
266
|
+
ref = note_reference_from_run(run, type)
|
|
267
|
+
next unless ref && ref.id
|
|
268
|
+
seen << ref.id unless seen.include?(ref.id)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
seen
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# note_reference_from_run lives in Helpers (single source for note-type dispatch).
|
|
276
|
+
|
|
277
|
+
def collect_note_ids(type)
|
|
278
|
+
ids = []
|
|
279
|
+
body = package.document&.body
|
|
280
|
+
return ids unless body
|
|
281
|
+
|
|
282
|
+
walk_body_paragraphs(body) do |para|
|
|
283
|
+
para.runs.each do |run|
|
|
284
|
+
ref = note_reference_from_run(run, type)
|
|
285
|
+
ids << ref.id if ref&.id
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
ids
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def ensure_notes_part(type)
|
|
292
|
+
notes = build_notes_collection(
|
|
293
|
+
type,
|
|
294
|
+
entries: [separator_entry(type), continuation_entry(type)],
|
|
295
|
+
)
|
|
296
|
+
set_notes_collection(notes, type)
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Docx
|
|
5
|
+
class Reconciler
|
|
6
|
+
# Package-level consistency enforcement.
|
|
7
|
+
#
|
|
8
|
+
# Rebuilds content types and relationships for standard parts,
|
|
9
|
+
# preserving non-standard entries from the source document.
|
|
10
|
+
module PackageStructure
|
|
11
|
+
# Relationship types for parts we don't model or serialize.
|
|
12
|
+
UNSUPPORTED_REL_TYPES = Set[
|
|
13
|
+
"http://schemas.microsoft.com/office/2007/relationships/stylesWithEffects",
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
# Rel types that belong in package-level _rels/.rels, not document.xml.rels.
|
|
17
|
+
PACKAGE_LEVEL_REL_TYPES = Set[
|
|
18
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
|
|
19
|
+
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
|
|
20
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties",
|
|
21
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties",
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
def reconcile_content_types
|
|
25
|
+
ct = package.content_types
|
|
26
|
+
return unless ct
|
|
27
|
+
|
|
28
|
+
ct.defaults = [
|
|
29
|
+
Uniword::ContentTypes::Default.new(
|
|
30
|
+
extension: "rels",
|
|
31
|
+
content_type: "application/vnd.openxmlformats-package.relationships+xml",
|
|
32
|
+
),
|
|
33
|
+
Uniword::ContentTypes::Default.new(
|
|
34
|
+
extension: "xml",
|
|
35
|
+
content_type: "application/xml",
|
|
36
|
+
),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
standard = content_type_overrides_for_present_parts
|
|
40
|
+
standard_parts = standard.to_set(&:part_name)
|
|
41
|
+
non_standard = ct.overrides.reject do |o|
|
|
42
|
+
standard_parts.include?(o.part_name)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
ct.overrides = standard + non_standard
|
|
46
|
+
record_fix(FixCodes::CONTENT_TYPES_ASSEMBLED, "Rebuilt content types for standard parts")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def reconcile_package_rels
|
|
50
|
+
rels = package.package_rels
|
|
51
|
+
return unless rels
|
|
52
|
+
|
|
53
|
+
base = "http://schemas.openxmlformats.org"
|
|
54
|
+
standard_defs = [
|
|
55
|
+
["rId1",
|
|
56
|
+
"#{base}/officeDocument/2006/relationships/officeDocument",
|
|
57
|
+
"word/document.xml"],
|
|
58
|
+
["rId2",
|
|
59
|
+
"#{base}/package/2006/relationships/metadata/core-properties",
|
|
60
|
+
"docProps/core.xml"],
|
|
61
|
+
["rId3",
|
|
62
|
+
"#{base}/officeDocument/2006/relationships/extended-properties",
|
|
63
|
+
"docProps/app.xml"],
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
standard_targets = standard_defs.to_set { |_, _, t| t }
|
|
67
|
+
standard_rids = standard_defs.to_set { |rid, _, _| rid }
|
|
68
|
+
non_standard = rels.relationships.reject do |r|
|
|
69
|
+
standard_targets.include?(r.target) || standard_rids.include?(r.id)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
existing_by_target = rels.relationships.to_h { |r| [r.target, r] }
|
|
73
|
+
standard = standard_defs.map do |rid, type, target|
|
|
74
|
+
existing = existing_by_target[target]
|
|
75
|
+
build_rel(existing ? existing.id : rid, type, target)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
rels.relationships = standard + non_standard
|
|
79
|
+
record_fix(FixCodes::RELATIONSHIPS_ASSEMBLED, "Rebuilt package relationships for standard parts")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def reconcile_document_rels
|
|
83
|
+
rels = package.document_rels
|
|
84
|
+
return unless rels
|
|
85
|
+
|
|
86
|
+
base = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
87
|
+
defs = [
|
|
88
|
+
["styles", "styles.xml", package.styles],
|
|
89
|
+
["settings", "settings.xml", package.settings],
|
|
90
|
+
["webSettings", "webSettings.xml", package.web_settings],
|
|
91
|
+
["fontTable", "fontTable.xml", package.font_table],
|
|
92
|
+
["theme", "theme/theme1.xml", package.theme],
|
|
93
|
+
["numbering", "numbering.xml", package.numbering],
|
|
94
|
+
["footnotes", "footnotes.xml", package.footnotes],
|
|
95
|
+
["endnotes", "endnotes.xml", package.endnotes],
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
standard_targets = defs.filter_map { |_, target, obj| target if obj }.to_set
|
|
99
|
+
|
|
100
|
+
# If allocator is present, use it to build rels — preserves existing rIds
|
|
101
|
+
alloc = allocator
|
|
102
|
+
if alloc
|
|
103
|
+
reconcile_document_rels_from_allocator(rels, base, defs, standard_targets, alloc)
|
|
104
|
+
else
|
|
105
|
+
reconcile_document_rels_legacy(rels, base, defs, standard_targets)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def reconcile_document_rels_from_allocator(rels, base, defs, standard_targets, alloc)
|
|
112
|
+
# Collect standard part rels from allocator
|
|
113
|
+
all_rels = []
|
|
114
|
+
defs.each do |suffix, target, obj|
|
|
115
|
+
next unless obj
|
|
116
|
+
r_id = alloc.rid_for(target: target, type: "#{base}/#{suffix}")
|
|
117
|
+
if r_id
|
|
118
|
+
all_rels << build_rel(r_id, "#{base}/#{suffix}", target)
|
|
119
|
+
else
|
|
120
|
+
all_rels << build_rel(
|
|
121
|
+
alloc.alloc_rid(target: target, type: "#{base}/#{suffix}"),
|
|
122
|
+
"#{base}/#{suffix}", target,
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Add allocator-managed rels (images, headers, footers, hyperlinks)
|
|
128
|
+
alloc.all_rels.each do |entry|
|
|
129
|
+
next if standard_targets.include?(entry[:target])
|
|
130
|
+
next if all_rels.any? { |r| r.target == entry[:target] }
|
|
131
|
+
next if unsupported_rel_type?(entry[:type])
|
|
132
|
+
next if package_level_rel?(entry[:type])
|
|
133
|
+
next unless header_footer_target_present?(entry[:target])
|
|
134
|
+
|
|
135
|
+
all_rels << build_rel(
|
|
136
|
+
entry[:id], entry[:type], entry[:target],
|
|
137
|
+
target_mode: entry[:target_mode],
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Preserve non-standard rels not managed by allocator
|
|
142
|
+
existing_targets = all_rels.to_set(&:target)
|
|
143
|
+
non_standard = rels.relationships.reject do |r|
|
|
144
|
+
existing_targets.include?(r.target) ||
|
|
145
|
+
standard_targets.include?(r.target) ||
|
|
146
|
+
unsupported_rel_type?(r.type) ||
|
|
147
|
+
package_level_rel?(r.type) ||
|
|
148
|
+
!header_footer_target_present?(r.target)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
rels.relationships = all_rels + non_standard
|
|
152
|
+
record_fix(FixCodes::RELATIONSHIPS_ASSEMBLED, "Assembled document relationships from allocator")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def reconcile_document_rels_legacy(rels, base, defs, standard_targets)
|
|
156
|
+
non_standard = rels.relationships.reject do |r|
|
|
157
|
+
standard_targets.include?(r.target) ||
|
|
158
|
+
unsupported_rel_type?(r.type) ||
|
|
159
|
+
package_level_rel?(r.type) ||
|
|
160
|
+
!header_footer_target_present?(r.target)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
all_rels = []
|
|
164
|
+
rid_mapping = {}
|
|
165
|
+
|
|
166
|
+
defs.each do |suffix, target, obj|
|
|
167
|
+
next unless obj
|
|
168
|
+
rid = "rId#{all_rels.size + 1}"
|
|
169
|
+
all_rels << build_rel(rid, "#{base}/#{suffix}", target)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
non_standard.each do |rel|
|
|
173
|
+
old_rid = rel.id
|
|
174
|
+
new_rid = "rId#{all_rels.size + 1}"
|
|
175
|
+
rid_mapping[old_rid] = new_rid if old_rid != new_rid
|
|
176
|
+
all_rels << build_rel(new_rid, rel.type, rel.target,
|
|
177
|
+
target_mode: rel.target_mode)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
rels.relationships = all_rels
|
|
181
|
+
update_sect_pr_rid_references(rid_mapping) unless rid_mapping.empty?
|
|
182
|
+
update_blip_embed_references(rid_mapping) unless rid_mapping.empty?
|
|
183
|
+
update_hyperlink_rid_references(rid_mapping) unless rid_mapping.empty?
|
|
184
|
+
record_fix(FixCodes::RELATIONSHIPS_ASSEMBLED, "Rebuilt document relationships with sequential rIds")
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def update_sect_pr_rid_references(mapping)
|
|
188
|
+
sect_pr = package.document&.body&.section_properties
|
|
189
|
+
return unless sect_pr
|
|
190
|
+
|
|
191
|
+
[sect_pr.header_references, sect_pr.footer_references].each do |refs|
|
|
192
|
+
next unless refs
|
|
193
|
+
|
|
194
|
+
refs.each do |ref|
|
|
195
|
+
new_rid = mapping[ref.r_id]
|
|
196
|
+
ref.r_id = new_rid if new_rid
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def update_blip_embed_references(mapping)
|
|
202
|
+
paragraphs = package.document&.body&.paragraphs
|
|
203
|
+
return unless paragraphs
|
|
204
|
+
|
|
205
|
+
paragraphs.each do |para|
|
|
206
|
+
next unless para.runs
|
|
207
|
+
|
|
208
|
+
para.runs.each do |run|
|
|
209
|
+
next unless run.drawings
|
|
210
|
+
|
|
211
|
+
run.drawings.each do |drawing|
|
|
212
|
+
update_drawing_blip(drawing, mapping)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def update_drawing_blip(drawing, mapping)
|
|
219
|
+
graphic = drawing.inline&.graphic || drawing.anchor&.graphic
|
|
220
|
+
return unless graphic
|
|
221
|
+
|
|
222
|
+
picture = graphic.graphic_data&.picture
|
|
223
|
+
return unless picture
|
|
224
|
+
|
|
225
|
+
blip = picture.blip_fill&.blip
|
|
226
|
+
return unless blip&.embed
|
|
227
|
+
|
|
228
|
+
new_rid = mapping[blip.embed.to_s]
|
|
229
|
+
blip.embed = new_rid if new_rid
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def update_hyperlink_rid_references(mapping)
|
|
233
|
+
body = package.document&.body
|
|
234
|
+
return unless body
|
|
235
|
+
|
|
236
|
+
walk_body_paragraphs(body) do |para|
|
|
237
|
+
(para.hyperlinks || []).each do |hl|
|
|
238
|
+
next unless hl.id
|
|
239
|
+
new_rid = mapping[hl.id.to_s]
|
|
240
|
+
hl.id = new_rid if new_rid
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def unsupported_rel_type?(type)
|
|
246
|
+
UNSUPPORTED_REL_TYPES.include?(type.to_s)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def package_level_rel?(type)
|
|
250
|
+
PACKAGE_LEVEL_REL_TYPES.include?(type.to_s)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Check if a header/footer rel target corresponds to a serialized part.
|
|
254
|
+
# Headers/footers may be in two places:
|
|
255
|
+
# - document.headers / document.footers (single-section hash)
|
|
256
|
+
# - document.header_footer_parts (multi-section ordered array)
|
|
257
|
+
# Headers/footers are numbered sequentially: header1.xml, header2.xml...
|
|
258
|
+
# If the model has fewer headers/footers than the target index, it's stale.
|
|
259
|
+
def header_footer_target_present?(target)
|
|
260
|
+
if target.start_with?("header") && target.end_with?(".xml")
|
|
261
|
+
num = target[/header(\d+)\.xml/, 1]&.to_i
|
|
262
|
+
hash_count = package.document&.headers&.size || 0
|
|
263
|
+
parts_count = count_parts_matching("header")
|
|
264
|
+
count = [hash_count, parts_count].max
|
|
265
|
+
return num && num <= count
|
|
266
|
+
end
|
|
267
|
+
if target.start_with?("footer") && target.end_with?(".xml")
|
|
268
|
+
num = target[/footer(\d+)\.xml/, 1]&.to_i
|
|
269
|
+
hash_count = package.document&.footers&.size || 0
|
|
270
|
+
parts_count = count_parts_matching("footer")
|
|
271
|
+
count = [hash_count, parts_count].max
|
|
272
|
+
return num && num <= count
|
|
273
|
+
end
|
|
274
|
+
true
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def count_parts_matching(prefix)
|
|
278
|
+
parts = package.document&.header_footer_parts
|
|
279
|
+
return 0 unless parts
|
|
280
|
+
|
|
281
|
+
parts.count { |p| p[:target].to_s.start_with?(prefix) }
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def content_type_overrides_for_present_parts
|
|
285
|
+
checks = [
|
|
286
|
+
[package.document, "/word/document.xml",
|
|
287
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"],
|
|
288
|
+
[package.styles, "/word/styles.xml",
|
|
289
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"],
|
|
290
|
+
[package.settings, "/word/settings.xml",
|
|
291
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"],
|
|
292
|
+
[package.font_table, "/word/fontTable.xml",
|
|
293
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml"],
|
|
294
|
+
[package.web_settings, "/word/webSettings.xml",
|
|
295
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml"],
|
|
296
|
+
[package.theme, "/word/theme/theme1.xml",
|
|
297
|
+
"application/vnd.openxmlformats-officedocument.theme+xml"],
|
|
298
|
+
[package.core_properties, "/docProps/core.xml",
|
|
299
|
+
"application/vnd.openxmlformats-package.core-properties+xml"],
|
|
300
|
+
[package.app_properties, "/docProps/app.xml",
|
|
301
|
+
"application/vnd.openxmlformats-officedocument.extended-properties+xml"],
|
|
302
|
+
[package.footnotes, "/word/footnotes.xml",
|
|
303
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml"],
|
|
304
|
+
[package.endnotes, "/word/endnotes.xml",
|
|
305
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml"],
|
|
306
|
+
[package.numbering, "/word/numbering.xml",
|
|
307
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"],
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
checks.filter_map do |obj, part_name, content_type|
|
|
311
|
+
next unless obj
|
|
312
|
+
|
|
313
|
+
Uniword::ContentTypes::Override.new(
|
|
314
|
+
part_name: part_name,
|
|
315
|
+
content_type: content_type,
|
|
316
|
+
)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|