Class: Uniword::Math::PlurimathAdapter

Inherits:
Object
  • Object
show all
Defined in:
lib/uniword/math/plurimath_adapter.rb

Overview

Adapter for Plurimath library integration Responsibility: Convert between OMML and Plurimath formula objects

This adapter provides bidirectional conversion between:

  • OMML (Office Math Markup Language) used in OOXML

  • Plurimath’s internal formula representation

The adapter handles:

  • Parsing OMML XML to Plurimath formulas

  • Serializing Plurimath formulas to OMML XML

  • Converting between display types (inline vs block)

  • Preserving equation properties and formatting

v4.0.0: Initial implementation for math equation support

Examples:

Convert OMML to Plurimath

omml = '<m:oMath>...</m:oMath>'
equation = PlurimathAdapter.from_omml(omml)
puts equation.to_latex

Convert Plurimath to OMML

equation = MathEquation.new(formula: formula)
omml = PlurimathAdapter.to_omml(equation)

See Also:

Constant Summary collapse

OMML_NAMESPACE =

OMML namespace URI

"http://schemas.openxmlformats.org/officeDocument/2006/math"

Class Method Summary collapse

Class Method Details

.determine_display_type(node) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Determine if equation is inline or block display

Parameters:

  • node (Nokogiri::XML::Node)

    OMML node

Returns:

  • (String)

    “inline” or “block”



138
139
140
141
142
# File 'lib/uniword/math/plurimath_adapter.rb', line 138

def self.determine_display_type(node)
  # m:oMathPara indicates block/display mode
  # m:oMath alone indicates inline mode
  node.name == "oMathPara" ? "block" : "inline"
end

.extract_math_node(node) ⇒ Nokogiri::XML::Node

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Extract the math node (m:oMath) from container

Parameters:

  • node (Nokogiri::XML::Node)

    OMML node (could be oMath or oMathPara)

Returns:

  • (Nokogiri::XML::Node)

    The m:oMath node



280
281
282
283
284
285
286
287
288
289
290
# File 'lib/uniword/math/plurimath_adapter.rb', line 280

def self.extract_math_node(node)
  if node.name == "oMath"
    node
  elsif node.name == "oMathPara"
    # Extract m:oMath from m:oMathPara
    node.at_xpath(".//m:oMath", "m" => OMML_NAMESPACE) || node
  else
    # Try to find m:oMath descendant
    node.at_xpath(".//m:oMath", "m" => OMML_NAMESPACE) || node
  end
end

.extract_properties(node) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Extract equation properties from OMML

Parameters:

  • node (Nokogiri::XML::Node)

    OMML node

Returns:

  • (Hash)

    Extracted properties



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/uniword/math/plurimath_adapter.rb', line 150

def self.extract_properties(node)
  properties = {}

  # Check for paragraph properties if this is oMathPara
  if node.name == "oMathPara"
    para_props = node.at_xpath(".//m:oMathParaPr", "m" => OMML_NAMESPACE)

    if para_props
      # Extract alignment (m:jc)
      jc_node = para_props.at_xpath(".//m:jc", "m" => OMML_NAMESPACE)
      if jc_node
        jc_val = jc_node.attr("m:val")
        properties[:alignment] = normalize_alignment(jc_val)
      end
    end
  end

  # Check for break settings (m:brk)
  brk_node = node.at_xpath(".//m:brk", "m" => OMML_NAMESPACE)
  properties[:break_enabled] = !brk_node.nil?

  properties
end

.format_omml(omml, options = {}) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Format OMML output

Parameters:

  • omml (String)

    OMML XML

  • options (Hash) (defaults to: {})

    Format options

Options Hash (options):

  • :pretty (Boolean)

    Pretty print

Returns:

  • (String)

    Formatted OMML



248
249
250
251
252
253
# File 'lib/uniword/math/plurimath_adapter.rb', line 248

def self.format_omml(omml, options = {})
  return omml unless options[:pretty]

  doc = Nokogiri::XML(omml)
  doc.to_xml(indent: 2)
end

.formula_to_omml(formula) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Convert Plurimath formula to OMML

Parameters:

  • formula (Plurimath::Math::Formula)

    The formula to convert

Returns:

  • (String)

    OMML XML content



123
124
125
126
127
128
129
130
# File 'lib/uniword/math/plurimath_adapter.rb', line 123

def self.formula_to_omml(formula)
  # Use Plurimath's OMML serializer
  formula.to_omml
rescue StandardError => e
  Uniword::Logger.warn("Failed to convert formula to OMML: #{e.message}")
  # Return minimal valid OMML
  "<m:oMath xmlns:m=\"#{OMML_NAMESPACE}\"/>"
end

.from_omml(omml_xml) ⇒ MathEquation

Create a MathEquation from OMML XML

Examples:

Parse OMML

xml = '<m:oMath xmlns:m="..."><m:r><m:t>x</m:t></m:r></m:oMath>'
equation = PlurimathAdapter.from_omml(xml)

Parameters:

  • omml_xml (String, Nokogiri::XML::Node)

    OMML XML content

Returns:

  • (MathEquation)

    New equation instance with parsed formula

Raises:

  • (ArgumentError)

    if XML is invalid or cannot be parsed



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/uniword/math/plurimath_adapter.rb', line 45

def self.from_omml(omml_xml)
  require "plurimath"
  node = parse_xml_node(omml_xml)

  # Determine if this is an inline or block equation
  display_type = determine_display_type(node)

  # Extract equation properties
  properties = extract_properties(node)

  # Convert OMML to Plurimath formula using Plurimath's OMML parser
  formula = parse_omml_to_formula(node)

  # Create and return MathEquation
  MathEquation.new(
    formula: formula,
    display_type: display_type,
    alignment: properties[:alignment],
    break_enabled: properties[:break_enabled],
  )
end

.normalize_alignment(omml_alignment) ⇒ String?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Normalize OMML alignment value to symbol

Parameters:

  • omml_alignment (String)

    OMML alignment value

Returns:

  • (String, nil)

    Normalized alignment



180
181
182
183
184
185
186
# File 'lib/uniword/math/plurimath_adapter.rb', line 180

def self.normalize_alignment(omml_alignment)
  case omml_alignment&.downcase
  when "left" then "left"
  when "center" then "center"
  when "right" then "right"
  end
end

.parse_omml_to_formula(node) ⇒ Plurimath::Math::Formula

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parse OMML to Plurimath formula

Parameters:

  • node (Nokogiri::XML::Node)

    OMML node

Returns:

  • (Plurimath::Math::Formula)

    Parsed formula



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/uniword/math/plurimath_adapter.rb', line 101

def self.parse_omml_to_formula(node)
  # Extract the math content (m:oMath or m:oMathPara)
  math_node = extract_math_node(node)

  # Use Plurimath's OMML parser
  # Plurimath.parse expects OMML string
  omml_string = math_node.to_xml

  Plurimath::Math.parse(omml_string, :omml)
rescue StandardError => e
  # If Plurimath parsing fails, create an empty formula
  # This allows graceful degradation
  Uniword::Logger.warn("Failed to parse OMML: #{e.message}")
  Plurimath::Math::Formula.new([])
end

.parse_xml_node(xml) ⇒ Nokogiri::XML::Node

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parse XML node from string or node

Parameters:

  • xml (String, Nokogiri::XML::Node)

    XML input

Returns:

  • (Nokogiri::XML::Node)

    Parsed node



261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/uniword/math/plurimath_adapter.rb', line 261

def self.parse_xml_node(xml)
  case xml
  when Nokogiri::XML::Node
    xml
  when String
    doc = Nokogiri::XML(xml)
    doc.root || raise(ArgumentError, "Invalid XML: no root element")
  else
    raise ArgumentError,
          "Expected String or Nokogiri::XML::Node, got #{xml.class}"
  end
end

.to_omml(equation, options = {}) ⇒ String

Convert a MathEquation to OMML XML

Examples:

Convert to OMML

equation = MathEquation.new(formula: formula)
omml = PlurimathAdapter.to_omml(equation)

Parameters:

  • equation (MathEquation)

    The equation to convert

  • options (Hash) (defaults to: {})

    Serialization options

Options Hash (options):

  • :pretty (Boolean) — default: false

    Pretty print XML

Returns:

  • (String)

    OMML XML string

Raises:

  • (ArgumentError)

    if equation has no formula



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/uniword/math/plurimath_adapter.rb', line 79

def self.to_omml(equation, options = {})
  unless equation.formula
    raise ArgumentError,
          "Equation must have a formula"
  end

  # Convert Plurimath formula to OMML using Plurimath's OMML serializer
  omml_content = formula_to_omml(equation.formula)

  # Wrap in appropriate container based on display type
  wrapped = wrap_omml(omml_content, equation, options)

  # Format output
  format_omml(wrapped, options)
end

.wrap_block_equation(omml_content, equation) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Wrap block equation with paragraph properties

Parameters:

  • omml_content (String)

    OMML math content

  • equation (MathEquation)

    The equation

Returns:

  • (String)

    Wrapped OMML



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/uniword/math/plurimath_adapter.rb', line 222

def self.wrap_block_equation(omml_content, equation)
  builder = Nokogiri::XML::Builder.new do |xml|
    xml["m"].oMathPara("xmlns:m" => OMML_NAMESPACE) do
      # Add paragraph properties if needed
      if equation.alignment || equation.break_enabled
        xml["m"].oMathParaPr do
          xml["m"].jc("m:val" => equation.alignment) if equation.alignment
        end
      end

      # Add the math content
      xml.parent << Nokogiri::XML.fragment(omml_content)
    end
  end

  builder.to_xml
end

.wrap_inline_equation(omml_content) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Wrap inline equation

Parameters:

  • omml_content (String)

    OMML math content

Returns:

  • (String)

    Wrapped OMML



210
211
212
213
# File 'lib/uniword/math/plurimath_adapter.rb', line 210

def self.wrap_inline_equation(omml_content)
  # Inline equations are just m:oMath
  omml_content
end

.wrap_omml(omml_content, equation, _options = {}) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Wrap OMML content in appropriate container

Parameters:

  • omml_content (String)

    OMML math content

  • equation (MathEquation)

    The equation

  • options (Hash)

    Options

Returns:

  • (String)

    Wrapped OMML



196
197
198
199
200
201
202
# File 'lib/uniword/math/plurimath_adapter.rb', line 196

def self.wrap_omml(omml_content, equation, _options = {})
  if equation.block?
    wrap_block_equation(omml_content, equation)
  else
    wrap_inline_equation(omml_content)
  end
end