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
|
@@ -24,6 +24,13 @@ module Uniword
|
|
|
24
24
|
# Generate package-level .rels file
|
|
25
25
|
#
|
|
26
26
|
# @return [PackageRelationships] Relationships object for _rels/.rels
|
|
27
|
+
def self.next_available_rid(relationships)
|
|
28
|
+
max = relationships.relationships.filter_map do |r|
|
|
29
|
+
r.id[/\ArId(\d+)\z/, 1]&.to_i
|
|
30
|
+
end.max || 0
|
|
31
|
+
"rId#{max + 1}"
|
|
32
|
+
end
|
|
33
|
+
|
|
27
34
|
def self.generate_package_rels
|
|
28
35
|
new(
|
|
29
36
|
relationships: [
|
|
@@ -28,8 +28,8 @@ module Uniword
|
|
|
28
28
|
namespace Ooxml::Namespaces::WordProcessingML
|
|
29
29
|
|
|
30
30
|
map_element "top", to: :top, render_nil: false
|
|
31
|
-
map_element "bottom", to: :bottom, render_nil: false
|
|
32
31
|
map_element "left", to: :left, render_nil: false
|
|
32
|
+
map_element "bottom", to: :bottom, render_nil: false
|
|
33
33
|
map_element "right", to: :right, render_nil: false
|
|
34
34
|
map_element "between", to: :between, render_nil: false
|
|
35
35
|
map_element "bar", to: :bar, render_nil: false
|
|
@@ -17,8 +17,8 @@ module Uniword
|
|
|
17
17
|
element "numPr"
|
|
18
18
|
namespace Ooxml::Namespaces::WordProcessingML
|
|
19
19
|
|
|
20
|
-
map_element "numId", to: :num_id, render_nil: false
|
|
21
20
|
map_element "ilvl", to: :ilvl, render_nil: false
|
|
21
|
+
map_element "numId", to: :num_id, render_nil: false
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
end
|
|
@@ -17,6 +17,19 @@ module Uniword
|
|
|
17
17
|
map_attribute "val", to: :value
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
# Cast raw style-id strings into references (mirrors Text.cast), so
|
|
21
|
+
# `properties.style = "Heading1"` stores a proper StyleReference
|
|
22
|
+
def self.cast(value)
|
|
23
|
+
case value
|
|
24
|
+
when StyleReference, nil
|
|
25
|
+
value
|
|
26
|
+
when String
|
|
27
|
+
new(value: value)
|
|
28
|
+
else
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
20
33
|
# Compare with another StyleReference or a string
|
|
21
34
|
def ==(other)
|
|
22
35
|
if other.is_a?(StyleReference)
|
|
@@ -72,7 +72,7 @@ module Uniword
|
|
|
72
72
|
# @param paragraph [Paragraph] The paragraph to check
|
|
73
73
|
# @return [Integer, nil] Heading level (1-9) or nil if not a heading
|
|
74
74
|
def extract_heading_level(paragraph)
|
|
75
|
-
style_name = paragraph.
|
|
75
|
+
style_name = paragraph.style
|
|
76
76
|
return nil unless style_name
|
|
77
77
|
|
|
78
78
|
match = style_name.to_s.match(HEADING_PATTERN)
|
|
@@ -105,12 +105,12 @@ module Uniword
|
|
|
105
105
|
# recursively process all text nodes
|
|
106
106
|
return unless element.is_a?(Uniword::Wordprocessingml::Run)
|
|
107
107
|
|
|
108
|
-
text = element.
|
|
108
|
+
text = element.text_string
|
|
109
109
|
# Simple variable replacement ({{var}})
|
|
110
110
|
text.scan(/\{\{([^@].+?)\}\}/).each do |match|
|
|
111
111
|
var_name = match[0]
|
|
112
112
|
value = resolver.resolve(var_name)
|
|
113
|
-
element.
|
|
113
|
+
element.substitute("{{#{var_name}}}", value.to_s)
|
|
114
114
|
end
|
|
115
115
|
end
|
|
116
116
|
end
|
|
@@ -175,7 +175,20 @@ module Uniword
|
|
|
175
175
|
|
|
176
176
|
return false if left_num.nil? || right_num.nil?
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
case operator
|
|
179
|
+
when :== then left_num == right_num
|
|
180
|
+
when :!= then left_num != right_num
|
|
181
|
+
when :> then left_num > right_num
|
|
182
|
+
when :>= then left_num >= right_num
|
|
183
|
+
when :< then left_num < right_num
|
|
184
|
+
when :<= then left_num <= right_num
|
|
185
|
+
when :+ then left_num + right_num
|
|
186
|
+
when :- then left_num - right_num
|
|
187
|
+
when :* then left_num * right_num
|
|
188
|
+
when :/ then left_num / right_num
|
|
189
|
+
else
|
|
190
|
+
raise ArgumentError, "Unsupported operator: #{operator.inspect}"
|
|
191
|
+
end
|
|
179
192
|
end
|
|
180
193
|
|
|
181
194
|
# Convert value to number
|
|
@@ -79,7 +79,7 @@ module Uniword
|
|
|
79
79
|
|
|
80
80
|
# Set the color using the appropriate attribute
|
|
81
81
|
attr_name = color_name == "folHlink" ? :fol_hlink : color_name.to_sym
|
|
82
|
-
scheme.
|
|
82
|
+
scheme.assign_color_by_name(attr_name, color_obj)
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
# Sync hash interface
|
|
@@ -142,14 +142,7 @@ module Uniword
|
|
|
142
142
|
# @param paragraph [Wordprocessingml::Paragraph] Paragraph to inspect
|
|
143
143
|
# @return [String, nil] Style name or nil
|
|
144
144
|
def resolve_style_name(paragraph)
|
|
145
|
-
|
|
146
|
-
return nil unless style_ref
|
|
147
|
-
|
|
148
|
-
if style_ref.is_a?(Uniword::Properties::StyleReference)
|
|
149
|
-
style_ref.value
|
|
150
|
-
else
|
|
151
|
-
style_ref.to_s
|
|
152
|
-
end
|
|
145
|
+
paragraph.style&.value
|
|
153
146
|
end
|
|
154
147
|
|
|
155
148
|
# Extract text content from a paragraph.
|
|
@@ -197,9 +190,7 @@ module Uniword
|
|
|
197
190
|
def build_title_paragraph
|
|
198
191
|
para = Wordprocessingml::Paragraph.new
|
|
199
192
|
para.properties = Wordprocessingml::ParagraphProperties.new
|
|
200
|
-
para.properties.style =
|
|
201
|
-
value: "TOCHeading",
|
|
202
|
-
)
|
|
193
|
+
para.properties.style = "TOCHeading"
|
|
203
194
|
|
|
204
195
|
run = Wordprocessingml::Run.new
|
|
205
196
|
run.text = "Table of Contents"
|
|
@@ -251,9 +242,7 @@ module Uniword
|
|
|
251
242
|
def build_entry_paragraph(entry)
|
|
252
243
|
para = Wordprocessingml::Paragraph.new
|
|
253
244
|
para.properties = Wordprocessingml::ParagraphProperties.new
|
|
254
|
-
para.properties.style =
|
|
255
|
-
value: "TOC#{entry.level}",
|
|
256
|
-
)
|
|
245
|
+
para.properties.style = "TOC#{entry.level}"
|
|
257
246
|
|
|
258
247
|
run = Wordprocessingml::Run.new
|
|
259
248
|
run.text = entry.text
|
|
@@ -69,7 +69,8 @@ module Uniword
|
|
|
69
69
|
# Convert OOXML Run to HTML
|
|
70
70
|
def run_to_html(run)
|
|
71
71
|
# Handle breaks and drawings first (these never get style wrapping)
|
|
72
|
-
|
|
72
|
+
# (parsed runs hold [] for absent collections, so check emptiness)
|
|
73
|
+
return break_to_html(run.break) if Array(run.break).any?
|
|
73
74
|
if run.drawings && !run.drawings.empty?
|
|
74
75
|
return drawing_to_html(run.drawings.first)
|
|
75
76
|
end
|
|
@@ -105,7 +106,8 @@ module Uniword
|
|
|
105
106
|
# Skip wrapping for internal OOXML styles that shouldn't produce
|
|
106
107
|
# visible HTML class wrappers
|
|
107
108
|
is_internal_style = %w[stem hyperlink].include?(style_id)
|
|
108
|
-
is_stem_spacer = (style_id == "stem") &&
|
|
109
|
+
is_stem_spacer = (style_id == "stem") &&
|
|
110
|
+
run.text_string.strip.empty?
|
|
109
111
|
unless is_internal_style || is_stem_spacer
|
|
110
112
|
style_val = run_style_to_class(props.style.value)
|
|
111
113
|
content = %(<span class="#{style_val}">#{content}</span>)
|
|
@@ -125,7 +127,7 @@ module Uniword
|
|
|
125
127
|
elsif run.tab
|
|
126
128
|
tab_to_html(run)
|
|
127
129
|
else
|
|
128
|
-
text = run.
|
|
130
|
+
text = run.text_string
|
|
129
131
|
return "" if text.empty?
|
|
130
132
|
|
|
131
133
|
escape_html(text)
|
|
@@ -142,13 +144,15 @@ module Uniword
|
|
|
142
144
|
%(<span class="MsoEndnoteReference"><span style="mso-special-character:endnote"></span></span>)
|
|
143
145
|
end
|
|
144
146
|
|
|
145
|
-
# Convert OOXML
|
|
146
|
-
def break_to_html(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
# Convert OOXML Breaks to HTML, one <br> per Break element
|
|
148
|
+
def break_to_html(breaks)
|
|
149
|
+
Array(breaks).map do |brk|
|
|
150
|
+
if brk.type == "page"
|
|
151
|
+
%(<br clear="all" style="mso-special-character:line-break;page-break-before:always" />)
|
|
152
|
+
else
|
|
153
|
+
%(<br />)
|
|
154
|
+
end
|
|
155
|
+
end.join
|
|
152
156
|
end
|
|
153
157
|
|
|
154
158
|
# Convert OOXML FieldChar to HTML span
|
|
@@ -272,7 +276,9 @@ module Uniword
|
|
|
272
276
|
end
|
|
273
277
|
end
|
|
274
278
|
|
|
275
|
-
# Convert OOXML OMath to HTML (wrapped in stem span)
|
|
279
|
+
# Convert an OOXML OMath model to HTML (wrapped in stem span).
|
|
280
|
+
# Accepts Omml::Models::CTOMath/OMath/OMathPara (the typed attribute
|
|
281
|
+
# values on Paragraph) — anything responding to `to_xml` works.
|
|
276
282
|
def omath_to_html(o_math)
|
|
277
283
|
xml = o_math.to_xml
|
|
278
284
|
# Strip XML declaration and namespace prefixes for clean inline HTML
|
|
@@ -491,23 +497,87 @@ module Uniword
|
|
|
491
497
|
|
|
492
498
|
private
|
|
493
499
|
|
|
494
|
-
#
|
|
495
|
-
#
|
|
500
|
+
# Render paragraph content (runs, hyperlinks, SDTs, oMath) in the
|
|
501
|
+
# document order captured by `paragraph.element_order`.
|
|
502
|
+
#
|
|
503
|
+
# Walking element_order (rather than `build_position_map` keyed on
|
|
504
|
+
# run_position) keeps MHTML positioning in lockstep with the DOCX
|
|
505
|
+
# serializer, which is the single source of truth for interleaving.
|
|
506
|
+
# It also handles inline math, which is now an Omml::Models::CTOMath
|
|
507
|
+
# instance without a run_position attribute.
|
|
496
508
|
def paragraph_content_to_html(paragraph)
|
|
509
|
+
order = paragraph.element_order
|
|
510
|
+
return paragraph_content_to_html_legacy(paragraph) if order.nil? || order.empty?
|
|
511
|
+
|
|
512
|
+
run_idx = 0
|
|
513
|
+
hl_idx = 0
|
|
514
|
+
omath_idx = 0
|
|
515
|
+
omp_idx = 0
|
|
516
|
+
sdt_idx = 0
|
|
517
|
+
|
|
518
|
+
parts = []
|
|
519
|
+
order.each do |entry|
|
|
520
|
+
next unless entry.element?
|
|
521
|
+
|
|
522
|
+
case entry.name
|
|
523
|
+
when "r"
|
|
524
|
+
collect_run_at(paragraph, run_idx, parts)
|
|
525
|
+
run_idx += 1
|
|
526
|
+
when "hyperlink"
|
|
527
|
+
hl = paragraph.hyperlinks[hl_idx]
|
|
528
|
+
parts << hyperlink_to_html(hl) if hl
|
|
529
|
+
hl_idx += 1
|
|
530
|
+
when "oMath"
|
|
531
|
+
om = paragraph.o_maths[omath_idx]
|
|
532
|
+
parts << omath_to_html(om) if om
|
|
533
|
+
omath_idx += 1
|
|
534
|
+
when "oMathPara"
|
|
535
|
+
omp = paragraph.o_math_paras[omp_idx]
|
|
536
|
+
parts << omath_to_html(omp) if omp
|
|
537
|
+
omp_idx += 1
|
|
538
|
+
when "sdt"
|
|
539
|
+
sdt = paragraph.sdts[sdt_idx]
|
|
540
|
+
parts << sdt_to_inline_html(sdt) if sdt
|
|
541
|
+
sdt_idx += 1
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# Defensive: emit any math/SDTs not represented in element_order.
|
|
546
|
+
paragraph.o_math_paras.each_with_index do |omp, i|
|
|
547
|
+
next if i < omp_idx
|
|
548
|
+
|
|
549
|
+
parts << omath_to_html(omp)
|
|
550
|
+
end
|
|
551
|
+
paragraph.sdts.each_with_index do |sdt, i|
|
|
552
|
+
next if i < sdt_idx
|
|
553
|
+
|
|
554
|
+
parts << sdt_to_inline_html(sdt)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
parts.join
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def collect_run_at(paragraph, idx, parts)
|
|
561
|
+
run = paragraph.runs[idx]
|
|
562
|
+
return unless run
|
|
563
|
+
|
|
564
|
+
parts << if run.is_a?(Uniword::Wordprocessingml::StructuredDocumentTag)
|
|
565
|
+
sdt_to_inline_html(run)
|
|
566
|
+
else
|
|
567
|
+
run_to_html(run)
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# Fallback for Paragraphs constructed without element_order tracking
|
|
572
|
+
# (older code paths). Preserves the previous behavior of appending
|
|
573
|
+
# inline math/hyperlinks at the end.
|
|
574
|
+
def paragraph_content_to_html_legacy(paragraph)
|
|
497
575
|
run_count = paragraph.runs.size
|
|
498
576
|
|
|
499
|
-
# Build position-indexed maps of hyperlinks and oMaths
|
|
500
577
|
hl_by_pos = build_position_map(paragraph.hyperlinks, run_count)
|
|
501
|
-
omath_by_pos = build_position_map(paragraph.o_maths, run_count)
|
|
502
578
|
|
|
503
579
|
parts = []
|
|
504
580
|
paragraph.runs.each_with_index do |run, idx|
|
|
505
|
-
# Insert any oMaths that should appear at this position
|
|
506
|
-
if omath_by_pos.key?(idx)
|
|
507
|
-
omath_by_pos[idx].each { |om| parts << omath_to_html(om) }
|
|
508
|
-
end
|
|
509
|
-
|
|
510
|
-
# Insert any hyperlinks that should appear at this position
|
|
511
581
|
if hl_by_pos.key?(idx)
|
|
512
582
|
hl_by_pos[idx].each { |hl| parts << hyperlink_to_html(hl) }
|
|
513
583
|
end
|
|
@@ -519,26 +589,13 @@ module Uniword
|
|
|
519
589
|
end
|
|
520
590
|
end
|
|
521
591
|
|
|
522
|
-
# Append any remaining oMaths at the end
|
|
523
|
-
if omath_by_pos.key?(run_count)
|
|
524
|
-
omath_by_pos[run_count].each { |om| parts << omath_to_html(om) }
|
|
525
|
-
end
|
|
526
|
-
|
|
527
|
-
# Append any remaining hyperlinks at the end
|
|
528
592
|
if hl_by_pos.key?(run_count)
|
|
529
593
|
hl_by_pos[run_count].each { |hl| parts << hyperlink_to_html(hl) }
|
|
530
594
|
end
|
|
531
595
|
|
|
532
|
-
paragraph.
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
parts << omath_to_html(om)
|
|
536
|
-
end
|
|
537
|
-
end
|
|
538
|
-
|
|
539
|
-
paragraph.sdts.each do |sdt|
|
|
540
|
-
parts << sdt_to_inline_html(sdt)
|
|
541
|
-
end
|
|
596
|
+
paragraph.o_maths.each { |om| parts << omath_to_html(om) }
|
|
597
|
+
paragraph.o_math_paras.each { |omp| parts << omath_to_html(omp) }
|
|
598
|
+
paragraph.sdts.each { |sdt| parts << sdt_to_inline_html(sdt) }
|
|
542
599
|
|
|
543
600
|
parts.join
|
|
544
601
|
end
|
|
@@ -54,7 +54,7 @@ module Uniword
|
|
|
54
54
|
# @param run [Uniword::Wordprocessingml::Run]
|
|
55
55
|
# @return [String] HTML text content with inline formatting
|
|
56
56
|
def self.run_to_html(run)
|
|
57
|
-
text = escape_html(run.
|
|
57
|
+
text = escape_html(run.text_string)
|
|
58
58
|
return text if text.empty?
|
|
59
59
|
|
|
60
60
|
props = run.properties
|
|
@@ -135,12 +135,10 @@ module Uniword
|
|
|
135
135
|
# @param paragraph [Uniword::Wordprocessingml::Paragraph]
|
|
136
136
|
# @return [String] HTML class/style attribute or empty string
|
|
137
137
|
def self.paragraph_style(paragraph)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
style = paragraph.properties.style
|
|
138
|
+
style = paragraph.style
|
|
141
139
|
return "" unless style
|
|
142
140
|
|
|
143
|
-
" class=\"#{escape_html(style)}\""
|
|
141
|
+
" class=\"#{escape_html(style.to_s)}\""
|
|
144
142
|
end
|
|
145
143
|
|
|
146
144
|
# Convert OOXML font size (half-points) to HTML font size
|
|
@@ -133,10 +133,9 @@ module Uniword
|
|
|
133
133
|
return [] unless document.is_a?(Uniword::Wordprocessingml::DocumentRoot)
|
|
134
134
|
|
|
135
135
|
document.paragraphs.select do |p|
|
|
136
|
-
p.
|
|
136
|
+
p.style&.to_s&.match?(/^Heading/)
|
|
137
137
|
end.filter_map do |p|
|
|
138
|
-
|
|
139
|
-
p.is_a?(Lutaml::Model::Serializable) && p.class.attributes.key?(:text) ? heading_to_bookmark(p.text) : nil
|
|
138
|
+
heading_to_bookmark(p.text)
|
|
140
139
|
end
|
|
141
140
|
end
|
|
142
141
|
|
data/lib/uniword/version.rb
CHANGED
|
@@ -103,7 +103,8 @@ module Uniword
|
|
|
103
103
|
# @param run [Run] The run to visit
|
|
104
104
|
# @return [void]
|
|
105
105
|
def visit_run(run)
|
|
106
|
-
|
|
106
|
+
text = run.text_string
|
|
107
|
+
@text_parts << text unless text.empty?
|
|
107
108
|
end
|
|
108
109
|
|
|
109
110
|
# Visit an image element.
|
|
@@ -108,11 +108,11 @@ module Uniword
|
|
|
108
108
|
return false unless paragraph.properties
|
|
109
109
|
return false unless paragraph.properties.is_a?(Uniword::Wordprocessingml::ParagraphProperties)
|
|
110
110
|
|
|
111
|
-
paragraph.
|
|
111
|
+
paragraph.style == "UniwordWatermark"
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
def extract_watermark_text(paragraph)
|
|
115
|
-
paragraph.runs.map
|
|
115
|
+
paragraph.runs.map(&:text_string).join
|
|
116
116
|
end
|
|
117
117
|
end
|
|
118
118
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
module Wordprocessingml
|
|
7
|
+
class AllowPng < Lutaml::Model::Serializable
|
|
8
|
+
xml do
|
|
9
|
+
element "allowPNG"
|
|
10
|
+
namespace Uniword::Ooxml::Namespaces::WordProcessingML
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -66,53 +66,70 @@ module Uniword
|
|
|
66
66
|
sync_element_order
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
# Record a programmatically-added element in element_order.
|
|
70
|
+
# Called by DocumentBuilder to preserve insertion order when
|
|
71
|
+
# interleaving paragraphs, tables, and SDTs.
|
|
72
|
+
def append_to_element_order(element_name)
|
|
73
|
+
entry = Lutaml::Xml::Element.new("Element", element_name)
|
|
74
|
+
if element_order.nil? || element_order.empty?
|
|
75
|
+
self.element_order = [entry]
|
|
76
|
+
else
|
|
77
|
+
self.element_order = element_order + [entry]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
69
81
|
private
|
|
70
82
|
|
|
71
83
|
def sync_element_order
|
|
72
84
|
return if element_order.nil? || element_order.empty?
|
|
73
85
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
needed = false
|
|
79
|
-
needed = true if paragraphs.size > counts["p"]
|
|
80
|
-
needed = true if tables.size > counts["tbl"]
|
|
81
|
-
needed = true if structured_document_tags.size > counts["sdt"]
|
|
82
|
-
needed = true if bookmark_starts.size > counts["bookmarkStart"]
|
|
83
|
-
needed = true if bookmark_ends.size > counts["bookmarkEnd"]
|
|
84
|
-
needed = true if section_properties && counts["sectPr"].zero?
|
|
86
|
+
# lutaml-model freezes element_order after XML parsing.
|
|
87
|
+
# Build a mutable copy to append missing elements.
|
|
88
|
+
new_entries = []
|
|
85
89
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
# Count how many p/tbl/sdt entries exist in element_order
|
|
91
|
+
ordered_p_count = element_order.count { |e| e.name == "p" }
|
|
92
|
+
ordered_tbl_count = element_order.count { |e| e.name == "tbl" }
|
|
93
|
+
ordered_sdt_count = element_order.count { |e| e.name == "sdt" }
|
|
94
|
+
ordered_bookmark_start_count = element_order.count do |e|
|
|
95
|
+
e.name == "bookmarkStart"
|
|
96
|
+
end
|
|
97
|
+
ordered_bookmark_end_count = element_order.count do |e|
|
|
98
|
+
e.name == "bookmarkEnd"
|
|
99
|
+
end
|
|
89
100
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
append_missing_elements("bookmarkStart",
|
|
95
|
-
bookmark_starts.size - counts["bookmarkStart"])
|
|
96
|
-
append_missing_elements("bookmarkEnd",
|
|
97
|
-
bookmark_ends.size - counts["bookmarkEnd"])
|
|
101
|
+
# Collect missing paragraphs
|
|
102
|
+
(paragraphs.size - ordered_p_count).times do
|
|
103
|
+
new_entries << Lutaml::Xml::Element.new("Element", "p")
|
|
104
|
+
end
|
|
98
105
|
|
|
99
|
-
|
|
106
|
+
# Collect missing tables
|
|
107
|
+
(tables.size - ordered_tbl_count).times do
|
|
108
|
+
new_entries << Lutaml::Xml::Element.new("Element", "tbl")
|
|
109
|
+
end
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
# Collect missing structured document tags
|
|
112
|
+
(structured_document_tags.size - ordered_sdt_count).times do
|
|
113
|
+
new_entries << Lutaml::Xml::Element.new("Element", "sdt")
|
|
114
|
+
end
|
|
103
115
|
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
# Collect missing bookmark starts
|
|
117
|
+
(bookmark_starts.size - ordered_bookmark_start_count).times do
|
|
118
|
+
new_entries << Lutaml::Xml::Element.new("Element", "bookmarkStart")
|
|
119
|
+
end
|
|
106
120
|
|
|
107
|
-
|
|
108
|
-
|
|
121
|
+
# Collect missing bookmark ends
|
|
122
|
+
(bookmark_ends.size - ordered_bookmark_end_count).times do
|
|
123
|
+
new_entries << Lutaml::Xml::Element.new("Element", "bookmarkEnd")
|
|
124
|
+
end
|
|
109
125
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
126
|
+
# Ensure section_properties is in element_order if present
|
|
127
|
+
if section_properties && element_order.none? { |e| e.name == "sectPr" }
|
|
128
|
+
new_entries << Lutaml::Xml::Element.new("Element", "sectPr")
|
|
129
|
+
end
|
|
113
130
|
|
|
114
|
-
|
|
115
|
-
|
|
131
|
+
# Replace frozen array with mutable copy containing new entries
|
|
132
|
+
self.element_order = element_order + new_entries unless new_entries.empty?
|
|
116
133
|
end
|
|
117
134
|
end
|
|
118
135
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
module Wordprocessingml
|
|
7
|
+
class BodyDiv < Lutaml::Model::Serializable
|
|
8
|
+
attribute :val, :string
|
|
9
|
+
|
|
10
|
+
xml do
|
|
11
|
+
element "bodyDiv"
|
|
12
|
+
namespace Uniword::Ooxml::Namespaces::WordProcessingML
|
|
13
|
+
map_attribute "val", to: :val
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
module Wordprocessingml
|
|
7
|
+
class DivBorder < Lutaml::Model::Serializable
|
|
8
|
+
attribute :val, :string
|
|
9
|
+
attribute :sz, :string
|
|
10
|
+
attribute :space, :string
|
|
11
|
+
attribute :color, :string
|
|
12
|
+
|
|
13
|
+
xml do
|
|
14
|
+
element "border"
|
|
15
|
+
namespace Uniword::Ooxml::Namespaces::WordProcessingML
|
|
16
|
+
map_attribute "val", to: :val
|
|
17
|
+
map_attribute "sz", to: :sz
|
|
18
|
+
map_attribute "space", to: :space
|
|
19
|
+
map_attribute "color", to: :color
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|