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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
module Docx
|
|
7
|
+
# Single owner of all ID assignment in a DOCX package.
|
|
8
|
+
#
|
|
9
|
+
# Every builder, the adapter, and the reconciler call into this class
|
|
10
|
+
# for rId, footnote, endnote, bookmark, comment, and paragraph ID
|
|
11
|
+
# allocation. No other code generates IDs.
|
|
12
|
+
#
|
|
13
|
+
# Two usage modes:
|
|
14
|
+
# Creating from scratch — allocator starts empty, all IDs are fresh.
|
|
15
|
+
# Editing a template — call seed_from_* methods BEFORE any builder
|
|
16
|
+
# runs, so new IDs don't collide with existing ones.
|
|
17
|
+
#
|
|
18
|
+
# This is the "populate-first" principle: when loading a template DOCX,
|
|
19
|
+
# parse and seed ALL existing IDs from the template before modification.
|
|
20
|
+
class IdAllocator
|
|
21
|
+
REL_TYPE_BASE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
22
|
+
|
|
23
|
+
IMAGE_REL_TYPE = "#{REL_TYPE_BASE}/image"
|
|
24
|
+
HEADER_REL_TYPE = "#{REL_TYPE_BASE}/header"
|
|
25
|
+
FOOTER_REL_TYPE = "#{REL_TYPE_BASE}/footer"
|
|
26
|
+
HYPERLINK_REL_TYPE = "#{REL_TYPE_BASE}/hyperlink"
|
|
27
|
+
CHART_REL_TYPE = "#{REL_TYPE_BASE}/chart"
|
|
28
|
+
FOOTNOTES_REL_TYPE = "#{REL_TYPE_BASE}/footnotes"
|
|
29
|
+
ENDNOTES_REL_TYPE = "#{REL_TYPE_BASE}/endnotes"
|
|
30
|
+
THEME_REL_TYPE = "#{REL_TYPE_BASE}/theme"
|
|
31
|
+
NUMBERING_REL_TYPE = "#{REL_TYPE_BASE}/numbering"
|
|
32
|
+
|
|
33
|
+
def initialize
|
|
34
|
+
@rid_counter = 0
|
|
35
|
+
@rid_entries = {} # [target, type] -> { id, type, target, target_mode }
|
|
36
|
+
@footnote_counter = 1
|
|
37
|
+
@endnote_counter = 1
|
|
38
|
+
@bookmark_counter = 0
|
|
39
|
+
@comment_counter = 0
|
|
40
|
+
@para_counter = 0
|
|
41
|
+
@rsid_counter = 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Allocate a relationship ID for a target+type pair.
|
|
45
|
+
# Returns existing rId if this target+type was already registered.
|
|
46
|
+
def alloc_rid(target:, type:, target_mode: nil)
|
|
47
|
+
key = [target, type.to_s]
|
|
48
|
+
@rid_entries[key] ||= begin
|
|
49
|
+
@rid_counter += 1
|
|
50
|
+
{ id: "rId#{@rid_counter}", type: type.to_s,
|
|
51
|
+
target: target, target_mode: target_mode }
|
|
52
|
+
end
|
|
53
|
+
@rid_entries[key][:id]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def alloc_footnote_id
|
|
57
|
+
id = @footnote_counter
|
|
58
|
+
@footnote_counter += 1
|
|
59
|
+
id
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def alloc_endnote_id
|
|
63
|
+
id = @endnote_counter
|
|
64
|
+
@endnote_counter += 1
|
|
65
|
+
id
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def alloc_bookmark_id
|
|
69
|
+
@bookmark_counter += 1
|
|
70
|
+
@bookmark_counter.to_s
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def alloc_comment_id
|
|
74
|
+
@comment_counter += 1
|
|
75
|
+
@comment_counter.to_s
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def alloc_para_id
|
|
79
|
+
@para_counter += 1
|
|
80
|
+
Digest::SHA256.hexdigest("para:#{@para_counter}").upcase[0, 12]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def alloc_rsid
|
|
84
|
+
@rsid_counter += 1
|
|
85
|
+
Digest::SHA256.hexdigest("rsid:#{@rsid_counter}").upcase[0, 12]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Seed from a relationships collection — preserves existing rIds.
|
|
89
|
+
def seed_from_rels(relationships)
|
|
90
|
+
return unless relationships
|
|
91
|
+
|
|
92
|
+
relationships.each do |r|
|
|
93
|
+
key = [r.target, r.type.to_s]
|
|
94
|
+
@rid_entries[key] = {
|
|
95
|
+
id: r.id,
|
|
96
|
+
type: r.type.to_s,
|
|
97
|
+
target: r.target,
|
|
98
|
+
target_mode: r.target_mode,
|
|
99
|
+
}
|
|
100
|
+
num = r.id[/\ArId(\d+)\z/, 1]&.to_i || 0
|
|
101
|
+
@rid_counter = [@rid_counter, num].max
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Seed footnote/endnote counters from existing note entries.
|
|
106
|
+
def seed_from_notes(footnote_entries, endnote_entries)
|
|
107
|
+
footnote_entries&.each do |e|
|
|
108
|
+
id = e.id.to_i
|
|
109
|
+
@footnote_counter = [@footnote_counter, id + 1].max if id > 0
|
|
110
|
+
end
|
|
111
|
+
endnote_entries&.each do |e|
|
|
112
|
+
id = e.id.to_i
|
|
113
|
+
@endnote_counter = [@endnote_counter, id + 1].max if id > 0
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Produce the final ordered list of all allocated relationships.
|
|
118
|
+
def all_rels
|
|
119
|
+
@rid_entries.values.sort_by { |r| r[:id][/\d+/]&.to_i || 0 }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Check if a relationship has been registered for a target+type.
|
|
123
|
+
def rid_for(target:, type:)
|
|
124
|
+
key = [target, type.to_s]
|
|
125
|
+
@rid_entries[key]&.fetch(:id, nil)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Build an allocator seeded from every source on a package in one call.
|
|
129
|
+
# Used by both Package#populate_allocator and DocumentBuilder.from_template
|
|
130
|
+
# so they cannot drift apart.
|
|
131
|
+
def self.populate_from_package(package)
|
|
132
|
+
alloc = new
|
|
133
|
+
alloc.seed_from_rels(package.document_rels&.relationships)
|
|
134
|
+
alloc.seed_from_rels(package.package_rels&.relationships)
|
|
135
|
+
alloc.seed_from_notes(
|
|
136
|
+
package.footnotes&.footnote_entries,
|
|
137
|
+
package.endnotes&.endnote_entries,
|
|
138
|
+
)
|
|
139
|
+
alloc
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
data/lib/uniword/docx/package.rb
CHANGED
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require "lutaml/model"
|
|
5
|
-
require_relative "reconciler"
|
|
6
|
-
require_relative "package_defaults"
|
|
7
|
-
require_relative "package_serialization"
|
|
8
5
|
|
|
9
6
|
module Uniword
|
|
10
7
|
module Docx
|
|
@@ -89,6 +86,18 @@ module Uniword
|
|
|
89
86
|
attr_accessor :chart_parts, :bibliography_sources, :profile
|
|
90
87
|
attr_accessor :settings_rels, :embeddings
|
|
91
88
|
|
|
89
|
+
# Central ID allocator — owns all rId, footnote, bookmark, etc. assignment.
|
|
90
|
+
# Seeded from template rels on load; used by builders during construction.
|
|
91
|
+
attr_accessor :allocator
|
|
92
|
+
|
|
93
|
+
# Raw XML from template ZIP for unmodified parts.
|
|
94
|
+
# When present, these are used verbatim instead of re-serializing
|
|
95
|
+
# through lutaml-model (which drops unmapped elements).
|
|
96
|
+
attr_accessor :raw_xml_parts
|
|
97
|
+
|
|
98
|
+
# Paths that have been modified and should not use raw XML passthrough.
|
|
99
|
+
attr_accessor :modified_part_paths
|
|
100
|
+
|
|
92
101
|
# Load DOCX package from file
|
|
93
102
|
#
|
|
94
103
|
# @param path [String] Path to .docx file
|
|
@@ -96,7 +105,9 @@ module Uniword
|
|
|
96
105
|
def self.from_file(path)
|
|
97
106
|
extractor = Infrastructure::ZipExtractor.new
|
|
98
107
|
zip_content = extractor.extract(path)
|
|
99
|
-
from_zip_content(zip_content, path)
|
|
108
|
+
package = from_zip_content(zip_content, path)
|
|
109
|
+
package.populate_allocator
|
|
110
|
+
package
|
|
100
111
|
end
|
|
101
112
|
|
|
102
113
|
# Create package from extracted ZIP content
|
|
@@ -364,7 +375,14 @@ module Uniword
|
|
|
364
375
|
else "image/#{ext}"
|
|
365
376
|
end
|
|
366
377
|
|
|
367
|
-
r_id =
|
|
378
|
+
r_id = if package.allocator
|
|
379
|
+
package.allocator.alloc_rid(
|
|
380
|
+
target: "media/#{filename}",
|
|
381
|
+
type: IdAllocator::IMAGE_REL_TYPE,
|
|
382
|
+
)
|
|
383
|
+
else
|
|
384
|
+
"rId#{package.document.image_parts.size + 1}"
|
|
385
|
+
end
|
|
368
386
|
|
|
369
387
|
binary_data = if zip_path
|
|
370
388
|
read_binary_from_zip(zip_path, media_path)
|
|
@@ -406,6 +424,12 @@ module Uniword
|
|
|
406
424
|
package.to_file(path)
|
|
407
425
|
end
|
|
408
426
|
|
|
427
|
+
# Populate the allocator from all existing template data.
|
|
428
|
+
# Must be called BEFORE any builder runs (populate-first principle).
|
|
429
|
+
def populate_allocator
|
|
430
|
+
@allocator = IdAllocator.populate_from_package(self)
|
|
431
|
+
end
|
|
432
|
+
|
|
409
433
|
# Extract media files from word/theme/media/ directory
|
|
410
434
|
def self.extract_theme_media(zip_content)
|
|
411
435
|
media = {}
|
|
@@ -443,14 +467,26 @@ module Uniword
|
|
|
443
467
|
self.font_table ||= Uniword::Wordprocessingml::FontTable.new
|
|
444
468
|
self.web_settings ||= Uniword::Wordprocessingml::WebSettings.new
|
|
445
469
|
|
|
446
|
-
Reconciler.new(self,
|
|
470
|
+
Reconciler.new(self,
|
|
471
|
+
profile: profile || Profile.defaults,
|
|
472
|
+
allocator: allocator).reconcile
|
|
447
473
|
|
|
448
474
|
inject_part_relationships(content, content_types, package_rels, document_rels)
|
|
449
475
|
serialize_package_parts(content, content_types, package_rels, document_rels)
|
|
450
476
|
|
|
477
|
+
# OOXML requires [Content_Types].xml as the first ZIP entry.
|
|
478
|
+
reorder_content_hash(content)
|
|
451
479
|
content
|
|
452
480
|
end
|
|
453
481
|
|
|
482
|
+
# Ensure [Content_Types].xml is first, _rels/.rels is second.
|
|
483
|
+
def reorder_content_hash(content)
|
|
484
|
+
priority = {}
|
|
485
|
+
priority["[Content_Types].xml"] = content.delete("[Content_Types].xml") if content.key?("[Content_Types].xml")
|
|
486
|
+
priority["_rels/.rels"] = content.delete("_rels/.rels") if content.key?("_rels/.rels")
|
|
487
|
+
content.replace(priority.merge(content))
|
|
488
|
+
end
|
|
489
|
+
|
|
454
490
|
# Delegate common DocumentRoot methods for API compatibility
|
|
455
491
|
|
|
456
492
|
def paragraphs
|
|
@@ -9,29 +9,8 @@ module Uniword
|
|
|
9
9
|
#
|
|
10
10
|
# @api private
|
|
11
11
|
module PackageDefaults
|
|
12
|
-
# Mapping from DocumentRoot attributes to Package attributes.
|
|
13
|
-
# Only entries where names differ need explicit mapping.
|
|
14
|
-
DOCUMENT_TO_PACKAGE_MAPPINGS = {
|
|
15
|
-
styles_configuration: :styles,
|
|
16
|
-
settings: :settings,
|
|
17
|
-
font_table: :font_table,
|
|
18
|
-
web_settings: :web_settings,
|
|
19
|
-
theme: :theme,
|
|
20
|
-
core_properties: :core_properties,
|
|
21
|
-
app_properties: :app_properties,
|
|
22
|
-
custom_properties: :custom_properties,
|
|
23
|
-
document_rels: :document_rels,
|
|
24
|
-
theme_rels: :theme_rels,
|
|
25
|
-
package_rels: :package_rels,
|
|
26
|
-
content_types: :content_types,
|
|
27
|
-
footnotes: :footnotes,
|
|
28
|
-
endnotes: :endnotes,
|
|
29
|
-
}.freeze
|
|
30
|
-
|
|
31
12
|
def self.included(base)
|
|
32
13
|
base.extend(ClassMethods)
|
|
33
|
-
base.const_set(:DOCUMENT_TO_PACKAGE_MAPPINGS,
|
|
34
|
-
DOCUMENT_TO_PACKAGE_MAPPINGS)
|
|
35
14
|
end
|
|
36
15
|
|
|
37
16
|
# Class methods for package construction
|
|
@@ -40,16 +19,28 @@ module Uniword
|
|
|
40
19
|
def copy_document_parts_to_package(document, package)
|
|
41
20
|
return unless document.is_a?(Uniword::Wordprocessingml::DocumentRoot)
|
|
42
21
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
22
|
+
package.styles = document.styles_configuration if document.styles_configuration
|
|
23
|
+
package.settings = document.settings if document.settings
|
|
24
|
+
package.font_table = document.font_table if document.font_table
|
|
25
|
+
package.web_settings = document.web_settings if document.web_settings
|
|
26
|
+
package.theme = document.theme if document.theme
|
|
27
|
+
package.core_properties = document.core_properties if document.core_properties
|
|
28
|
+
package.app_properties = document.app_properties if document.app_properties
|
|
29
|
+
package.custom_properties = document.custom_properties if document.custom_properties
|
|
30
|
+
package.document_rels = document.document_rels if document.document_rels
|
|
31
|
+
package.theme_rels = document.theme_rels if document.theme_rels
|
|
32
|
+
package.package_rels = document.package_rels if document.package_rels
|
|
33
|
+
package.content_types = document.content_types if document.content_types
|
|
34
|
+
package.footnotes = document.footnotes if document.footnotes
|
|
35
|
+
package.endnotes = document.endnotes if document.endnotes
|
|
36
|
+
package.allocator = document.allocator if document.allocator
|
|
47
37
|
|
|
48
38
|
package.numbering = document.numbering_configuration if document.numbering_configuration_loaded?
|
|
49
39
|
|
|
50
40
|
package.chart_parts = document.chart_parts if document.chart_parts
|
|
51
41
|
package.custom_xml_items = document.custom_xml_items if document.custom_xml_items
|
|
52
42
|
package.bibliography_sources = document.bibliography_sources if document.bibliography_sources
|
|
43
|
+
package.embeddings = document.embeddings if document.embeddings
|
|
53
44
|
end
|
|
54
45
|
|
|
55
46
|
# Create minimal content types for a valid DOCX
|
|
@@ -12,6 +12,30 @@ module Uniword
|
|
|
12
12
|
#
|
|
13
13
|
# @api private
|
|
14
14
|
module PackageSerialization
|
|
15
|
+
DOCX_XML_OPTIONS = {
|
|
16
|
+
encoding: "UTF-8",
|
|
17
|
+
indent: 0,
|
|
18
|
+
line_ending: "\r\n",
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
DOCX_PART_OPTIONS = {
|
|
22
|
+
**DOCX_XML_OPTIONS,
|
|
23
|
+
prefix: true,
|
|
24
|
+
standalone: true,
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
DOCX_INFRA_OPTIONS = {
|
|
28
|
+
**DOCX_XML_OPTIONS,
|
|
29
|
+
declaration: true,
|
|
30
|
+
standalone: true,
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
DOCX_PROPS_OPTIONS = {
|
|
34
|
+
**DOCX_XML_OPTIONS,
|
|
35
|
+
prefix: false,
|
|
36
|
+
standalone: true,
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
15
39
|
# Inject content types and relationships for all package parts
|
|
16
40
|
def inject_part_relationships(content, content_types, package_rels,
|
|
17
41
|
document_rels)
|
|
@@ -45,15 +69,11 @@ document_rels)
|
|
|
45
69
|
end
|
|
46
70
|
if app_properties
|
|
47
71
|
content["docProps/app.xml"] =
|
|
48
|
-
app_properties.to_xml(
|
|
49
|
-
prefix: false,
|
|
50
|
-
standalone: true)
|
|
72
|
+
app_properties.to_xml(DOCX_PROPS_OPTIONS.dup)
|
|
51
73
|
end
|
|
52
74
|
if custom_properties
|
|
53
75
|
content["docProps/custom.xml"] =
|
|
54
|
-
custom_properties.to_xml(
|
|
55
|
-
prefix: false,
|
|
56
|
-
standalone: true)
|
|
76
|
+
custom_properties.to_xml(DOCX_PROPS_OPTIONS.dup)
|
|
57
77
|
end
|
|
58
78
|
|
|
59
79
|
# Custom XML data items
|
|
@@ -141,18 +161,47 @@ document_rels)
|
|
|
141
161
|
serialize_embeddings(content)
|
|
142
162
|
end
|
|
143
163
|
|
|
144
|
-
# Serialize an OOXML document part with standard encoding
|
|
164
|
+
# Serialize an OOXML document part with standard encoding, single-line output
|
|
145
165
|
def serialize_part(model)
|
|
146
|
-
model.to_xml(
|
|
166
|
+
model.to_xml(DOCX_PART_OPTIONS.dup)
|
|
147
167
|
end
|
|
148
168
|
|
|
149
|
-
# Serialize package infrastructure (rels, content types) with declaration
|
|
169
|
+
# Serialize package infrastructure (rels, content types) with declaration, single-line output
|
|
150
170
|
def serialize_infrastructure(model)
|
|
151
|
-
model.to_xml(
|
|
171
|
+
model.to_xml(DOCX_INFRA_OPTIONS.dup)
|
|
152
172
|
end
|
|
153
173
|
|
|
154
174
|
private
|
|
155
175
|
|
|
176
|
+
# Generate the next numeric relationship ID for a Relationships object.
|
|
177
|
+
def next_rid(relationships)
|
|
178
|
+
Ooxml::Relationships::PackageRelationships.next_available_rid(
|
|
179
|
+
relationships,
|
|
180
|
+
)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Shared pattern: ensure a part has both a content type override and
|
|
184
|
+
# a document relationship. Idempotent — skips if already present.
|
|
185
|
+
# When allocator is present, only adds content type (rels handled by allocator).
|
|
186
|
+
def ensure_content_type(content_types, rels, part_name:,
|
|
187
|
+
content_type:, rel_type:, target:)
|
|
188
|
+
unless content_types.overrides.any? { |o| o.part_name == part_name }
|
|
189
|
+
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
190
|
+
part_name: part_name, content_type: content_type,
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
return if allocator
|
|
195
|
+
return if rels.relationships.any? { |r| r.target == target }
|
|
196
|
+
|
|
197
|
+
rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
198
|
+
id: next_rid(rels), type: rel_type, target: target,
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Legacy alias — calls ensure_content_type (same behavior)
|
|
203
|
+
alias ensure_part_registered ensure_content_type
|
|
204
|
+
|
|
156
205
|
def inject_image_parts(content, content_types, document_rels)
|
|
157
206
|
return unless document&.image_parts && !document.image_parts.empty?
|
|
158
207
|
|
|
@@ -165,8 +214,15 @@ document_rels)
|
|
|
165
214
|
)
|
|
166
215
|
end
|
|
167
216
|
|
|
168
|
-
document.image_parts.
|
|
217
|
+
document.image_parts.each_value do |image_data|
|
|
169
218
|
content["word/#{image_data[:target]}"] = image_data[:data]
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
return if allocator
|
|
222
|
+
|
|
223
|
+
document.image_parts.each do |r_id, image_data|
|
|
224
|
+
next if document_rels.relationships.any? { |r| r.target == image_data[:target] }
|
|
225
|
+
|
|
170
226
|
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
171
227
|
id: r_id,
|
|
172
228
|
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
@@ -200,24 +256,11 @@ document_rels)
|
|
|
200
256
|
def inject_bibliography(content_types, document_rels)
|
|
201
257
|
return unless document&.bibliography_sources
|
|
202
258
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
content_type: "application/vnd.openxmlformats-officedocument.bibliography+xml",
|
|
209
|
-
)
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
return if document_rels.relationships.any? do |r|
|
|
213
|
-
r.target == "sources.xml"
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
217
|
-
id: "rIdSrc#{SecureRandom.hex(4)}",
|
|
218
|
-
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/bibliography",
|
|
219
|
-
target: "sources.xml",
|
|
220
|
-
)
|
|
259
|
+
ensure_content_type(content_types, document_rels,
|
|
260
|
+
part_name: "/word/sources.xml",
|
|
261
|
+
content_type: "application/vnd.openxmlformats-officedocument.bibliography+xml",
|
|
262
|
+
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/bibliography",
|
|
263
|
+
target: "sources.xml")
|
|
221
264
|
end
|
|
222
265
|
|
|
223
266
|
def inject_custom_properties(content_types, package_rels)
|
|
@@ -232,15 +275,15 @@ document_rels)
|
|
|
232
275
|
)
|
|
233
276
|
end
|
|
234
277
|
|
|
235
|
-
|
|
278
|
+
return if package_rels.relationships.any? do |r|
|
|
236
279
|
r.type.to_s.include?("officeDocument/2006/relationships/custom-properties")
|
|
237
280
|
end
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
281
|
+
|
|
282
|
+
package_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
283
|
+
id: next_rid(package_rels),
|
|
284
|
+
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties",
|
|
285
|
+
target: "docProps/custom.xml",
|
|
286
|
+
)
|
|
244
287
|
end
|
|
245
288
|
|
|
246
289
|
def inject_custom_xml(content_types)
|
|
@@ -265,17 +308,23 @@ document_rels)
|
|
|
265
308
|
counter = 0
|
|
266
309
|
document.headers.each_key do |type|
|
|
267
310
|
counter += 1
|
|
268
|
-
|
|
311
|
+
target = "header#{counter}.xml"
|
|
269
312
|
|
|
270
|
-
content_types.overrides
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
313
|
+
unless content_types.overrides.any? { |o| o.part_name == "/word/#{target}" }
|
|
314
|
+
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
315
|
+
part_name: "/word/#{target}",
|
|
316
|
+
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
|
|
317
|
+
)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
next if allocator
|
|
321
|
+
next if document_rels.relationships.any? { |r| r.target == target }
|
|
274
322
|
|
|
323
|
+
r_id = next_rid(document_rels)
|
|
275
324
|
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
276
325
|
id: r_id,
|
|
277
326
|
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header",
|
|
278
|
-
target:
|
|
327
|
+
target: target,
|
|
279
328
|
)
|
|
280
329
|
|
|
281
330
|
wire_header_reference(type, r_id)
|
|
@@ -288,17 +337,23 @@ document_rels)
|
|
|
288
337
|
counter = 0
|
|
289
338
|
document.footers.each_key do |type|
|
|
290
339
|
counter += 1
|
|
291
|
-
|
|
340
|
+
target = "footer#{counter}.xml"
|
|
292
341
|
|
|
293
|
-
content_types.overrides
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
342
|
+
unless content_types.overrides.any? { |o| o.part_name == "/word/#{target}" }
|
|
343
|
+
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
344
|
+
part_name: "/word/#{target}",
|
|
345
|
+
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
|
|
346
|
+
)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
next if allocator
|
|
350
|
+
next if document_rels.relationships.any? { |r| r.target == target }
|
|
297
351
|
|
|
352
|
+
r_id = next_rid(document_rels)
|
|
298
353
|
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
299
354
|
id: r_id,
|
|
300
355
|
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer",
|
|
301
|
-
target:
|
|
356
|
+
target: target,
|
|
302
357
|
)
|
|
303
358
|
|
|
304
359
|
wire_footer_reference(type, r_id)
|
|
@@ -317,104 +372,53 @@ document_rels)
|
|
|
317
372
|
)
|
|
318
373
|
end
|
|
319
374
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
375
|
+
next if allocator
|
|
376
|
+
next if document_rels.relationships.any? { |r| r.id == part[:r_id] }
|
|
377
|
+
|
|
378
|
+
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
379
|
+
id: part[:r_id],
|
|
380
|
+
type: part[:rel_type],
|
|
381
|
+
target: part[:target],
|
|
382
|
+
)
|
|
327
383
|
end
|
|
328
384
|
end
|
|
329
385
|
|
|
330
386
|
def inject_notes(content_types, document_rels)
|
|
331
387
|
if footnotes
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
|
|
338
|
-
)
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
unless document_rels.relationships.any? do |r|
|
|
342
|
-
r.target == "footnotes.xml"
|
|
343
|
-
end
|
|
344
|
-
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
345
|
-
id: "rIdFootnotes",
|
|
346
|
-
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes",
|
|
347
|
-
target: "footnotes.xml",
|
|
348
|
-
)
|
|
349
|
-
end
|
|
388
|
+
ensure_content_type(content_types, document_rels,
|
|
389
|
+
part_name: "/word/footnotes.xml",
|
|
390
|
+
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
|
|
391
|
+
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes",
|
|
392
|
+
target: "footnotes.xml")
|
|
350
393
|
end
|
|
351
394
|
|
|
352
395
|
return unless endnotes
|
|
353
396
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml",
|
|
360
|
-
)
|
|
361
|
-
end
|
|
362
|
-
|
|
363
|
-
return if document_rels.relationships.any? do |r|
|
|
364
|
-
r.target == "endnotes.xml"
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
368
|
-
id: "rIdEndnotes",
|
|
369
|
-
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes",
|
|
370
|
-
target: "endnotes.xml",
|
|
371
|
-
)
|
|
397
|
+
ensure_content_type(content_types, document_rels,
|
|
398
|
+
part_name: "/word/endnotes.xml",
|
|
399
|
+
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml",
|
|
400
|
+
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes",
|
|
401
|
+
target: "endnotes.xml")
|
|
372
402
|
end
|
|
373
403
|
|
|
374
404
|
def inject_theme(content_types, document_rels)
|
|
375
405
|
return unless theme
|
|
376
406
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
content_type: "application/vnd.openxmlformats-officedocument.theme+xml",
|
|
383
|
-
)
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
return if document_rels.relationships.any? do |r|
|
|
387
|
-
r.target == "theme/theme1.xml"
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
391
|
-
id: "rIdTheme",
|
|
392
|
-
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme",
|
|
393
|
-
target: "theme/theme1.xml",
|
|
394
|
-
)
|
|
407
|
+
ensure_content_type(content_types, document_rels,
|
|
408
|
+
part_name: "/word/theme/theme1.xml",
|
|
409
|
+
content_type: "application/vnd.openxmlformats-officedocument.theme+xml",
|
|
410
|
+
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme",
|
|
411
|
+
target: "theme/theme1.xml")
|
|
395
412
|
end
|
|
396
413
|
|
|
397
414
|
def inject_numbering(content_types, document_rels)
|
|
398
415
|
return unless numbering
|
|
399
416
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml",
|
|
406
|
-
)
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
return if document_rels.relationships.any? do |r|
|
|
410
|
-
r.target == "numbering.xml"
|
|
411
|
-
end
|
|
412
|
-
|
|
413
|
-
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
414
|
-
id: "rIdNumbering",
|
|
415
|
-
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering",
|
|
416
|
-
target: "numbering.xml",
|
|
417
|
-
)
|
|
417
|
+
ensure_content_type(content_types, document_rels,
|
|
418
|
+
part_name: "/word/numbering.xml",
|
|
419
|
+
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml",
|
|
420
|
+
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering",
|
|
421
|
+
target: "numbering.xml")
|
|
418
422
|
end
|
|
419
423
|
|
|
420
424
|
def inject_embeddings(content_types, document_rels)
|