uniword 1.0.11 → 1.2.5
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 +35 -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/has_borders.rb +8 -1
- 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 -20
- 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 +141 -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 +487 -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 +46 -1096
- data/lib/uniword/docx.rb +3 -0
- data/lib/uniword/drawingml/color_scheme.rb +37 -5
- data/lib/uniword/drawingml/theme.rb +0 -1
- 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/ooxml/schema/element_serializer.rb +4 -1
- 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/resource/theme_processor.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 +18 -1
- data/lib/uniword/theme/theme_xml_parser.rb +1 -1
- data/lib/uniword/themes/theme_transformation.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/footnote_reference_checker.rb +4 -1
- data/lib/uniword/validation/checkers/internal_link_checker.rb +2 -3
- data/lib/uniword/validation/rules/document_context.rb +1 -1
- data/lib/uniword/version.rb +1 -1
- data/lib/uniword/visitor/text_extractor.rb +2 -1
- data/lib/uniword/warnings/warning_collector.rb +6 -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/font.rb +1 -1
- 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 +28 -55
- 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/style.rb +2 -2
- 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/table_properties.rb +10 -15
- 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,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, 8]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def alloc_rsid
|
|
84
|
+
@rsid_counter += 1
|
|
85
|
+
Digest::SHA256.hexdigest("rsid:#{@rsid_counter}").upcase[0, 8]
|
|
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,10 +19,21 @@ 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
|
|
|
@@ -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
|
+
add_standalone(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
|
+
add_standalone(custom_properties.to_xml(DOCX_PROPS_OPTIONS.dup))
|
|
57
77
|
end
|
|
58
78
|
|
|
59
79
|
# Custom XML data items
|
|
@@ -141,18 +161,57 @@ 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.
|
|
165
|
+
# All DOCX parts require standalone="yes" in the XML declaration for Word compatibility.
|
|
145
166
|
def serialize_part(model)
|
|
146
|
-
model.to_xml(
|
|
167
|
+
add_standalone(model.to_xml(DOCX_PART_OPTIONS.dup))
|
|
147
168
|
end
|
|
148
169
|
|
|
149
|
-
# Serialize package infrastructure (rels, content types) with declaration
|
|
170
|
+
# Serialize package infrastructure (rels, content types) with declaration, single-line output
|
|
150
171
|
def serialize_infrastructure(model)
|
|
151
|
-
model.to_xml(
|
|
172
|
+
add_standalone(model.to_xml(DOCX_INFRA_OPTIONS.dup))
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Ensure the XML declaration includes standalone="yes".
|
|
176
|
+
# lutaml-model omits this by default; Word's strict OPC validation requires it.
|
|
177
|
+
def add_standalone(xml)
|
|
178
|
+
xml.sub(
|
|
179
|
+
%r{<\?xml version="1.0" encoding="UTF-8"\?>},
|
|
180
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>',
|
|
181
|
+
)
|
|
152
182
|
end
|
|
153
183
|
|
|
154
184
|
private
|
|
155
185
|
|
|
186
|
+
# Generate the next numeric relationship ID for a Relationships object.
|
|
187
|
+
def next_rid(relationships)
|
|
188
|
+
Ooxml::Relationships::PackageRelationships.next_available_rid(
|
|
189
|
+
relationships,
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Shared pattern: ensure a part has both a content type override and
|
|
194
|
+
# a document relationship. Idempotent — skips if already present.
|
|
195
|
+
# When allocator is present, only adds content type (rels handled by allocator).
|
|
196
|
+
def ensure_content_type(content_types, rels, part_name:,
|
|
197
|
+
content_type:, rel_type:, target:)
|
|
198
|
+
unless content_types.overrides.any? { |o| o.part_name == part_name }
|
|
199
|
+
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
200
|
+
part_name: part_name, content_type: content_type,
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
return if allocator
|
|
205
|
+
return if rels.relationships.any? { |r| r.target == target }
|
|
206
|
+
|
|
207
|
+
rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
208
|
+
id: next_rid(rels), type: rel_type, target: target,
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Legacy alias — calls ensure_content_type (same behavior)
|
|
213
|
+
alias ensure_part_registered ensure_content_type
|
|
214
|
+
|
|
156
215
|
def inject_image_parts(content, content_types, document_rels)
|
|
157
216
|
return unless document&.image_parts && !document.image_parts.empty?
|
|
158
217
|
|
|
@@ -165,8 +224,15 @@ document_rels)
|
|
|
165
224
|
)
|
|
166
225
|
end
|
|
167
226
|
|
|
168
|
-
document.image_parts.
|
|
227
|
+
document.image_parts.each_value do |image_data|
|
|
169
228
|
content["word/#{image_data[:target]}"] = image_data[:data]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
return if allocator
|
|
232
|
+
|
|
233
|
+
document.image_parts.each do |r_id, image_data|
|
|
234
|
+
next if document_rels.relationships.any? { |r| r.target == image_data[:target] }
|
|
235
|
+
|
|
170
236
|
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
171
237
|
id: r_id,
|
|
172
238
|
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
@@ -200,24 +266,11 @@ document_rels)
|
|
|
200
266
|
def inject_bibliography(content_types, document_rels)
|
|
201
267
|
return unless document&.bibliography_sources
|
|
202
268
|
|
|
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
|
-
)
|
|
269
|
+
ensure_content_type(content_types, document_rels,
|
|
270
|
+
part_name: "/word/sources.xml",
|
|
271
|
+
content_type: "application/vnd.openxmlformats-officedocument.bibliography+xml",
|
|
272
|
+
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/bibliography",
|
|
273
|
+
target: "sources.xml")
|
|
221
274
|
end
|
|
222
275
|
|
|
223
276
|
def inject_custom_properties(content_types, package_rels)
|
|
@@ -232,15 +285,15 @@ document_rels)
|
|
|
232
285
|
)
|
|
233
286
|
end
|
|
234
287
|
|
|
235
|
-
|
|
288
|
+
return if package_rels.relationships.any? do |r|
|
|
236
289
|
r.type.to_s.include?("officeDocument/2006/relationships/custom-properties")
|
|
237
290
|
end
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
291
|
+
|
|
292
|
+
package_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
293
|
+
id: next_rid(package_rels),
|
|
294
|
+
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties",
|
|
295
|
+
target: "docProps/custom.xml",
|
|
296
|
+
)
|
|
244
297
|
end
|
|
245
298
|
|
|
246
299
|
def inject_custom_xml(content_types)
|
|
@@ -265,17 +318,23 @@ document_rels)
|
|
|
265
318
|
counter = 0
|
|
266
319
|
document.headers.each_key do |type|
|
|
267
320
|
counter += 1
|
|
268
|
-
|
|
321
|
+
target = "header#{counter}.xml"
|
|
269
322
|
|
|
270
|
-
content_types.overrides
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
323
|
+
unless content_types.overrides.any? { |o| o.part_name == "/word/#{target}" }
|
|
324
|
+
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
325
|
+
part_name: "/word/#{target}",
|
|
326
|
+
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
|
|
327
|
+
)
|
|
328
|
+
end
|
|
274
329
|
|
|
330
|
+
next if allocator
|
|
331
|
+
next if document_rels.relationships.any? { |r| r.target == target }
|
|
332
|
+
|
|
333
|
+
r_id = next_rid(document_rels)
|
|
275
334
|
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
276
335
|
id: r_id,
|
|
277
336
|
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header",
|
|
278
|
-
target:
|
|
337
|
+
target: target,
|
|
279
338
|
)
|
|
280
339
|
|
|
281
340
|
wire_header_reference(type, r_id)
|
|
@@ -288,17 +347,23 @@ document_rels)
|
|
|
288
347
|
counter = 0
|
|
289
348
|
document.footers.each_key do |type|
|
|
290
349
|
counter += 1
|
|
291
|
-
|
|
350
|
+
target = "footer#{counter}.xml"
|
|
292
351
|
|
|
293
|
-
content_types.overrides
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
352
|
+
unless content_types.overrides.any? { |o| o.part_name == "/word/#{target}" }
|
|
353
|
+
content_types.overrides << Uniword::ContentTypes::Override.new(
|
|
354
|
+
part_name: "/word/#{target}",
|
|
355
|
+
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
|
|
356
|
+
)
|
|
357
|
+
end
|
|
297
358
|
|
|
359
|
+
next if allocator
|
|
360
|
+
next if document_rels.relationships.any? { |r| r.target == target }
|
|
361
|
+
|
|
362
|
+
r_id = next_rid(document_rels)
|
|
298
363
|
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
299
364
|
id: r_id,
|
|
300
365
|
type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer",
|
|
301
|
-
target:
|
|
366
|
+
target: target,
|
|
302
367
|
)
|
|
303
368
|
|
|
304
369
|
wire_footer_reference(type, r_id)
|
|
@@ -317,104 +382,53 @@ document_rels)
|
|
|
317
382
|
)
|
|
318
383
|
end
|
|
319
384
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
385
|
+
next if allocator
|
|
386
|
+
next if document_rels.relationships.any? { |r| r.id == part[:r_id] }
|
|
387
|
+
|
|
388
|
+
document_rels.relationships << Ooxml::Relationships::Relationship.new(
|
|
389
|
+
id: part[:r_id],
|
|
390
|
+
type: part[:rel_type],
|
|
391
|
+
target: part[:target],
|
|
392
|
+
)
|
|
327
393
|
end
|
|
328
394
|
end
|
|
329
395
|
|
|
330
396
|
def inject_notes(content_types, document_rels)
|
|
331
397
|
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
|
|
398
|
+
ensure_content_type(content_types, document_rels,
|
|
399
|
+
part_name: "/word/footnotes.xml",
|
|
400
|
+
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
|
|
401
|
+
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes",
|
|
402
|
+
target: "footnotes.xml")
|
|
350
403
|
end
|
|
351
404
|
|
|
352
405
|
return unless endnotes
|
|
353
406
|
|
|
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
|
-
)
|
|
407
|
+
ensure_content_type(content_types, document_rels,
|
|
408
|
+
part_name: "/word/endnotes.xml",
|
|
409
|
+
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml",
|
|
410
|
+
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes",
|
|
411
|
+
target: "endnotes.xml")
|
|
372
412
|
end
|
|
373
413
|
|
|
374
414
|
def inject_theme(content_types, document_rels)
|
|
375
415
|
return unless theme
|
|
376
416
|
|
|
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
|
-
)
|
|
417
|
+
ensure_content_type(content_types, document_rels,
|
|
418
|
+
part_name: "/word/theme/theme1.xml",
|
|
419
|
+
content_type: "application/vnd.openxmlformats-officedocument.theme+xml",
|
|
420
|
+
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme",
|
|
421
|
+
target: "theme/theme1.xml")
|
|
395
422
|
end
|
|
396
423
|
|
|
397
424
|
def inject_numbering(content_types, document_rels)
|
|
398
425
|
return unless numbering
|
|
399
426
|
|
|
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
|
-
)
|
|
427
|
+
ensure_content_type(content_types, document_rels,
|
|
428
|
+
part_name: "/word/numbering.xml",
|
|
429
|
+
content_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml",
|
|
430
|
+
rel_type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering",
|
|
431
|
+
target: "numbering.xml")
|
|
418
432
|
end
|
|
419
433
|
|
|
420
434
|
def inject_embeddings(content_types, document_rels)
|