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, prefixedmml:, and namespace-less input -
Opal support: Runs in the browser via Ruby-to-JavaScript compilation
Installation
gem 'mml'$ bundle install
# or
$ gem install mmlQuick 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 |
|
New element |
|
Deprecated (not serialized) |
|
Deprecated (recognized but hidden) |
|
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 outputFor 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")]),
],
),
],
)Hyperlinks (MathML 4 only)
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: :valuefor text content -
Container elements: use
mixed_contentfor 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
endCommonAttributes
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 IRBContributing
Bug reports and pull requests are welcome on GitHub at https://github.com/plurimath/mml.
Copyright and license
Copyright Ribose Inc.