uniword 1.0.10 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. checksums.yaml +4 -4
  2. data/config/font_metadata.yml +56 -0
  3. data/lib/uniword/accessibility/rules/descriptive_headings_rule.rb +1 -1
  4. data/lib/uniword/assembly/toc.rb +4 -14
  5. data/lib/uniword/assembly/variable_substitutor.rb +3 -4
  6. data/lib/uniword/batch/stages/normalize_styles_stage.rb +4 -2
  7. data/lib/uniword/builder/base_builder.rb +25 -0
  8. data/lib/uniword/builder/chart_builder.rb +33 -14
  9. data/lib/uniword/builder/deterministic_id.rb +17 -0
  10. data/lib/uniword/builder/document_builder.rb +129 -34
  11. data/lib/uniword/builder/footnote_builder.rb +22 -23
  12. data/lib/uniword/builder/header_footer_builder.rb +10 -10
  13. data/lib/uniword/builder/image_builder.rb +24 -16
  14. data/lib/uniword/builder/list_builder.rb +2 -1
  15. data/lib/uniword/builder/paragraph_builder.rb +95 -20
  16. data/lib/uniword/builder/run_utils.rb +97 -0
  17. data/lib/uniword/builder/section_builder.rb +40 -67
  18. data/lib/uniword/builder/table_builder.rb +32 -13
  19. data/lib/uniword/builder/table_cell_builder.rb +19 -0
  20. data/lib/uniword/builder.rb +27 -19
  21. data/lib/uniword/cli/diff_cli.rb +0 -1
  22. data/lib/uniword/cli/generate_cli.rb +0 -1
  23. data/lib/uniword/cli/headers_cli.rb +0 -1
  24. data/lib/uniword/cli/images_cli.rb +0 -1
  25. data/lib/uniword/cli/main.rb +6 -7
  26. data/lib/uniword/cli/protect_cli.rb +0 -1
  27. data/lib/uniword/cli/resources_cli.rb +0 -1
  28. data/lib/uniword/cli/review_cli.rb +0 -1
  29. data/lib/uniword/cli/spellcheck_cli.rb +0 -1
  30. data/lib/uniword/cli/styleset_cli.rb +0 -1
  31. data/lib/uniword/cli/template_cli.rb +0 -1
  32. data/lib/uniword/cli/theme_cli.rb +0 -1
  33. data/lib/uniword/cli/toc_cli.rb +0 -1
  34. data/lib/uniword/cli/watermark_cli.rb +0 -1
  35. data/lib/uniword/cli.rb +6 -17
  36. data/lib/uniword/diff/document_differ.rb +23 -5
  37. data/lib/uniword/diff/package_differ.rb +0 -1
  38. data/lib/uniword/diff.rb +4 -0
  39. data/lib/uniword/document_factory.rb +16 -24
  40. data/lib/uniword/docx/document_statistics.rb +42 -54
  41. data/lib/uniword/docx/id_allocator.rb +143 -0
  42. data/lib/uniword/docx/package.rb +42 -6
  43. data/lib/uniword/docx/package_defaults.rb +16 -25
  44. data/lib/uniword/docx/package_serialization.rb +131 -127
  45. data/lib/uniword/docx/reconciler/body.rb +197 -0
  46. data/lib/uniword/docx/reconciler/fix_codes.rb +74 -0
  47. data/lib/uniword/docx/reconciler/helpers.rb +337 -0
  48. data/lib/uniword/docx/reconciler/notes.rb +301 -0
  49. data/lib/uniword/docx/reconciler/package_structure.rb +322 -0
  50. data/lib/uniword/docx/reconciler/parts.rb +484 -0
  51. data/lib/uniword/docx/reconciler/referential_integrity.rb +449 -0
  52. data/lib/uniword/docx/reconciler/tables.rb +162 -0
  53. data/lib/uniword/docx/reconciler/theme.rb +141 -0
  54. data/lib/uniword/docx/reconciler.rb +44 -1069
  55. data/lib/uniword/docx.rb +3 -0
  56. data/lib/uniword/drawingml/color_scheme.rb +38 -5
  57. data/lib/uniword/hyperlink.rb +27 -26
  58. data/lib/uniword/infrastructure/zip_packager.rb +41 -38
  59. data/lib/uniword/lazy_loader.rb +36 -62
  60. data/lib/uniword/math.rb +3 -86
  61. data/lib/uniword/mhtml/document.rb +1 -3
  62. data/lib/uniword/model_attribute_access.rb +27 -0
  63. data/lib/uniword/ooxml/namespaces.rb +62 -2
  64. data/lib/uniword/ooxml/relationships/package_relationships.rb +7 -0
  65. data/lib/uniword/presentationml/slide_id.rb +0 -1
  66. data/lib/uniword/presentationml/slide_master_id.rb +0 -1
  67. data/lib/uniword/properties/bold.rb +0 -2
  68. data/lib/uniword/properties/boolean_formatting.rb +0 -2
  69. data/lib/uniword/properties/borders.rb +1 -1
  70. data/lib/uniword/properties/italic.rb +0 -2
  71. data/lib/uniword/properties/numbering_properties.rb +1 -1
  72. data/lib/uniword/properties/style_reference.rb +13 -0
  73. data/lib/uniword/quality/rules/heading_hierarchy_rule.rb +1 -1
  74. data/lib/uniword/quality/rules/style_consistency_rule.rb +1 -1
  75. data/lib/uniword/review/review_manager.rb +0 -2
  76. data/lib/uniword/template/helpers/loop_helper.rb +2 -2
  77. data/lib/uniword/template/variable_resolver.rb +14 -1
  78. data/lib/uniword/theme/theme_xml_parser.rb +1 -1
  79. data/lib/uniword/toc/toc_generator.rb +3 -14
  80. data/lib/uniword/transformation/mhtml_element_renderer.rb +94 -37
  81. data/lib/uniword/transformation/mhtml_metadata_builder.rb +1 -1
  82. data/lib/uniword/transformation/mhtml_style_builder.rb +9 -2
  83. data/lib/uniword/transformation/ooxml_to_html_converter.rb +3 -5
  84. data/lib/uniword/transformation/ooxml_to_mhtml_converter.rb +7 -15
  85. data/lib/uniword/transformation/yaml_css_generator.rb +815 -0
  86. data/lib/uniword/transformation.rb +2 -0
  87. data/lib/uniword/validation/checkers/internal_link_checker.rb +2 -3
  88. data/lib/uniword/version.rb +1 -1
  89. data/lib/uniword/visitor/text_extractor.rb +2 -1
  90. data/lib/uniword/watermark/manager.rb +2 -2
  91. data/lib/uniword/wordprocessingml/allow_png.rb +14 -0
  92. data/lib/uniword/wordprocessingml/body.rb +53 -18
  93. data/lib/uniword/wordprocessingml/body_div.rb +17 -0
  94. data/lib/uniword/wordprocessingml/div_border.rb +23 -0
  95. data/lib/uniword/wordprocessingml/div_borders.rb +24 -0
  96. data/lib/uniword/wordprocessingml/divs_child.rb +17 -0
  97. data/lib/uniword/wordprocessingml/document_root.rb +6 -3
  98. data/lib/uniword/wordprocessingml/endnote.rb +1 -1
  99. data/lib/uniword/wordprocessingml/endnotes.rb +4 -0
  100. data/lib/uniword/wordprocessingml/footer.rb +4 -0
  101. data/lib/uniword/wordprocessingml/footer_reference.rb +0 -1
  102. data/lib/uniword/wordprocessingml/footnote.rb +1 -1
  103. data/lib/uniword/wordprocessingml/footnotes.rb +4 -0
  104. data/lib/uniword/wordprocessingml/header.rb +4 -0
  105. data/lib/uniword/wordprocessingml/header_reference.rb +0 -1
  106. data/lib/uniword/wordprocessingml/hyperlink.rb +2 -2
  107. data/lib/uniword/wordprocessingml/instr_text.rb +20 -5
  108. data/lib/uniword/wordprocessingml/level.rb +2 -0
  109. data/lib/uniword/wordprocessingml/mar_bottom.rb +17 -0
  110. data/lib/uniword/wordprocessingml/mar_left.rb +17 -0
  111. data/lib/uniword/wordprocessingml/mar_right.rb +17 -0
  112. data/lib/uniword/wordprocessingml/mar_top.rb +17 -0
  113. data/lib/uniword/wordprocessingml/num.rb +4 -0
  114. data/lib/uniword/wordprocessingml/numbering.rb +1 -22
  115. data/lib/uniword/wordprocessingml/optimize_for_browser.rb +14 -0
  116. data/lib/uniword/wordprocessingml/page_defaults.rb +39 -0
  117. data/lib/uniword/wordprocessingml/paragraph.rb +16 -13
  118. data/lib/uniword/wordprocessingml/paragraph_properties.rb +12 -8
  119. data/lib/uniword/wordprocessingml/run.rb +32 -36
  120. data/lib/uniword/wordprocessingml/run_properties/merging.rb +45 -0
  121. data/lib/uniword/wordprocessingml/run_properties.rb +8 -5
  122. data/lib/uniword/wordprocessingml/section_properties.rb +3 -3
  123. data/lib/uniword/wordprocessingml/separator_char.rb +21 -0
  124. data/lib/uniword/wordprocessingml/settings.rb +19 -17
  125. data/lib/uniword/wordprocessingml/styles/style_definition.rb +1 -1
  126. data/lib/uniword/wordprocessingml/styles/style_library.rb +16 -0
  127. data/lib/uniword/wordprocessingml/table_defaults.rb +44 -0
  128. data/lib/uniword/wordprocessingml/text.rb +8 -2
  129. data/lib/uniword/wordprocessingml/web_div.rb +41 -0
  130. data/lib/uniword/wordprocessingml/web_divs.rb +18 -0
  131. data/lib/uniword/wordprocessingml/web_encoding.rb +17 -0
  132. data/lib/uniword/wordprocessingml/web_settings.rb +5 -21
  133. data/lib/uniword/wordprocessingml/zoom.rb +2 -0
  134. data/lib/uniword/wordprocessingml.rb +23 -0
  135. data/lib/uniword.rb +27 -0
  136. metadata +47 -70
  137. data/lib/uniword/math/accent.rb +0 -25
  138. data/lib/uniword/math/accent_properties.rb +0 -25
  139. data/lib/uniword/math/argument_properties.rb +0 -22
  140. data/lib/uniword/math/bar.rb +0 -25
  141. data/lib/uniword/math/bar_properties.rb +0 -25
  142. data/lib/uniword/math/begin_char.rb +0 -22
  143. data/lib/uniword/math/border_box.rb +0 -25
  144. data/lib/uniword/math/border_box_properties.rb +0 -39
  145. data/lib/uniword/math/box.rb +0 -25
  146. data/lib/uniword/math/box_properties.rb +0 -33
  147. data/lib/uniword/math/char.rb +0 -22
  148. data/lib/uniword/math/control_properties.rb +0 -27
  149. data/lib/uniword/math/degree.rb +0 -26
  150. data/lib/uniword/math/delimiter.rb +0 -25
  151. data/lib/uniword/math/delimiter_properties.rb +0 -33
  152. data/lib/uniword/math/denominator.rb +0 -26
  153. data/lib/uniword/math/element.rb +0 -61
  154. data/lib/uniword/math/end_char.rb +0 -22
  155. data/lib/uniword/math/equation_array.rb +0 -25
  156. data/lib/uniword/math/equation_array_properties.rb +0 -33
  157. data/lib/uniword/math/fraction.rb +0 -27
  158. data/lib/uniword/math/fraction_properties.rb +0 -25
  159. data/lib/uniword/math/function.rb +0 -27
  160. data/lib/uniword/math/function_name.rb +0 -26
  161. data/lib/uniword/math/function_properties.rb +0 -23
  162. data/lib/uniword/math/group_char.rb +0 -25
  163. data/lib/uniword/math/group_char_properties.rb +0 -29
  164. data/lib/uniword/math/lim.rb +0 -26
  165. data/lib/uniword/math/lower_limit.rb +0 -27
  166. data/lib/uniword/math/lower_limit_properties.rb +0 -23
  167. data/lib/uniword/math/math_break.rb +0 -22
  168. data/lib/uniword/math/math_font.rb +0 -22
  169. data/lib/uniword/math/math_properties.rb +0 -53
  170. data/lib/uniword/math/math_run.rb +0 -42
  171. data/lib/uniword/math/math_run_properties.rb +0 -37
  172. data/lib/uniword/math/math_simple_int_val.rb +0 -24
  173. data/lib/uniword/math/math_simple_val.rb +0 -24
  174. data/lib/uniword/math/math_style.rb +0 -22
  175. data/lib/uniword/math/math_text.rb +0 -23
  176. data/lib/uniword/math/matrix.rb +0 -25
  177. data/lib/uniword/math/matrix_column.rb +0 -23
  178. data/lib/uniword/math/matrix_column_properties.rb +0 -24
  179. data/lib/uniword/math/matrix_columns.rb +0 -23
  180. data/lib/uniword/math/matrix_properties.rb +0 -37
  181. data/lib/uniword/math/matrix_row.rb +0 -23
  182. data/lib/uniword/math/nary.rb +0 -29
  183. data/lib/uniword/math/nary_properties.rb +0 -33
  184. data/lib/uniword/math/numerator.rb +0 -26
  185. data/lib/uniword/math/o_math.rb +0 -65
  186. data/lib/uniword/math/o_math_para.rb +0 -25
  187. data/lib/uniword/math/o_math_para_properties.rb +0 -22
  188. data/lib/uniword/math/phantom.rb +0 -25
  189. data/lib/uniword/math/phantom_properties.rb +0 -33
  190. data/lib/uniword/math/pre_sub_superscript.rb +0 -29
  191. data/lib/uniword/math/pre_sub_superscript_properties.rb +0 -23
  192. data/lib/uniword/math/radical.rb +0 -27
  193. data/lib/uniword/math/radical_properties.rb +0 -25
  194. data/lib/uniword/math/separator_char.rb +0 -22
  195. data/lib/uniword/math/sub.rb +0 -26
  196. data/lib/uniword/math/sub_superscript.rb +0 -29
  197. data/lib/uniword/math/sub_superscript_properties.rb +0 -25
  198. data/lib/uniword/math/subscript.rb +0 -27
  199. data/lib/uniword/math/subscript_properties.rb +0 -23
  200. data/lib/uniword/math/sup.rb +0 -26
  201. data/lib/uniword/math/superscript.rb +0 -27
  202. data/lib/uniword/math/superscript_properties.rb +0 -23
  203. data/lib/uniword/math/upper_limit.rb +0 -27
  204. 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: [
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
- require_relative "../properties/relationship_id"
5
4
 
6
5
  module Uniword
7
6
  module Presentationml
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
- require_relative "../properties/relationship_id"
5
4
 
6
5
  module Uniword
7
6
  module Presentationml
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "boolean_element_factory"
4
-
5
3
  module Uniword
6
4
  module Properties
7
5
  BooleanElementFactory.define("b", "Bold")
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "boolean_element_factory"
4
-
5
3
  module Uniword
6
4
  module Properties
7
5
  # Run-level boolean formatting elements
@@ -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
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "boolean_element_factory"
4
-
5
3
  module Uniword
6
4
  module Properties
7
5
  BooleanElementFactory.define("i", "Italic")
@@ -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.properties&.style
75
+ style_name = paragraph.style
76
76
  return nil unless style_name
77
77
 
78
78
  match = style_name.to_s.match(HEADING_PATTERN)
@@ -76,7 +76,7 @@ module Uniword
76
76
  # @param paragraph [Paragraph] The paragraph to check
77
77
  # @return [Boolean] true if has style
78
78
  def has_style?(paragraph)
79
- style = paragraph.properties&.style
79
+ style = paragraph.style
80
80
  style&.value && !style.value.empty?
81
81
  end
82
82
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "accept_reject"
4
-
5
3
  module Uniword
6
4
  module Review
7
5
  # Orchestrator for comments and tracked changes in a document.
@@ -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.text
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.text = text.gsub("{{#{var_name}}}", value.to_s)
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
- left_num.public_send(operator, right_num)
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.public_send(:"#{attr_name}=", color_obj)
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
- style_ref = paragraph.properties&.style
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 = Uniword::Properties::StyleReference.new(
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 = Uniword::Properties::StyleReference.new(
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
- return break_to_html(run.break) if run.break
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") && run.text.to_s.strip.empty?
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.text.to_s
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 Break to HTML
146
- def break_to_html(brk)
147
- if brk.type == "page"
148
- %(<br clear="all" style="mso-special-character:line-break;page-break-before:always" />)
149
- else
150
- %(<br />)
151
- end
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
- # Get paragraph content including runs, hyperlinks, SDTs, and oMath
495
- # Interleaves all element types in correct order using position markers.
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.o_math_paras.each do |omp|
533
- # Block math: render each oMath inside the oMathPara
534
- omp.o_maths.each do |om|
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
@@ -184,7 +184,7 @@ module Uniword
184
184
  para_chars = 0
185
185
 
186
186
  element.runs.each do |run|
187
- text = run.text.to_s
187
+ text = run.text_string
188
188
  characters += text.length
189
189
  para_chars += text.length
190
190
  words += text.split.length
@@ -697,6 +697,12 @@ module Uniword
697
697
  </style>
698
698
  LSB_STYLE
699
699
 
700
+ class << self
701
+ # Custom CSS override set by callers (e.g., ISO adapter).
702
+ # When set, #style_block returns this instead of the default.
703
+ attr_accessor :custom_style_block
704
+ end
705
+
700
706
  # Build the w:LatentStyles block.
701
707
  # @return [String] Static latent styles XML
702
708
  def self.latent_styles
@@ -704,9 +710,10 @@ module Uniword
704
710
  end
705
711
 
706
712
  # Build the CSS style block.
707
- # @return [String] Static CSS style block
713
+ # Returns custom CSS if set via custom_style_block=, otherwise default.
714
+ # @return [String] CSS style block
708
715
  def self.style_block
709
- STYLE_BLOCK
716
+ custom_style_block || STYLE_BLOCK
710
717
  end
711
718
  end
712
719
  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.text || "")
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
- return "" unless paragraph.properties
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
@@ -18,7 +18,8 @@ module Uniword
18
18
  # output = Uniword::Infrastructure::MimePackager.new(mhtml_doc).build_mime_content
19
19
  #
20
20
  class OoxmlToMhtmlConverter
21
- # Static MsoNormalTable CSS (used in wrap_html_document head)
21
+ # Static MsoNormalTable CSS (used in wrap_html_document head).
22
+ # Only used when MhtmlStyleBuilder does not provide custom CSS.
22
23
  MSO_NORMAL_TABLE_STYLE = <<~CSS
23
24
  <!--[if gte mso 10]>
24
25
  <style>
@@ -30,21 +31,12 @@ module Uniword
30
31
  mso-style-noshow:yes;
31
32
  mso-style-priority:99;
32
33
  mso-style-parent:"";
33
- mso-padding-alt:0in 5.4pt 0in 5.4pt;
34
- mso-para-margin-top:0in;
35
- mso-para-margin-right:0in;
36
- mso-para-margin-bottom:8.0pt;
37
- mso-para-margin-left:0in;
38
- line-height:115%;
34
+ mso-padding-alt:0cm 5.4pt 0cm 5.4pt;
35
+ mso-para-margin:0cm;
36
+ mso-para-margin-bottom:.0001pt;
39
37
  mso-pagination:widow-orphan;
40
- font-size:12.0pt;
41
- font-family:"Aptos",sans-serif;
42
- mso-ascii-font-family:Aptos;
43
- mso-ascii-theme-font:minor-latin;
44
- mso-hansi-font-family:Aptos;
45
- mso-hansi-theme-font:minor-latin;
46
- mso-font-kerning:1.0pt;
47
- mso-ligatures:standardcontextual;}
38
+ font-size:10pt;
39
+ font-family:"Cambria",serif;}
48
40
  </style>
49
41
  <![endif]-->
50
42
  CSS