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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1517a752442fd3914efbcc8f94130cf6f4983c110807b0790f03bebf9424d875
|
|
4
|
+
data.tar.gz: a88ecadccf094ffd2c1031d32e13ded47f361b98ca0d594c373bd48cc74f6898
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d728a6dc5a0df3560cc607281fb6ca8dd384c56d75247d7ca4b57a1791a15312b8fa8a2e5a8ae18c4ec60ff0574f76fafde2e68994c4b13b326b329cb5fd134e
|
|
7
|
+
data.tar.gz: cd7339df777bb848a7ad69c09b3d44fc0478fd9ece6a9f4d9c79ebb3114fa205824487f9bab85753338f24ac8aa22345319a254fc6a841d9f54117d2c0f07945
|
data/config/font_metadata.yml
CHANGED
|
@@ -90,6 +90,62 @@ fonts:
|
|
|
90
90
|
# --- East Asian fonts (per locale) ---
|
|
91
91
|
|
|
92
92
|
# Chinese Simplified (zh-CN)
|
|
93
|
+
SimSun:
|
|
94
|
+
alt_name: "宋体"
|
|
95
|
+
panose1: "02010600030101010101"
|
|
96
|
+
charset: "86"
|
|
97
|
+
family: roman
|
|
98
|
+
pitch: variable
|
|
99
|
+
sig:
|
|
100
|
+
usb0: "00000003"
|
|
101
|
+
usb1: "080E0000"
|
|
102
|
+
usb2: "00000010"
|
|
103
|
+
usb3: "00000000"
|
|
104
|
+
csb0: "00040000"
|
|
105
|
+
csb1: "00000000"
|
|
106
|
+
|
|
107
|
+
SimHei:
|
|
108
|
+
alt_name: "黑体"
|
|
109
|
+
panose1: "02010609060101010101"
|
|
110
|
+
charset: "86"
|
|
111
|
+
family: modern
|
|
112
|
+
pitch: fixed
|
|
113
|
+
sig:
|
|
114
|
+
usb0: "800002BF"
|
|
115
|
+
usb1: "38CF7CFA"
|
|
116
|
+
usb2: "00000016"
|
|
117
|
+
usb3: "00000000"
|
|
118
|
+
csb0: "0004000F"
|
|
119
|
+
csb1: "00000000"
|
|
120
|
+
|
|
121
|
+
MS Mincho:
|
|
122
|
+
alt_name: "MS 明朝"
|
|
123
|
+
panose1: "02020609040205080304"
|
|
124
|
+
charset: "80"
|
|
125
|
+
family: roman
|
|
126
|
+
pitch: fixed
|
|
127
|
+
sig:
|
|
128
|
+
usb0: "E00002FF"
|
|
129
|
+
usb1: "6FFF07FF"
|
|
130
|
+
usb2: "00000020"
|
|
131
|
+
usb3: "00000000"
|
|
132
|
+
csb0: "0000019F"
|
|
133
|
+
csb1: "00000000"
|
|
134
|
+
|
|
135
|
+
MS Gothic:
|
|
136
|
+
alt_name: "MS ゴシック"
|
|
137
|
+
panose1: "020B0609020205080204"
|
|
138
|
+
charset: "80"
|
|
139
|
+
family: modern
|
|
140
|
+
pitch: fixed
|
|
141
|
+
sig:
|
|
142
|
+
usb0: "E00002FF"
|
|
143
|
+
usb1: "6FFF07FF"
|
|
144
|
+
usb2: "00000020"
|
|
145
|
+
usb3: "00000000"
|
|
146
|
+
csb0: "0000019F"
|
|
147
|
+
csb1: "00000000"
|
|
148
|
+
|
|
93
149
|
DengXian:
|
|
94
150
|
alt_name: "等线"
|
|
95
151
|
panose1: "02010600030101010101"
|
|
@@ -88,7 +88,7 @@ module Uniword
|
|
|
88
88
|
if paragraph.is_a?(Lutaml::Model::Serializable) && paragraph.class.attributes.key?(:text)
|
|
89
89
|
paragraph.text
|
|
90
90
|
elsif paragraph.is_a?(Lutaml::Model::Serializable) && paragraph.class.attributes.key?(:runs)
|
|
91
|
-
paragraph.runs.map(&:
|
|
91
|
+
paragraph.runs.map(&:text_string).join
|
|
92
92
|
end
|
|
93
93
|
end
|
|
94
94
|
end
|
data/lib/uniword/assembly/toc.rb
CHANGED
|
@@ -166,11 +166,11 @@ module Uniword
|
|
|
166
166
|
# @param paragraph [Wordprocessingml::Paragraph] Paragraph to check
|
|
167
167
|
# @return [Integer, nil] Heading level or nil
|
|
168
168
|
def heading_level(paragraph)
|
|
169
|
-
|
|
170
|
-
return nil unless
|
|
169
|
+
style = paragraph.style
|
|
170
|
+
return nil unless style
|
|
171
171
|
|
|
172
172
|
# Check for heading styles (Heading 1, Heading 2, etc.)
|
|
173
|
-
match =
|
|
173
|
+
match = style.to_s.match(/^Heading\s*(\d+)$/i)
|
|
174
174
|
return match[1].to_i if match
|
|
175
175
|
|
|
176
176
|
# Check for outline level in properties
|
|
@@ -187,17 +187,7 @@ module Uniword
|
|
|
187
187
|
# @param paragraph [Wordprocessingml::Paragraph] Source paragraph
|
|
188
188
|
# @return [String] Extracted text
|
|
189
189
|
def extract_text(paragraph)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
paragraph.runs.each do |run|
|
|
193
|
-
run_text = run.text
|
|
194
|
-
next unless run_text
|
|
195
|
-
|
|
196
|
-
# Handle Text objects by converting to string
|
|
197
|
-
text_parts << run_text.to_s
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
text_parts.join
|
|
190
|
+
paragraph.runs.map(&:text_string).join
|
|
201
191
|
end
|
|
202
192
|
|
|
203
193
|
# Create title paragraph.
|
|
@@ -186,10 +186,9 @@ module Uniword
|
|
|
186
186
|
# @param run [Run] Run to process
|
|
187
187
|
# @return [void]
|
|
188
188
|
def substitute_run(run)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
run.text = substitute(content)
|
|
189
|
+
run.substitute_with_block(VARIABLE_PATTERN) do |match|
|
|
190
|
+
resolve_variable(match[1])
|
|
191
|
+
end
|
|
193
192
|
end
|
|
194
193
|
|
|
195
194
|
# Collect all keys from nested hash.
|
|
@@ -76,8 +76,10 @@ module Uniword
|
|
|
76
76
|
|
|
77
77
|
# Detect style based on properties
|
|
78
78
|
detected_style = detect_paragraph_style(paragraph)
|
|
79
|
+
target = detected_style && @target_styles[detected_style]
|
|
80
|
+
next unless target
|
|
79
81
|
|
|
80
|
-
paragraph.properties.style =
|
|
82
|
+
paragraph.properties.style = target
|
|
81
83
|
end
|
|
82
84
|
end
|
|
83
85
|
|
|
@@ -102,7 +104,7 @@ module Uniword
|
|
|
102
104
|
|
|
103
105
|
# Check style name
|
|
104
106
|
if paragraph.style
|
|
105
|
-
style_name = paragraph.style.downcase
|
|
107
|
+
style_name = paragraph.style.to_s.downcase
|
|
106
108
|
return :heading1 if style_name.include?("heading 1")
|
|
107
109
|
return :heading2 if style_name.include?("heading 2")
|
|
108
110
|
return :heading3 if style_name.include?("heading 3")
|
|
@@ -39,6 +39,31 @@ module Uniword
|
|
|
39
39
|
raise NotImplementedError,
|
|
40
40
|
"#{name} must implement .default_model_class"
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# Tag name for the properties element (override in subclasses).
|
|
46
|
+
# e.g., "pPr" for ParagraphBuilder, "tcPr" for TableCellBuilder.
|
|
47
|
+
def properties_tag
|
|
48
|
+
raise NotImplementedError,
|
|
49
|
+
"#{self.class.name} must implement #properties_tag"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def track_element_order(tag)
|
|
53
|
+
@model.element_order ||= []
|
|
54
|
+
ensure_properties_in_order
|
|
55
|
+
@model.element_order << Lutaml::Xml::Element.new("Element", tag)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def ensure_properties_in_order
|
|
59
|
+
return unless @model.element_order
|
|
60
|
+
return if @model.element_order.any? { |e| e.element? && e.name == properties_tag }
|
|
61
|
+
return unless @model.properties
|
|
62
|
+
|
|
63
|
+
@model.element_order.unshift(
|
|
64
|
+
Lutaml::Xml::Element.new("Element", properties_tag)
|
|
65
|
+
)
|
|
66
|
+
end
|
|
42
67
|
end
|
|
43
68
|
end
|
|
44
69
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "nokogiri"
|
|
4
|
-
require "
|
|
4
|
+
require "digest"
|
|
5
5
|
|
|
6
6
|
module Uniword
|
|
7
7
|
module Builder
|
|
@@ -134,6 +134,8 @@ module Uniword
|
|
|
134
134
|
|
|
135
135
|
private
|
|
136
136
|
|
|
137
|
+
include DeterministicId
|
|
138
|
+
|
|
137
139
|
# Build the inner chart XML
|
|
138
140
|
def build_chart_xml(xml)
|
|
139
141
|
xml["c"].chart do
|
|
@@ -265,19 +267,36 @@ module Uniword
|
|
|
265
267
|
|
|
266
268
|
# Build a chart axis
|
|
267
269
|
def build_axis(xml, tag, id, position, cross_id)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
xml["c"].
|
|
271
|
-
xml
|
|
270
|
+
case tag
|
|
271
|
+
when "catAx"
|
|
272
|
+
xml["c"].catAx do
|
|
273
|
+
write_axis_content(xml, id, position, cross_id)
|
|
274
|
+
end
|
|
275
|
+
when "valAx"
|
|
276
|
+
xml["c"].valAx do
|
|
277
|
+
write_axis_content(xml, id, position, cross_id)
|
|
272
278
|
end
|
|
273
|
-
|
|
274
|
-
xml["c"].
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
279
|
+
when "serAx"
|
|
280
|
+
xml["c"].serAx do
|
|
281
|
+
write_axis_content(xml, id, position, cross_id)
|
|
282
|
+
end
|
|
283
|
+
else
|
|
284
|
+
raise ArgumentError, "Unknown axis type: #{tag}"
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def write_axis_content(xml, id, position, cross_id)
|
|
289
|
+
xml["c"].axId("val" => id)
|
|
290
|
+
xml["c"].scaling do
|
|
291
|
+
xml["c"].orientation("val" => "minMax")
|
|
280
292
|
end
|
|
293
|
+
xml["c"].delete("val" => "0")
|
|
294
|
+
xml["c"].axPos("val" => position)
|
|
295
|
+
xml["c"].majorGridlines
|
|
296
|
+
xml["c"].numFmt("formatCode" => "General",
|
|
297
|
+
"sourceLinked" => "1")
|
|
298
|
+
xml["c"].tickLblPos("val" => "nextTo")
|
|
299
|
+
xml["c"].crossAx("val" => cross_id)
|
|
281
300
|
end
|
|
282
301
|
|
|
283
302
|
# Create a Drawing element with chart reference
|
|
@@ -288,8 +307,8 @@ module Uniword
|
|
|
288
307
|
inline.extent = WpDrawing::Extent.new(cx: @width, cy: @height)
|
|
289
308
|
inline.effect_extent = WpDrawing::EffectExtent.new
|
|
290
309
|
inline.doc_properties = WpDrawing::DocProperties.new(
|
|
291
|
-
id:
|
|
292
|
-
name: "Chart #{
|
|
310
|
+
id: deterministic_id("chart", @chart_type, r_id),
|
|
311
|
+
name: "Chart #{@chart_type}",
|
|
293
312
|
)
|
|
294
313
|
|
|
295
314
|
graphic = Drawingml::Graphic.new
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
module Builder
|
|
7
|
+
# Shared deterministic ID generation for builders.
|
|
8
|
+
#
|
|
9
|
+
# Produces stable integer IDs from seed values using SHA256,
|
|
10
|
+
# ensuring reproducible output across runs.
|
|
11
|
+
module DeterministicId
|
|
12
|
+
def deterministic_id(*seeds)
|
|
13
|
+
Digest::SHA256.hexdigest(seeds.join(":")).to_i(16) % 1_000_000_000
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -29,15 +29,17 @@ module Uniword
|
|
|
29
29
|
# doc.footer { |f| f << Builder.page_number_field }
|
|
30
30
|
# doc.save('report.docx')
|
|
31
31
|
class DocumentBuilder < BaseBuilder
|
|
32
|
+
attr_reader :allocator
|
|
33
|
+
|
|
32
34
|
def self.default_model_class
|
|
33
35
|
Wordprocessingml::DocumentRoot
|
|
34
36
|
end
|
|
35
37
|
|
|
36
|
-
def initialize(model = nil)
|
|
37
|
-
super
|
|
38
|
-
@
|
|
39
|
-
@
|
|
40
|
-
@
|
|
38
|
+
def initialize(model = nil, allocator: nil)
|
|
39
|
+
super(model)
|
|
40
|
+
@allocator = allocator
|
|
41
|
+
@model.allocator = allocator if allocator
|
|
42
|
+
@footnote_builder = FootnoteBuilder.new(self, allocator: @allocator)
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
# Load a document from file for manipulation
|
|
@@ -48,6 +50,90 @@ module Uniword
|
|
|
48
50
|
new(Uniword.load(path))
|
|
49
51
|
end
|
|
50
52
|
|
|
53
|
+
# Load a DOCX as a template, reset its body to a clean state, and
|
|
54
|
+
# seed the IdAllocator from the template's existing relationships.
|
|
55
|
+
#
|
|
56
|
+
# Use this when you want the template's OOXML scaffolding (settings,
|
|
57
|
+
# fonts, styles, theme, content_types, namespace declarations) but
|
|
58
|
+
# will replace the document body with new content. This is the
|
|
59
|
+
# recommended path to producing Word-valid output: starting from a
|
|
60
|
+
# known-good DOCX preserves mc:Ignorable prefixes, rsid values, and
|
|
61
|
+
# other structural guarantees that Word expects.
|
|
62
|
+
#
|
|
63
|
+
# Resets performed:
|
|
64
|
+
# - Body content cleared (paragraphs, tables, SDTs, bookmarks,
|
|
65
|
+
# element_order, section_properties)
|
|
66
|
+
# - User footnotes/endnotes cleared (separator/continuation kept)
|
|
67
|
+
# - custom_properties and custom_xml_items cleared
|
|
68
|
+
# - Stale image and customXml relationships removed
|
|
69
|
+
# - IdAllocator seeded from document_rels and package_rels
|
|
70
|
+
#
|
|
71
|
+
# @param path [String] Path to .docx template file
|
|
72
|
+
# @return [DocumentBuilder]
|
|
73
|
+
# @raise [ArgumentError] if path does not exist
|
|
74
|
+
def self.from_template(path)
|
|
75
|
+
raise ArgumentError, "Template not found: #{path}" unless File.exist?(path)
|
|
76
|
+
|
|
77
|
+
root = Uniword.load(path)
|
|
78
|
+
reset_template_body(root)
|
|
79
|
+
clear_user_notes(root)
|
|
80
|
+
root.custom_properties = nil
|
|
81
|
+
root.custom_xml_items = nil
|
|
82
|
+
remove_stale_relationships(root)
|
|
83
|
+
seed_allocator(root)
|
|
84
|
+
new(root, allocator: root.allocator)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.reset_template_body(root)
|
|
88
|
+
return unless root.body
|
|
89
|
+
|
|
90
|
+
root.body.paragraphs.clear
|
|
91
|
+
root.body.tables.clear
|
|
92
|
+
root.body.structured_document_tags.clear
|
|
93
|
+
root.body.bookmark_starts.clear
|
|
94
|
+
root.body.bookmark_ends.clear
|
|
95
|
+
root.body.element_order = [] if root.body.element_order
|
|
96
|
+
root.body.section_properties = nil
|
|
97
|
+
end
|
|
98
|
+
private_class_method :reset_template_body
|
|
99
|
+
|
|
100
|
+
def self.clear_user_notes(root)
|
|
101
|
+
clear_user_note_entries(root.footnotes, :footnote) if root.footnotes
|
|
102
|
+
clear_user_note_entries(root.endnotes, :endnote) if root.endnotes
|
|
103
|
+
end
|
|
104
|
+
private_class_method :clear_user_notes
|
|
105
|
+
|
|
106
|
+
def self.clear_user_note_entries(notes, kind)
|
|
107
|
+
entries = kind == :footnote ? notes.footnote_entries : notes.endnote_entries
|
|
108
|
+
entries.reject! { |e| e.type != "separator" && e.type != "continuationSeparator" }
|
|
109
|
+
notes.element_order = [] if notes.element_order
|
|
110
|
+
end
|
|
111
|
+
private_class_method :clear_user_note_entries
|
|
112
|
+
|
|
113
|
+
def self.remove_stale_relationships(root)
|
|
114
|
+
remove_rels_by_type_fragment(root.document_rels, "/image")
|
|
115
|
+
remove_rels_by_type_fragment(root.document_rels, "/customXml")
|
|
116
|
+
remove_rels_by_type_fragment(root.package_rels, "custom-properties")
|
|
117
|
+
root.content_types&.overrides&.reject! do |o|
|
|
118
|
+
part = o.part_name.to_s
|
|
119
|
+
part == "/docProps/custom.xml" || part.include?("customXml/")
|
|
120
|
+
end
|
|
121
|
+
root.image_parts = nil
|
|
122
|
+
end
|
|
123
|
+
private_class_method :remove_stale_relationships
|
|
124
|
+
|
|
125
|
+
def self.remove_rels_by_type_fragment(rels, fragment)
|
|
126
|
+
return unless rels&.relationships
|
|
127
|
+
|
|
128
|
+
rels.relationships.reject! { |r| r.type.to_s.include?(fragment) }
|
|
129
|
+
end
|
|
130
|
+
private_class_method :remove_rels_by_type_fragment
|
|
131
|
+
|
|
132
|
+
def self.seed_allocator(root)
|
|
133
|
+
root.allocator = Docx::IdAllocator.populate_from_package(root)
|
|
134
|
+
end
|
|
135
|
+
private_class_method :seed_allocator
|
|
136
|
+
|
|
51
137
|
# Append a top-level element (paragraph or table)
|
|
52
138
|
#
|
|
53
139
|
# @param element [Paragraph, Table, ParagraphBuilder, TableBuilder]
|
|
@@ -56,12 +142,16 @@ module Uniword
|
|
|
56
142
|
case element
|
|
57
143
|
when Wordprocessingml::Paragraph
|
|
58
144
|
@model.body.paragraphs << element
|
|
145
|
+
@model.body.append_to_element_order("p")
|
|
59
146
|
when Wordprocessingml::Table
|
|
60
147
|
@model.body.tables << element
|
|
148
|
+
@model.body.append_to_element_order("tbl")
|
|
61
149
|
when ParagraphBuilder
|
|
62
150
|
@model.body.paragraphs << element.build
|
|
151
|
+
@model.body.append_to_element_order("p")
|
|
63
152
|
when TableBuilder
|
|
64
153
|
@model.body.tables << element.build
|
|
154
|
+
@model.body.append_to_element_order("tbl")
|
|
65
155
|
else
|
|
66
156
|
raise ArgumentError, "Cannot add #{element.class} to document"
|
|
67
157
|
end
|
|
@@ -74,10 +164,10 @@ module Uniword
|
|
|
74
164
|
# @yield [ParagraphBuilder] Builder for configuration
|
|
75
165
|
# @return [ParagraphBuilder] The paragraph builder
|
|
76
166
|
def paragraph(text = nil, &block)
|
|
77
|
-
para = ParagraphBuilder.new
|
|
167
|
+
para = ParagraphBuilder.new(allocator: @allocator)
|
|
78
168
|
para << text if text
|
|
79
169
|
block.call(para) if block_given?
|
|
80
|
-
|
|
170
|
+
self << para
|
|
81
171
|
para
|
|
82
172
|
end
|
|
83
173
|
|
|
@@ -88,10 +178,10 @@ module Uniword
|
|
|
88
178
|
# @yield [ParagraphBuilder] Builder for additional configuration
|
|
89
179
|
# @return [ParagraphBuilder] The paragraph builder
|
|
90
180
|
def heading(text, level: 1)
|
|
91
|
-
para = ParagraphBuilder.new
|
|
181
|
+
para = ParagraphBuilder.new(allocator: @allocator)
|
|
92
182
|
para.style = "Heading#{level}"
|
|
93
183
|
para << text
|
|
94
|
-
|
|
184
|
+
self << para
|
|
95
185
|
para
|
|
96
186
|
end
|
|
97
187
|
|
|
@@ -99,7 +189,7 @@ module Uniword
|
|
|
99
189
|
#
|
|
100
190
|
# @return [self]
|
|
101
191
|
def page_break
|
|
102
|
-
|
|
192
|
+
self << Wordprocessingml::Paragraph.new(
|
|
103
193
|
runs: [Builder.page_break]
|
|
104
194
|
)
|
|
105
195
|
self
|
|
@@ -112,7 +202,7 @@ module Uniword
|
|
|
112
202
|
def table(&block)
|
|
113
203
|
tbl = TableBuilder.new
|
|
114
204
|
block.call(tbl) if block_given?
|
|
115
|
-
|
|
205
|
+
self << tbl
|
|
116
206
|
tbl
|
|
117
207
|
end
|
|
118
208
|
|
|
@@ -135,7 +225,7 @@ module Uniword
|
|
|
135
225
|
# @yield [HeaderFooterBuilder] Builder for header content
|
|
136
226
|
# @return [HeaderFooterBuilder] The header/footer builder
|
|
137
227
|
def header(type: "default", &block)
|
|
138
|
-
hf = HeaderFooterBuilder.new(:header, type: type)
|
|
228
|
+
hf = HeaderFooterBuilder.new(:header, type: type, allocator: @allocator)
|
|
139
229
|
block.call(hf) if block_given?
|
|
140
230
|
(@model.headers ||= {})[type] = hf.build
|
|
141
231
|
hf
|
|
@@ -147,7 +237,7 @@ module Uniword
|
|
|
147
237
|
# @yield [HeaderFooterBuilder] Builder for footer content
|
|
148
238
|
# @return [HeaderFooterBuilder] The header/footer builder
|
|
149
239
|
def footer(type: "default", &block)
|
|
150
|
-
hf = HeaderFooterBuilder.new(:footer, type: type)
|
|
240
|
+
hf = HeaderFooterBuilder.new(:footer, type: type, allocator: @allocator)
|
|
151
241
|
block.call(hf) if block_given?
|
|
152
242
|
(@model.footers ||= {})[type] = hf.build
|
|
153
243
|
hf
|
|
@@ -160,7 +250,7 @@ module Uniword
|
|
|
160
250
|
# @return [self]
|
|
161
251
|
def toc(title: "Table of Contents", styles: nil)
|
|
162
252
|
TocBuilder.build(title: title, styles: styles).each do |para|
|
|
163
|
-
|
|
253
|
+
self << para
|
|
164
254
|
end
|
|
165
255
|
self
|
|
166
256
|
end
|
|
@@ -198,16 +288,17 @@ module Uniword
|
|
|
198
288
|
# @yield [ParagraphBuilder] Builder for bookmark content
|
|
199
289
|
# @return [ParagraphBuilder] The paragraph builder
|
|
200
290
|
def bookmark(name, &block)
|
|
201
|
-
@
|
|
202
|
-
|
|
291
|
+
id = @allocator ? @allocator.alloc_bookmark_id : begin
|
|
292
|
+
@bookmark_counter ||= 0
|
|
293
|
+
@bookmark_counter += 1
|
|
294
|
+
@bookmark_counter.to_s
|
|
295
|
+
end
|
|
203
296
|
|
|
204
|
-
para = ParagraphBuilder.new
|
|
205
|
-
para.
|
|
206
|
-
Wordprocessingml::BookmarkStart.new(id: id, name: name)
|
|
297
|
+
para = ParagraphBuilder.new(allocator: @allocator)
|
|
298
|
+
para << Wordprocessingml::BookmarkStart.new(id: id, name: name)
|
|
207
299
|
block.call(para) if block_given?
|
|
208
|
-
para.
|
|
209
|
-
|
|
210
|
-
@model.body.paragraphs << para.build
|
|
300
|
+
para << Wordprocessingml::BookmarkEnd.new(id: id)
|
|
301
|
+
self << para
|
|
211
302
|
para
|
|
212
303
|
end
|
|
213
304
|
|
|
@@ -306,12 +397,12 @@ module Uniword
|
|
|
306
397
|
# @param size [Integer] Border size in eighths of a point (default 6)
|
|
307
398
|
# @return [self]
|
|
308
399
|
def horizontal_rule(style: "single", color: "auto", size: 6)
|
|
309
|
-
para = ParagraphBuilder.new
|
|
400
|
+
para = ParagraphBuilder.new(allocator: @allocator)
|
|
310
401
|
para.borders(
|
|
311
402
|
bottom: { style: style, color: color, size: size }
|
|
312
403
|
)
|
|
313
404
|
para.spacing(after: 0)
|
|
314
|
-
|
|
405
|
+
self << para
|
|
315
406
|
self
|
|
316
407
|
end
|
|
317
408
|
|
|
@@ -327,7 +418,7 @@ module Uniword
|
|
|
327
418
|
para.runs << ImageBuilder.create_run(
|
|
328
419
|
self, path, width: width, height: height, alt_text: alt_text
|
|
329
420
|
)
|
|
330
|
-
|
|
421
|
+
self << para
|
|
331
422
|
self
|
|
332
423
|
end
|
|
333
424
|
|
|
@@ -350,7 +441,7 @@ module Uniword
|
|
|
350
441
|
self, path, width: width, height: height, alt_text: alt_text,
|
|
351
442
|
align: align, wrap: wrap, behind_text: behind_text
|
|
352
443
|
)
|
|
353
|
-
|
|
444
|
+
self << para
|
|
354
445
|
self
|
|
355
446
|
end
|
|
356
447
|
|
|
@@ -389,10 +480,14 @@ module Uniword
|
|
|
389
480
|
# @yield [CommentBuilder] Builder for rich comment content
|
|
390
481
|
# @return [Comment] The created Comment model
|
|
391
482
|
def comment(author:, text: nil, initials: nil, &block)
|
|
392
|
-
@
|
|
483
|
+
comment_id = @allocator ? @allocator.alloc_comment_id : begin
|
|
484
|
+
@comment_counter ||= 0
|
|
485
|
+
@comment_counter += 1
|
|
486
|
+
@comment_counter.to_s
|
|
487
|
+
end
|
|
393
488
|
cb = CommentBuilder.new(
|
|
394
489
|
author: author,
|
|
395
|
-
comment_id:
|
|
490
|
+
comment_id: comment_id,
|
|
396
491
|
initials: initials
|
|
397
492
|
)
|
|
398
493
|
cb << text if text
|
|
@@ -437,7 +532,7 @@ module Uniword
|
|
|
437
532
|
sdt = SdtBuilder.bibliography.build
|
|
438
533
|
para = Wordprocessingml::Paragraph.new
|
|
439
534
|
para.sdts << sdt
|
|
440
|
-
|
|
535
|
+
self << para
|
|
441
536
|
self
|
|
442
537
|
end
|
|
443
538
|
|
|
@@ -458,7 +553,7 @@ module Uniword
|
|
|
458
553
|
run.drawings << drawing
|
|
459
554
|
para = Wordprocessingml::Paragraph.new
|
|
460
555
|
para.runs << run
|
|
461
|
-
|
|
556
|
+
self << para
|
|
462
557
|
cb
|
|
463
558
|
end
|
|
464
559
|
|
|
@@ -466,7 +561,7 @@ module Uniword
|
|
|
466
561
|
#
|
|
467
562
|
# @return [self]
|
|
468
563
|
def page_number
|
|
469
|
-
|
|
564
|
+
self << Builder.page_number_field
|
|
470
565
|
self
|
|
471
566
|
end
|
|
472
567
|
|
|
@@ -474,7 +569,7 @@ module Uniword
|
|
|
474
569
|
#
|
|
475
570
|
# @return [self]
|
|
476
571
|
def total_pages
|
|
477
|
-
|
|
572
|
+
self << Builder.total_pages_field
|
|
478
573
|
self
|
|
479
574
|
end
|
|
480
575
|
|
|
@@ -483,7 +578,7 @@ module Uniword
|
|
|
483
578
|
# @param format [String] Date format (default 'M/d/yyyy')
|
|
484
579
|
# @return [self]
|
|
485
580
|
def date_field(format: "M/d/yyyy")
|
|
486
|
-
|
|
581
|
+
self << Builder.date_field(format: format)
|
|
487
582
|
self
|
|
488
583
|
end
|
|
489
584
|
|
|
@@ -492,7 +587,7 @@ module Uniword
|
|
|
492
587
|
# @param format [String] Time format (default 'h:mm:ss am/pm')
|
|
493
588
|
# @return [self]
|
|
494
589
|
def time_field(format: "h:mm:ss am/pm")
|
|
495
|
-
|
|
590
|
+
self << Builder.time_field(format: format)
|
|
496
591
|
self
|
|
497
592
|
end
|
|
498
593
|
|