A family covering common Office Math (OMML) constructs — fraction, radical, super/subscript, n-ary summation and integral, matrix, and a plain identifier run.
| Library | Pass | Fail | Pending |
|---|---|---|---|
python-docx | 0 | 0 | 8 |
docxjs | 0 | 0 | 8 |
equation (8 values): fraction, radical, superscript, subscript, summation, integral, matrix, identifierscripts/gen_math_equation.py — runs with --arg_template --shape {equation.id} --out fixtures/docx/math-equation--{equation.id}.docx
#!/usr/bin/env python3
"""Generate ``fixtures/docx/math-equation--<shape>.docx`` fixtures.
Each run builds one paragraph hosting one OMML construct. The ``--shape``
argument selects which OMML element family is emitted:
- ``fraction`` — ``m:f``
- ``radical`` — ``m:rad``
- ``superscript`` — ``m:sSup``
- ``subscript`` — ``m:sSub``
- ``summation`` — ``m:nary`` with ``m:chr m:val='∑'``
- ``integral`` — ``m:nary`` with ``m:chr m:val='∫'``
- ``matrix`` — ``m:m`` 2x2
- ``identifier`` — a plain ``m:r``/``m:t`` run of ``y``
The first five go through the fork's high-level builder functions from
``docx.equations``. The summation, integral, and matrix shapes use
hand-authored OMML XML strings passed to ``Paragraph.add_equation`` —
the fork currently has no n-ary or matrix builder.
The manifest at ``features/docx/math-equation.json`` drives the ``--shape``
choice via its ``generator.arg_template`` at expansion time.
"""
from __future__ import annotations
import argparse
from pathlib import Path
from docx import Document
from docx.equations import (
build_fraction,
build_identifier,
build_radical,
build_subscript,
build_superscript,
)
_REPO_ROOT = Path(__file__).resolve().parent.parent
# -- OMML namespace URI for hand-authored OMML XML fragments. --
_M_NS = "http://schemas.openxmlformats.org/officeDocument/2006/math"
def _summation_omml() -> str:
"""Return an ``m:oMath`` expressing Σ from i=1 to n of i.
Uses ``m:nary`` with ``m:naryPr/m:chr m:val='∑'`` and limLoc=undOvr so
the sub/sup limits sit under and over the sigma.
"""
return (
'<m:oMath xmlns:m="%s">'
"<m:nary>"
'<m:naryPr><m:chr m:val="∑"/><m:limLoc m:val="undOvr"/></m:naryPr>'
"<m:sub><m:r><m:t>i=1</m:t></m:r></m:sub>"
"<m:sup><m:r><m:t>n</m:t></m:r></m:sup>"
"<m:e><m:r><m:t>i</m:t></m:r></m:e>"
"</m:nary>"
"</m:oMath>"
) % _M_NS
def _integral_omml() -> str:
"""Return an ``m:oMath`` expressing ∫ from 0 to 1 of x.
Uses ``m:nary`` with ``m:chr m:val='∫'``. Default limLoc (subSup)
places limits to the right of the sign, matching Word's behaviour
for integrals.
"""
return (
'<m:oMath xmlns:m="%s">'
"<m:nary>"
'<m:naryPr><m:chr m:val="∫"/></m:naryPr>'
"<m:sub><m:r><m:t>0</m:t></m:r></m:sub>"
"<m:sup><m:r><m:t>1</m:t></m:r></m:sup>"
"<m:e><m:r><m:t>x</m:t></m:r></m:e>"
"</m:nary>"
"</m:oMath>"
) % _M_NS
def _matrix_omml() -> str:
"""Return an ``m:oMath`` expressing a 2x2 matrix of a,b / c,d.
Uses ``m:m`` with one ``m:mcs`` carrying a single ``m:mc`` that sets
column count to 2 and center justification, then two ``m:mr`` rows of
two ``m:e`` cells each.
"""
return (
'<m:oMath xmlns:m="%s">'
"<m:m>"
"<m:mPr><m:mcs><m:mc><m:mcPr>"
'<m:count m:val="2"/><m:mcJc m:val="center"/>'
"</m:mcPr></m:mc></m:mcs></m:mPr>"
"<m:mr>"
"<m:e><m:r><m:t>a</m:t></m:r></m:e>"
"<m:e><m:r><m:t>b</m:t></m:r></m:e>"
"</m:mr>"
"<m:mr>"
"<m:e><m:r><m:t>c</m:t></m:r></m:e>"
"<m:e><m:r><m:t>d</m:t></m:r></m:e>"
"</m:mr>"
"</m:m>"
"</m:oMath>"
) % _M_NS
_SHAPES: dict[str, callable] = { # type: ignore[type-arg]
"fraction": lambda: build_fraction("a", "b"),
"radical": lambda: build_radical("x", degree_text="2"),
"superscript": lambda: build_superscript("a", "2"),
"subscript": lambda: build_subscript("x", "1"),
"summation": _summation_omml,
"integral": _integral_omml,
"matrix": _matrix_omml,
"identifier": lambda: build_identifier("y"),
}
def _write(shape: str, out: Path) -> None:
if shape not in _SHAPES:
raise SystemExit(
"Unknown shape %r; expected one of %s"
% (shape, ", ".join(sorted(_SHAPES)))
)
doc = Document()
p = doc.add_paragraph()
p.add_equation(_SHAPES[shape]())
out.parent.mkdir(parents=True, exist_ok=True)
doc.save(out, reproducible=True)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--shape",
default="fraction",
choices=sorted(_SHAPES),
help="Which OMML construct to emit.",
)
parser.add_argument(
"--out",
default=str(_REPO_ROOT / "fixtures" / "docx" / "math-equation.docx"),
)
args = parser.parse_args()
out_path = Path(args.out)
if not out_path.is_absolute():
out_path = _REPO_ROOT / out_path
_write(args.shape, out_path)
print(out_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/math-equation",
"kind": "parameterised",
"title": "Math equation (OMML)",
"format": "docx",
"category": "math",
"summary": "A family covering common Office Math (OMML) constructs — fraction, radical, super/subscript, n-ary summation and integral, matrix, and a plain identifier run.",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "22",
"element": "m:oMath",
"rnc_reference": "spec/ecma-376-5/part-1/rnc/shared-math.rnc",
"xsd_reference": "spec/ecma-376-5/part-1/xsd/shared-math.xsd",
"notes": "Each expanded case writes one paragraph containing an inline <m:oMath> with a distinct OMML shape: fraction (m:f), radical (m:rad), superscript (m:sSup), subscript (m:sSub), summation (m:nary with m:chr val=U+2211), integral (m:nary with m:chr val=U+222B), matrix (m:m with two rows of two cells), and a plain identifier (m:r/m:t). Eight cases total. The fork's python-docx equation builders (build_fraction, build_radical, build_superscript, build_subscript, build_identifier) cover five of them; the summation / integral / matrix shapes go through Paragraph.add_equation with hand-written OMML XML since the fork has no high-level n-ary / matrix builder. The summation/integral shape_xpath uses m:naryPr/m:chr/@m:val to distinguish the two nary glyphs. All assertions run against word/document.xml using the m namespace."
},
"fixtures": {
"machine": "docx/math-equation",
"office": "docx/math-equation"
},
"generator": {
"python": "scripts/gen_math_equation.py",
"arg_template": "--shape {equation.id} --out fixtures/docx/math-equation--{equation.id}.docx"
},
"parameters": {
"equation": [
{
"id": "fraction",
"shape_xpath": "//m:oMath//m:f"
},
{
"id": "radical",
"shape_xpath": "//m:oMath//m:rad"
},
{
"id": "superscript",
"shape_xpath": "//m:oMath//m:sSup"
},
{
"id": "subscript",
"shape_xpath": "//m:oMath//m:sSub"
},
{
"id": "summation",
"shape_xpath": "//m:oMath//m:nary[m:naryPr/m:chr[@m:val='∑']]"
},
{
"id": "integral",
"shape_xpath": "//m:oMath//m:nary[m:naryPr/m:chr[@m:val='∫']]"
},
{
"id": "matrix",
"shape_xpath": "//m:oMath//m:m"
},
{
"id": "identifier",
"shape_xpath": "//m:oMath/m:r/m:t[normalize-space()='y']"
}
]
},
"assertions_template": [
{
"id": "omath-present-{equation.id}",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
"m": "http://schemas.openxmlformats.org/officeDocument/2006/math"
},
"xpath": "//m:oMath",
"must": "exist",
"description": "At least one <m:oMath> element must be present in the body. Every OMML construct is hosted inside an <m:oMath> (inline) or <m:oMathPara> (display)."
},
{
"id": "omml-shape-{equation.id}",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
"m": "http://schemas.openxmlformats.org/officeDocument/2006/math"
},
"xpath": "{equation.shape_xpath}",
"must": "exist",
"description": "The OMML expression must contain the shape specific to this case (fraction / radical / super / sub / summation / integral / matrix / identifier)."
}
]
}