Skip to content

fix: align element content models with W3C schema - #33

Merged
ronaldtse merged 5 commits into
mainfrom
fix/element-content-model-schema-compliance
Jul 14, 2026
Merged

fix: align element content models with W3C schema#33
ronaldtse merged 5 commits into
mainfrom
fix/element-content-model-schema-compliance

Conversation

@ronaldtse

Copy link
Copy Markdown
Contributor

Summary

Aligns the gem's element content models with the W3C MathML 3 (XSD) and MathML 4 (RelaxNG) schemas. Three independent correctness fixes, plus regression specs and CLAUDE.md documentation.

1. Element-only containers migrated from mixed_content to ordered

31 base files were declared mixed_content even though their schema content models are element-only (ImpliedMrow, MathExpression, MathExpression, MstackExpression*, TableRowExpression*, etc.). Under mixed_content, lutaml-model's each_mixed_content iteration yields inter-element whitespace as String children, which shifts positional indices in consumers like Plurimath (msub_to_base reads children[0] as base, children[1] as subscript — a phantom whitespace String at position 0 silently corrupts the tree).

Under ordered, iteration order is preserved but whitespace-only text nodes are skipped.

Files: 27 originally flagged by the audit + 4 additional (munder, mrow, mfenced, msgroup). The four extras also drop the stray content (or msgroup_text) attribute and map_content declaration — the schema forbids text content there.

2. Token elements capture inline mglyph and malignmark

Per MathML schema, token elements (mi, mn, mo, ms, mtext) accept (#PCDATA | mglyph | malignmark)*. The gem's mi/mn already mapped mglyph; malignmark was missing everywhere, and mo/mtext/ms were missing one or both inline element mappings.

All five token elements now consistently declare mglyph_value and malignmark_value collections and map them via map_element. mo and mtext also gain mixed_content (required for inline-element interleaving).

3. Mo#value and Mtext#value are now String collections

Required by mixed_content (lutaml-model enforces this). Matches Mi, Mn, Ms. Text split by an inline element is captured as multiple entries: <mi>x<malignmark/>y</mi> yields value == ["x", "y"]. Plurimath's text_value helper already handles both String and Array inputs.

Schema verification

Each modified element was checked against the authoritative schemas in this repo:

grep -A15 'name="<tag>"' schemas/mathml3/mathml3-presentation.xsd schemas/mathml3/mathml3-common.xsd
grep -A3 '<tag>\s*=\s*element' schemas/mathml4/mathml4-core.rnc schemas/mathml4/mathml4-presentation.rnc

Test plan

  • bundle exec rspec — 2926 examples, 0 failures, 36 pre-existing pending
  • bundle exec rubocop — 493 files, no offenses
  • New spec/mml/ordered_content_spec.rb (54 specs across V3/V4) — covers positional-index whitespace handling for 11 element types + inline mglyph/malignmark round-trip for all 5 token elements
  • Existing spec assertions updated to expect Array values from Mo#value and Mtext#value

Out of scope

  • The lazy-registration architecture (autoload + bottom-of-file ensure_registered!) is in a separate open PR (feat: Opal boot file + lazy registration for v2/v3/v4 #31). This PR is based on main and does not touch that infrastructure.
  • V3 centralized register_models! block (MECE consistency with V2/V4) — same reason; belongs with PR feat: Opal boot file + lazy registration for v2/v3/v4 #31.
  • Upstream lutaml-model PR for string-type resolution — closed as rejected after the lutaml-model team clarified that the bottom-of-file ensure_registered! already exploits Type.const_get's segment-by-segment resolution to register siblings.

Breaking changes

  • Mrow#content, Munder#content, Mfenced#content, Msgroup#msgroup_text — removed (element-only schemas; no consumer in Plurimath uses them).
  • Mo#value, Mtext#value — return Array<String> instead of String. Consumers that treat these as String will need to join (Plurimath already does via text_value).

…d_content)

31 element-only base files were declared as mixed_content even though the MathML 3 XSD and MathML 4 RelaxNG define them as element-only content models (ImpliedMrow, MathExpression sequences, MstackExpression*, TableRowExpression*, etc.). Under mixed_content, lutaml-model's each_mixed_content iteration yields inter-element whitespace as String children, which shifts positional indices in consumers like Plurimath (msub_to_base reads children[0] as base, children[1] as subscript).

Switching to ordered preserves iteration order but skips whitespace-only text nodes. For mrow, munder, mfenced, msgroup the stray content (or msgroup_text) attribute and map_content declaration are also removed: the schema does not permit text content in these elements.

Schema verification: schemas/mathml3/mathml3-presentation.xsd, schemas/mathml4/mathml4-core.rnc, schemas/mathml4/mathml4-presentation.rnc.
…chema

Token elements (mi, mn, mo, ms, mtext) accept (#PCDATA | mglyph | malignmark)* per the MathML schema. The gem's mi and mn already mapped mglyph; malignmark was missing everywhere, and mo/mtext/ms were missing one or both inline element mappings.

All five token elements now consistently declare mglyph_value and malignmark_value collections and map them via map_element. mo and mtext also gain mixed_content (required for the inline-element interleaving use case).

Because mixed_content requires the content attribute to be a collection, Mo#value and Mtext#value are now String collections (matching Mi, Mn, Ms). Text split by an inline element is captured as multiple entries, e.g. <mi>x<malignmark/>y</mi> yields value == ['x', 'y']. Plurimath's text_value helper handles both String and Array inputs.
Follows the token-element schema fix: Mo#value and Mtext#value are now String collections (matching Mi, Mn, Ms). Spec assertions across v2/v3/v4 element specs, adapter_configuration_spec, and context_support_spec are updated to expect Array values where they previously expected String.
Locks in the mixed_content vs ordered distinction so future drift is caught immediately. The existing suite uses canon's spec_friendly profile which is whitespace-tolerant, so positional-index bugs are invisible without explicit assertions.

Covers three scenarios across Mml::V3 and Mml::V4:

1. Positional elements (msub, msup, msubsup, mfrac, mroot, mover, munder, munderover) with whitespace between children: each_mixed_content yields no whitespace-only Strings, round-trip preserves children.

2. Container elements (mrow, mfenced, msgroup) with whitespace between children: same invariants.

3. Token elements (mi, mn, mo, ms, mtext) with inline mglyph or malignmark: the inline element is captured alongside text, value is split into a collection when interrupted.
Document the mixed_content vs ordered vs map_content decision based on W3C schema authority, with verification workflow pointing at schemas/mathml3 and schemas/mathml4. Document token element pattern (mglyph + malignmark inline children, value as collection). Reference spec/mml/ordered_content_spec.rb as the regression test. List reference-docs and schemas as authoritative sources.
@ronaldtse
ronaldtse merged commit a9e9e0f into main Jul 14, 2026
14 checks passed
@ronaldtse
ronaldtse deleted the fix/element-content-model-schema-compliance branch July 14, 2026 10:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant