Project

mml

0.0
The project is in a healthy, maintained state
MathML parser and builder used in Plurimath.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 0.8.0
>= 0
 Project Readme

Mml: MathML parser and builder

Purpose

Mml provides MathML 3 and MathML 4 XML parsing and serialization for Ruby. It maps the full MathML element set into Ruby model classes using the lutaml-model framework and is used by Plurimath for mathematical formula representation.

Key features:

  • Dual MathML version support: Separate class hierarchies for MathML 3 and MathML 4 with explicit version selection

  • Round-trip fidelity: Parse XML to an object graph, modify, and serialize back

  • Namespace handling: Default xmlns, prefixed mml:, and namespace-less input

  • Opal support: Runs in the browser via Ruby-to-JavaScript compilation

Installation

gem 'mml'
$ bundle install
# or
$ gem install mml

Quick start

require "mml"

# Parse MathML (defaults to MathML 3)
math = Mml.parse('<math xmlns="http://www.w3.org/1998/Math/MathML"><mi>x</mi></math>')

# Serialize back to XML
math.to_xml
# => "<math xmlns=\"http://www.w3.org/1998/Math/MathML\"><mi>x</mi></math>"

# Parse with explicit version
math4 = Mml.parse(input, version: 4)

MathML version architecture

Mml maintains two parallel class hierarchies under Mml::V3 and Mml::V4. Both versions share the same namespace URI (http://www.w3.org/1998/Math/MathML) for backward compatibility.

                   ┌───────────────────────────────────────────┐
                   │                   Mml                     │
                   │  (aliases Mml::V3 for backward compat)    │
                   └────────────────────┬──────────────────────┘
                                        │
                    ┌───────────────────┴────────────────────┐
                    │                                        │
               ┌────┴────┐                              ┌────┴────┐
               │ Mml::V3 │                              │ Mml::V4 │
               └────┬────┘                              └────┬────┘
                    │                                        │
    ┌───────────────┼─────────────────────┐  ┌───────────────┼───────────────┐
    │  ┌────────────┴───────────────┐     │  │  ┌────────────┴────────────┐  │
    │  │ Element classes            │     │  │  │ Element classes         │  │
    │  │ (Mi, Mn, Mo, Mrow, ...)    │     │  │  │ (Mi, Mn, Mo, Mrow, ...) │  │
    │  │                            │     │  │  │ + intent, arg,          │  │
    │  │ Inherits from              │     │  │  │   displaystyle,         │  │
    │  │ Lutaml::Model::Serializable│     │  │  │   scriptlevel           │  │
    │  └────────────┬───────────────┘     │  │  │  + <a> element          │  │
    │               │                     │  │  └────────────┬────────────┘  │
    │  ┌────────────┴────────────┐        │  │  ┌────────────┴────────────┐  │
    │  │ CommonAttributes        │        │  │  │ CommonAttributes        │  │
    │  │ (child element mixin)   │        │  │  │ (v4 version)            │  │
    │  └─────────────────────────┘        │  |  └─────────────────────────┘  |
    └─────────────────────────────────────┘  └───────────────────────────────┘
                                           │
                        ┌──────────────────┴──────────────────────┐
                        │         Lutaml::Model::Serializable     │
                        │         (XML mapping framework)         │
                        └─────────────────────────────────────────┘

Version selection

Mml.parse(input)              # Default: MathML 3 (Mml::V3)
Mml.parse(input, version: 3)  # Explicit MathML 3
Mml.parse(input, version: 4)  # MathML 4 with intent/arg attributes
Mml::V4.parse(input)          # Direct v4 parsing (no default fallback)

Key differences between MathML 3 and MathML 4

Feature MathML 4 additions

Universal attributes

intent, arg, displaystyle, scriptlevel available on all presentation elements

New element

<a> hyperlink element with href, hreflang

Deprecated (not serialized)

  • fontfamily, fontweight, fontstyle, fontsize, color, background on mstyle, mglyph, mspace

  • groupalign on mtable, mtr, mlabeledtr

  • fence, separator on mo

Deprecated (recognized but hidden)

<mlabeledtr>, <none> removed from CommonAttributes but classes still exist

Migration from MathML 3 to MathML 4

For gem users (upgrading parsing)

If you currently parse MathML and want to use MathML 4 features:

# Change this:
math = Mml.parse(input)

# To this:
math = Mml.parse(input, version: 4)

For gem users (upgrading serialized output)

If you serialize MathML 4 documents and want to ensure compatibility:

# MathML 4 serialization removes deprecated attributes from XML output
math = Mml.parse(input, version: 4)
math.to_xml  # No fontfamily, fontweight, color, groupalign, etc. in output

For gem extenders (adding custom elements)

Custom elements registered via Configuration.custom_models are version-specific:

# Register a custom element for MathML 4 only
Mml::V4::Configuration.custom_models = { Mi => MyCustomMiV4 }

CommonAttributes is version-specific. Elements that exist only in v4 (like <a>) are automatically handled:

# The <a> element is only in v4 CommonAttributes
Mml::V4::Mrow.new(a_value: [Mml::V4::A.new(href: "https://...")])

Parsing and serialization

Parsing

# Default namespace
Mml.parse('<math xmlns="http://www.w3.org/1998/Math/MathML"><mi>x</mi></math>')

# Prefixed namespace
Mml.parse('<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML"><mml:mi>x</mml:mi></mml:math>')

# No namespace (namespace injected internally)
Mml.parse("<math><mi>x</mi></math>", namespace_exist: false)

Serialization

math.to_xml
# => "<math xmlns=\"http://www.w3.org/1998/Math/MathML\"><mi>x</mi></math>"

math.to_xml(prefix: true)
# => "<mml:math xmlns:mml=\"http://www.w3.org/1998/Math/MathML\"><mml:mi>x</mml:mi></mml:math>"

math.to_xml(declaration: false)
# => "<math xmlns=\"...\"><mi>x</mi></math>"

Round-trip (parse, modify, serialize)

math = Mml.parse('<math xmlns="http://www.w3.org/1998/Math/MathML"><mi>x</mi></math>')
math.display = "block"
math.to_xml
# => "<math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\"><mi>x</mi></math>"

Element reference

Element types

Token elements: mi, mn, mo, ms, mtext, mspace, mglyph

General layout: mrow, mfrac, msqrt, mroot, mstyle, merror, mpadded, mphantom, mfenced, menclose, maction

Script elements: msub, msup, msubsup, munder, mover, munderover, mmultiscripts, mprescripts

Table elements: mtable, mtr, mtd

Row and stack elements: mstack, msrow, mscarries, mscarry, msline, msgroup, mlongdiv

Semantic elements: mfraction, semantics

v4 only: a (hyperlink)

Deprecated: mlabeledtr, none (classes exist but hidden from CommonAttributes in v4)

Token elements (leaf nodes)

Token elements hold text content in the value attribute:

Mml::Mi.new(value: "x")
Mml::Mn.new(value: "42")
Mml::Mo.new(value: "+")
Mml::Ms.new(value: "text")
Mml::Mtext.new(value: "label")
Mml::Mspace.new(width: "1em")
Mml::Mglyph.new(alt: "symbol")

Container elements

Container elements hold child elements via #{tag}_value collection attributes:

Mml::Mrow.new(
  mi_value: [Mml::Mi.new(value: "x")],
  mo_value: [Mml::Mo.new(value: "+")],
  mn_value: [Mml::Mn.new(value: "1")],
)
# => <mrow><mi>x</mi><mo>+</mo><mn>1</mn></mrow>

Composing expressions

Build an expression tree by nesting elements:

Mml::Math.new(
  mfrac_value: [
    Mml::Mfrac.new(
      mi_value: [Mml::Mi.new(value: "a"), Mml::Mi.new(value: "b")],
    ),
  ],
)
# => <math><mfrac><mi>a</mi><mi>b</mi></mfrac></math>

Tables

Mml::Mtable.new(
  mtr_value: [
    Mml::Mtr.new(
      mtd_value: [
        Mml::Mtd.new(mi_value: [Mml::Mi.new(value: "a")]),
        Mml::Mtd.new(mi_value: [Mml::Mi.new(value: "b")]),
      ],
    ),
  ],
)
Mml::V4::A.new(
  href: "https://example.com",
  hreflang: "en",
  mi_value: [Mml::V4::Mi.new(value: "click")]
)
# => <a href="https://example.com" hreflang="en"><mi>click</mi></a>

Internal architecture

Element class patterns

All element classes inherit from Lutaml::Model::Serializable:

  • Leaf elements: use map_content to: :value for text content

  • Container elements: use mixed_content for child elements

# Leaf — text content
class Mi < Lutaml::Model::Serializable
  attribute :value, :string
  xml do
    element "mi"
    map_content to: :value
  end
end

# Container — child elements
class Mrow < Lutaml::Model::Serializable
  xml do
    element "mrow"
    mixed_content
  end
end

CommonAttributes

Container elements that accept arbitrary MathML children import CommonAttributes, which defines #{tag}_value collection attributes for all supported child elements. The list of classes that receive this mixin is in Configuration::COMMON_ATTRIBUTES_CLASSES.

Namespace

All elements use the MathML namespace URI (http://www.w3.org/1998/Math/MathML). Three input forms are supported:

  • Default namespace: <math xmlns="http://www.w3.org/1998/Math/MathML">

  • Prefixed: <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML">

  • No namespace: namespace is injected before parsing when namespace_exist: false

Configuration

# Switch XML adapter (default: :ox, also supports :nokogiri)
Mml::Configuration.adapter = :nokogiri

# Register custom model replacements
Mml::Configuration.custom_models = { Mi => MyCustomMi }

Development

rake                    # Run specs + rubocop
bundle exec rspec       # Run tests
bundle exec rubocop     # Lint
bin/console             # Interactive IRB

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/plurimath/mml.

Copyright Ribose Inc.