mml 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +14 -4
  3. data/CLAUDE.md +79 -0
  4. data/README.adoc +166 -43
  5. data/lib/mml/base/deprecated_font_attributes.rb +31 -0
  6. data/lib/mml/base/maction.rb +28 -0
  7. data/lib/mml/base/maligngroup.rb +26 -0
  8. data/lib/mml/base/malignmark.rb +26 -0
  9. data/lib/mml/base/math.rb +23 -0
  10. data/lib/mml/base/menclose.rb +27 -0
  11. data/lib/mml/base/merror.rb +25 -0
  12. data/lib/mml/base/mfenced.rb +34 -0
  13. data/lib/mml/base/mfrac.rb +33 -0
  14. data/lib/mml/base/mfraction.rb +33 -0
  15. data/lib/mml/base/mglyph.rb +42 -0
  16. data/lib/mml/base/mi.rb +32 -0
  17. data/lib/mml/base/mlabeledtr.rb +37 -0
  18. data/lib/mml/base/mlongdiv.rb +31 -0
  19. data/lib/mml/base/mmultiscripts.rb +31 -0
  20. data/lib/mml/base/mn.rb +32 -0
  21. data/lib/mml/base/mo.rb +76 -0
  22. data/lib/mml/base/mover.rb +29 -0
  23. data/lib/mml/base/mpadded.rb +35 -0
  24. data/lib/mml/base/mphantom.rb +25 -0
  25. data/lib/mml/base/mprescripts.rb +24 -0
  26. data/lib/mml/base/mroot.rb +25 -0
  27. data/lib/mml/base/mrow.rb +29 -0
  28. data/lib/mml/base/ms.rb +37 -0
  29. data/lib/mml/base/mscarries.rb +33 -0
  30. data/lib/mml/base/mscarry.rb +29 -0
  31. data/lib/mml/base/msgroup.rb +31 -0
  32. data/lib/mml/base/msline.rb +34 -0
  33. data/lib/mml/base/mspace.rb +52 -0
  34. data/lib/mml/base/msqrt.rb +25 -0
  35. data/lib/mml/base/msrow.rb +27 -0
  36. data/lib/mml/base/mstack.rb +33 -0
  37. data/lib/mml/base/mstyle.rb +189 -0
  38. data/lib/mml/base/msub.rb +27 -0
  39. data/lib/mml/base/msubsup.rb +29 -0
  40. data/lib/mml/base/msup.rb +27 -0
  41. data/lib/mml/base/mtable.rb +57 -0
  42. data/lib/mml/base/mtd.rb +29 -0
  43. data/lib/mml/base/mtext.rb +32 -0
  44. data/lib/mml/base/mtr.rb +35 -0
  45. data/lib/mml/base/munder.rb +31 -0
  46. data/lib/mml/base/munderover.rb +31 -0
  47. data/lib/mml/base/none.rb +24 -0
  48. data/lib/mml/base/semantics.rb +23 -0
  49. data/lib/mml/base/v3_only/operator_attrs.rb +24 -0
  50. data/lib/mml/base/v3_only/style_attrs.rb +31 -0
  51. data/lib/mml/base/v3_only/table_attrs.rb +28 -0
  52. data/lib/mml/base/v3_only.rb +11 -0
  53. data/lib/mml/base/v4_attributes.rb +30 -0
  54. data/lib/mml/base.rb +51 -0
  55. data/lib/mml/common_elements.rb +85 -0
  56. data/lib/mml/context_configuration.rb +118 -0
  57. data/lib/mml/context_options.rb +64 -0
  58. data/lib/mml/namespace.rb +10 -0
  59. data/lib/mml/v3/common_elements.rb +8 -0
  60. data/lib/mml/v3/configuration.rb +4 -96
  61. data/lib/mml/v3/maction.rb +2 -14
  62. data/lib/mml/v3/maligngroup.rb +2 -12
  63. data/lib/mml/v3/malignmark.rb +2 -12
  64. data/lib/mml/v3/math.rb +3 -10
  65. data/lib/mml/v3/menclose.rb +3 -14
  66. data/lib/mml/v3/merror.rb +3 -12
  67. data/lib/mml/v3/mfenced.rb +3 -21
  68. data/lib/mml/v3/mfrac.rb +3 -20
  69. data/lib/mml/v3/mfraction.rb +3 -20
  70. data/lib/mml/v3/mglyph.rb +3 -38
  71. data/lib/mml/v3/mi.rb +3 -30
  72. data/lib/mml/v3/mlabeledtr.rb +2 -23
  73. data/lib/mml/v3/mlongdiv.rb +3 -18
  74. data/lib/mml/v3/mmultiscripts.rb +3 -20
  75. data/lib/mml/v3/mn.rb +3 -30
  76. data/lib/mml/v3/mo.rb +4 -78
  77. data/lib/mml/v3/mover.rb +3 -16
  78. data/lib/mml/v3/mpadded.rb +3 -22
  79. data/lib/mml/v3/mphantom.rb +3 -12
  80. data/lib/mml/v3/mprescripts.rb +2 -10
  81. data/lib/mml/v3/mroot.rb +3 -12
  82. data/lib/mml/v3/mrow.rb +3 -18
  83. data/lib/mml/v3/ms.rb +4 -36
  84. data/lib/mml/v3/mscarries.rb +3 -20
  85. data/lib/mml/v3/mscarry.rb +3 -16
  86. data/lib/mml/v3/msgroup.rb +3 -18
  87. data/lib/mml/v3/msline.rb +2 -20
  88. data/lib/mml/v3/mspace.rb +3 -50
  89. data/lib/mml/v3/msqrt.rb +3 -12
  90. data/lib/mml/v3/msrow.rb +3 -14
  91. data/lib/mml/v3/mstack.rb +3 -20
  92. data/lib/mml/v3/mstyle.rb +5 -204
  93. data/lib/mml/v3/msub.rb +3 -14
  94. data/lib/mml/v3/msubsup.rb +3 -16
  95. data/lib/mml/v3/msup.rb +3 -14
  96. data/lib/mml/v3/mtable.rb +3 -53
  97. data/lib/mml/v3/mtd.rb +3 -16
  98. data/lib/mml/v3/mtext.rb +3 -30
  99. data/lib/mml/v3/mtr.rb +2 -21
  100. data/lib/mml/v3/munder.rb +3 -18
  101. data/lib/mml/v3/munderover.rb +3 -18
  102. data/lib/mml/v3/namespace.rb +1 -4
  103. data/lib/mml/v3/none.rb +2 -10
  104. data/lib/mml/v3/semantics.rb +3 -12
  105. data/lib/mml/v3.rb +51 -77
  106. data/lib/mml/v4/a.rb +4 -13
  107. data/lib/mml/v4/common_elements.rb +13 -0
  108. data/lib/mml/v4/configuration.rb +4 -97
  109. data/lib/mml/v4/maction.rb +4 -19
  110. data/lib/mml/v4/maligngroup.rb +4 -17
  111. data/lib/mml/v4/malignmark.rb +4 -17
  112. data/lib/mml/v4/math.rb +4 -14
  113. data/lib/mml/v4/menclose.rb +4 -18
  114. data/lib/mml/v4/merror.rb +4 -16
  115. data/lib/mml/v4/mfenced.rb +4 -25
  116. data/lib/mml/v4/mfrac.rb +4 -24
  117. data/lib/mml/v4/mfraction.rb +4 -20
  118. data/lib/mml/v4/mglyph.rb +4 -35
  119. data/lib/mml/v4/mi.rb +4 -30
  120. data/lib/mml/v4/mlabeledtr.rb +6 -23
  121. data/lib/mml/v4/mlongdiv.rb +4 -18
  122. data/lib/mml/v4/mmultiscripts.rb +4 -24
  123. data/lib/mml/v4/mn.rb +4 -29
  124. data/lib/mml/v4/mo.rb +4 -75
  125. data/lib/mml/v4/mover.rb +4 -20
  126. data/lib/mml/v4/mpadded.rb +4 -26
  127. data/lib/mml/v4/mphantom.rb +4 -16
  128. data/lib/mml/v4/mprescripts.rb +4 -11
  129. data/lib/mml/v4/mroot.rb +4 -16
  130. data/lib/mml/v4/mrow.rb +4 -20
  131. data/lib/mml/v4/ms.rb +4 -34
  132. data/lib/mml/v4/mscarries.rb +4 -20
  133. data/lib/mml/v4/mscarry.rb +4 -16
  134. data/lib/mml/v4/msgroup.rb +4 -18
  135. data/lib/mml/v4/msline.rb +4 -21
  136. data/lib/mml/v4/mspace.rb +4 -49
  137. data/lib/mml/v4/msqrt.rb +4 -16
  138. data/lib/mml/v4/msrow.rb +4 -18
  139. data/lib/mml/v4/mstack.rb +4 -24
  140. data/lib/mml/v4/mstyle.rb +4 -186
  141. data/lib/mml/v4/msub.rb +4 -18
  142. data/lib/mml/v4/msubsup.rb +4 -20
  143. data/lib/mml/v4/msup.rb +4 -18
  144. data/lib/mml/v4/mtable.rb +4 -55
  145. data/lib/mml/v4/mtd.rb +4 -20
  146. data/lib/mml/v4/mtext.rb +4 -29
  147. data/lib/mml/v4/mtr.rb +4 -25
  148. data/lib/mml/v4/munder.rb +4 -22
  149. data/lib/mml/v4/munderover.rb +4 -22
  150. data/lib/mml/v4/namespace.rb +1 -4
  151. data/lib/mml/v4/none.rb +6 -11
  152. data/lib/mml/v4/semantics.rb +4 -12
  153. data/lib/mml/v4.rb +52 -74
  154. data/lib/mml/version.rb +1 -1
  155. data/lib/mml/versioned_parser.rb +46 -0
  156. data/lib/mml.rb +34 -6
  157. metadata +60 -4
  158. data/lib/mml/v3/common_attributes.rb +0 -22
  159. data/lib/mml/v4/common_attributes.rb +0 -26
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b681a8e9ba48bcb41372e146fa0f16c95eae47303df8db3eb48a9561e959ce6d
4
- data.tar.gz: d0d046fceba746e66519e9c6c44b395b22d0078d06140e1db04bb0aadb29f37d
3
+ metadata.gz: 2c1d691b0a249ad8e7c35c7eb2daeaa315202df3bd5ce479cca4ff73ef8bd98e
4
+ data.tar.gz: 1c200909f2749d9b62136a3d23e9a5462454e46e3591587a463114abd7d201f8
5
5
  SHA512:
6
- metadata.gz: 5e21cda0df4bada88b50899cb98b58d78ec46f3edca237ae54f064a33038fef2b7b0f5a6ce053709c2113adc0d01ffdea6efb436ca40debb85a6c54c21dbde26
7
- data.tar.gz: 9544c69937ff05d4df59cd16943c9f393df5bd8a8f3a8a9cd24ba84d3bf9cb457bf6681e24e824faa6bb72c06a30729ef42963741c4034cffbb86a65925a689e
6
+ metadata.gz: 6328fec56b6a4bb170ba7085a0a27ae8264d16922119c63e255c72059d442f80ba80bae26179bf8f098ef53a47d25dfd69cacff8056f64f68de95f7219fcfa3f
7
+ data.tar.gz: c9c9ac8e92274886ea12c9c09d32e899583cbe1b99d753f890465e1ec06822b7b80ad34dd168b246fd0494bdeba588bb8bf8144bdb9ae18ec6c6f6450911a19b
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-03-31 12:25:08 UTC using RuboCop version 1.86.0.
3
+ # on 2026-04-05 08:05:31 UTC using RuboCop version 1.86.0.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -16,13 +16,23 @@ Layout/LineLength:
16
16
  - 'spec/mml_spec.rb'
17
17
  - 'spec/spec_helper.rb'
18
18
 
19
- # Offense count: 1
19
+ # Offense count: 18
20
+ # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
21
+ Metrics/AbcSize:
22
+ Enabled: false
23
+
24
+ # Offense count: 5
20
25
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
21
26
  # AllowedMethods: refine
22
27
  Metrics/BlockLength:
23
- Max: 27
28
+ Max: 172
24
29
 
25
- # Offense count: 3
30
+ # Offense count: 43
31
+ # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
32
+ Metrics/MethodLength:
33
+ Max: 175
34
+
35
+ # Offense count: 2
26
36
  # Configuration parameters: Prefixes, AllowedPatterns.
27
37
  # Prefixes: when, with, without
28
38
  RSpec/ContextWording:
data/CLAUDE.md ADDED
@@ -0,0 +1,79 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ `mml` is a Ruby gem that provides MathML 3 and MathML 4 XML parsing and serialization. It maps MathML elements into Ruby model classes using the `lutaml-model` framework. Part of the [Plurimath](https://github.com/plurimath/mml) ecosystem.
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ rake # Run specs + rubocop (default task)
13
+ bundle exec rspec # Run tests
14
+ bundle exec rspec spec/mml_spec.rb:42 # Run single test by line
15
+ bundle exec rspec --only-failures # Run only previously failing tests
16
+ bundle exec rubocop # Lint
17
+ bundle exec rubocop -a # Auto-fix lint issues
18
+ bin/console # IRB with gem loaded
19
+ ```
20
+
21
+ ## Versioned Architecture
22
+
23
+ **MathML 3 vs MathML 4:** The gem maintains separate class hierarchies for MathML 3 (`Mml::V3::`) and MathML 4 (`Mml::V4::`). Users must reference the versioned namespace explicitly — no backward-compat aliases.
24
+
25
+ ```ruby
26
+ Mml.parse(input) # Default: MathML 3
27
+ Mml.parse(input, version: 3) # Explicit MathML 3
28
+ Mml.parse(input, version: 4) # MathML 4 with intent/arg attributes
29
+ ```
30
+
31
+ **Directory structure:**
32
+ - `lib/mml/v3/` — MathML 3 element classes (original)
33
+ - `lib/mml/v4/` — MathML 4 element classes (intent/arg added, deprecated attrs removed)
34
+
35
+ **Key difference:** MathML 4 adds `intent`, `arg`, `displaystyle`, and `scriptlevel` attributes as universal presentation attributes for accessibility markup.
36
+
37
+ **No hidden delegation:** The `Mml` module does not alias or delegate constants. Use `Mml::V3::Math`, `Mml::V4::Mi`, etc. directly.
38
+
39
+ ## Entry Points
40
+
41
+ - `Mml.parse(input, version: N)` — parse XML, returns `Mml::V3::Math` or `Mml::V4::Math` object graph
42
+ - `Mml::V3.parse(input)` / `Mml::V4.parse(input)` — version-specific parsing
43
+ - `Mml::V4::Math.from_xml(input)` — directly parse with v4 classes
44
+ - Call `to_xml` on any element to serialize back
45
+
46
+ ## Pattern
47
+
48
+ Each MathML element is a `Mml::V3::` or `Mml::V4::` class inheriting from `Lutaml::Model::Serializable` with an `xml do...end` DSL block. Two element types:
49
+ - **Leaf elements** (e.g., `Mi`, `Mn`, `Mo`): use `map_content to: :value` for text content
50
+ - **Container elements** (e.g., `Math`, `Mrow`, `Mfrac`): use `mixed_content` to accept arbitrary child elements
51
+
52
+ **CommonAttributes:** A `no_root` Lutaml model imported into container elements via `import_model`. It dynamically creates `#{tag}_value` attributes for each tag in `Configuration::SUPPORTED_TAGS`. Classes that receive it are listed in `Configuration::COMMON_ATTRIBUTES_CLASSES`.
53
+
54
+ **Autoloading:** Each version (`lib/mml/v3.rb`, `lib/mml/v4.rb`) autoloads its element classes. `CommonAttributes` is required after all classes exist, then `update_attributes` mixes it into the configured classes.
55
+
56
+ **Namespace:** Both versions use the same URI (`http://www.w3.org/1998/Math/MathML`) — MathML 4 chose backward compatibility over a new namespace.
57
+
58
+ ## Spec Structure
59
+
60
+ - `spec/mml_spec.rb` — tests `Mml::V3` and `Mml::V4` with separate fixture directories
61
+ - `spec/fixtures/with_namespace/` — v3 fixtures (MathML 3)
62
+ - `spec/fixtures/with_namespace_prefix/` — v3 fixtures with namespace prefix
63
+ - `spec/fixtures/v4/` — v4 fixtures (MathML 4 with intent attributes)
64
+
65
+ Specs use the `:nokogiri` Lutaml adapter (configured in `spec_helper.rb`). Runtime uses `:ox` adapter by default.
66
+
67
+ ## Key Dependencies
68
+
69
+ - `lutaml-model` (~ 0.8.0) — data mapper framework; all element classes inherit from `Lutaml::Model::Serializable`
70
+ - `moxml` — XML parsing library (uses `:ox` adapter by default)
71
+ - `canon` — XML comparison for specs
72
+
73
+ ## Conventions
74
+
75
+ - `frozen_string_literal: true` in all files
76
+ - RuboCop targets Ruby 3.0; inherits from Ribose OSS guides
77
+ - CI workflows are auto-generated by Cimas — do not edit manually
78
+ - `Gemfile.lock` is gitignored; dependencies come from the gemspec
79
+ - Type signatures exist in `sig/mml.rbs`
data/README.adoc CHANGED
@@ -47,8 +47,8 @@ math4 = Mml.parse(input, version: 4)
47
47
  math.to_xml
48
48
 
49
49
  # Or use versioned modules directly
50
- Mml::V3::Math.from_xml(input)
51
- Mml::V4::Math.from_xml(input)
50
+ Mml::V3.parse(input)
51
+ Mml::V4.parse(input)
52
52
  ----
53
53
 
54
54
  == MathML version architecture
@@ -80,7 +80,7 @@ for backward compatibility.
80
80
  │ └────────────┬───────────────┘ │ │ │ + <a> element │ │
81
81
  │ │ │ │ └────────────┬────────────┘ │
82
82
  │ ┌────────────┴────────────┐ │ │ ┌────────────┴────────────┐ │
83
- │ │ CommonAttributes │ │ │ │ CommonAttributes │ │
83
+ │ │ CommonElements │ │ │ │ CommonElements │ │
84
84
  │ │ (child element mixin) │ │ │ │ (v4 version) │ │
85
85
  │ └─────────────────────────┘ │ | └─────────────────────────┘ |
86
86
  └─────────────────────────────────────┘ └───────────────────────────────┘
@@ -109,11 +109,13 @@ Mml::V4.parse(input) # Direct v4 parsing
109
109
  |Feature |MathML 4 additions
110
110
  |Universal attributes |`intent`, `arg`, `displaystyle`, `scriptlevel` available on all presentation elements
111
111
  |New element |`<a>` hyperlink element with `href`, `hreflang`
112
- |Deprecated (not serialized) a|
113
- * `fontfamily`, `fontweight`, `fontstyle`, `fontsize`, `color`, `background` on `mstyle`, `mglyph`, `mspace`
114
- * `groupalign` on `mtable`, `mtr`, `mlabeledtr`
115
- * `fence`, `separator` on `mo`
116
- |Deprecated (recognized but hidden) |`<mlabeledtr>`, `<none>` removed from `CommonAttributes` but classes still exist
112
+ |Deprecated in MathML 4 (still recognized for backward compatibility) a|
113
+ * `fontfamily`, `fontweight`, `fontstyle`, `fontsize`, `color`, `background` on token elements, `mstyle`, `mglyph`, `mspace`
114
+ * `mode`, `macros` on `math`
115
+ * `index` on `mglyph`
116
+
117
+ See https://github.com/w3c/mathml/issues/5[w3c/mathml#5] and
118
+ https://github.com/w3c/mathml/issues/303[w3c/mathml#303] for details.
117
119
  |===
118
120
 
119
121
  === Migration from previous versions
@@ -128,19 +130,29 @@ namespace:
128
130
  # Before (no longer supported)
129
131
  require "mml/configuration"
130
132
  Mml::Configuration.adapter = :nokogiri
131
- Mml::Configuration.custom_models = { Mi => MyCustomMi }
133
+ Mml::Configuration.create_context(id: :custom_v3)
132
134
  Mml::Math.new(...)
133
135
 
134
136
  # After — explicit version
135
137
  require "mml"
136
138
  Mml::V3::Configuration.adapter = :nokogiri
137
- Mml::V3::Configuration.custom_models = { Mi => MyCustomMi }
138
- Mml::V3::Math.new(...)
139
+ Mml::V3::Configuration.create_context(
140
+ id: :custom_v3,
141
+ substitutions: [
142
+ { from_type: Mml::V3::Mi, to_type: MyCustomMi }
143
+ ]
144
+ )
145
+ Mml::V3.parse(input, context: :custom_v3)
139
146
 
140
147
  # Or for MathML 4
141
148
  Mml::V4::Configuration.adapter = :nokogiri
142
- Mml::V4::Configuration.custom_models = { Mi => MyCustomMi }
143
- Mml::V4::Math.new(...)
149
+ Mml::V4::Configuration.create_context(
150
+ id: :custom_v4,
151
+ substitutions: [
152
+ { from_type: Mml::V4::Mi, to_type: MyCustomMi }
153
+ ]
154
+ )
155
+ Mml::V4.parse(input, context: :custom_v4)
144
156
  ----
145
157
 
146
158
  ==== Element class references
@@ -233,7 +245,7 @@ math.to_xml
233
245
 
234
246
  *v4 only*: `a` (hyperlink)
235
247
 
236
- *Deprecated*: `mlabeledtr`, `none` (classes exist but hidden from CommonAttributes in v4)
248
+ *Deprecated*: `mlabeledtr`, `none` (classes exist but hidden from CommonElements in v4)
237
249
 
238
250
  === Token elements (leaf nodes)
239
251
 
@@ -257,6 +269,7 @@ Container elements hold child elements via `#{tag}_value` collection attributes:
257
269
  [source,ruby]
258
270
  ----
259
271
  Mml::V3::Mrow.new(
272
+ lutaml_register: Mml::V3::Configuration.context_id,
260
273
  mi_value: [Mml::V3::Mi.new(value: "x")],
261
274
  mo_value: [Mml::V3::Mo.new(value: "+")],
262
275
  mn_value: [Mml::V3::Mn.new(value: "1")],
@@ -271,8 +284,10 @@ Build an expression tree by nesting elements:
271
284
  [source,ruby]
272
285
  ----
273
286
  Mml::V3::Math.new(
287
+ lutaml_register: Mml::V3::Configuration.context_id,
274
288
  mfrac_value: [
275
289
  Mml::V3::Mfrac.new(
290
+ lutaml_register: Mml::V3::Configuration.context_id,
276
291
  mi_value: [Mml::V3::Mi.new(value: "a"), Mml::V3::Mi.new(value: "b")],
277
292
  ),
278
293
  ],
@@ -285,11 +300,19 @@ Mml::V3::Math.new(
285
300
  [source,ruby]
286
301
  ----
287
302
  Mml::V3::Mtable.new(
303
+ lutaml_register: Mml::V3::Configuration.context_id,
288
304
  mtr_value: [
289
305
  Mml::V3::Mtr.new(
306
+ lutaml_register: Mml::V3::Configuration.context_id,
290
307
  mtd_value: [
291
- Mml::V3::Mtd.new(mi_value: [Mml::V3::Mi.new(value: "a")]),
292
- Mml::V3::Mtd.new(mi_value: [Mml::V3::Mi.new(value: "b")]),
308
+ Mml::V3::Mtd.new(
309
+ lutaml_register: Mml::V3::Configuration.context_id,
310
+ mi_value: [Mml::V3::Mi.new(value: "a")]
311
+ ),
312
+ Mml::V3::Mtd.new(
313
+ lutaml_register: Mml::V3::Configuration.context_id,
314
+ mi_value: [Mml::V3::Mi.new(value: "b")]
315
+ ),
293
316
  ],
294
317
  ),
295
318
  ],
@@ -301,6 +324,7 @@ Mml::V3::Mtable.new(
301
324
  [source,ruby]
302
325
  ----
303
326
  Mml::V4::A.new(
327
+ lutaml_register: Mml::V4::Configuration.context_id,
304
328
  href: "https://example.com",
305
329
  hreflang: "en",
306
330
  mi_value: [Mml::V4::Mi.new(value: "click")]
@@ -308,41 +332,116 @@ Mml::V4::A.new(
308
332
  # => <a href="https://example.com" hreflang="en"><mi>click</mi></a>
309
333
  ----
310
334
 
335
+ == MathML V4 Compliance
336
+
337
+ This implementation has been audited against the
338
+ https://www.w3.org/TR/MathML4/[MathML 4 W3C Recommendation].
339
+
340
+ === Universal V4 Attributes
341
+
342
+ All MathML 4 presentation elements include `intent`, `arg`, `displaystyle`,
343
+ `scriptlevel`, `mathcolor`, and `mathbackground` via the shared
344
+ `Base::V4Attributes` module.
345
+
346
+ === Legacy Schema Support
347
+
348
+ For backwards compatibility with existing MathML content, this gem supports
349
+ both the strict V4 schema and the legacy schema:
350
+
351
+ [cols="1,2",options="header"]
352
+ |===
353
+ |Feature |Status
354
+ |Universal V4 attributes (`intent`, `arg`, `displaystyle`, `scriptlevel`) |Full support
355
+ |`mathcolor`, `mathbackground` on all presentation elements |Full support
356
+ |`<a>` hyperlink element |Full support
357
+ |Deprecated font attributes (`fontfamily`, `fontweight`, etc.) |Legacy support (V3 + V4 legacy schema)
358
+ |`fence`, `separator` on `<mo>` |Legacy support (removed from default V4 schema)
359
+ |`none` element |Deprecated in V4 (empty `<mrow>` recommended)
360
+ |`mlabeledtr` element |Legacy support (removed from default V4 schema)
361
+ |===
362
+
363
+
311
364
  == Internal architecture
312
365
 
313
366
  === Element class patterns
314
367
 
315
- All element classes inherit from `Lutaml::Model::Serializable`:
368
+ Shared attributes and mappings live in `Base::` modules (`lib/mml/base/`).
369
+ V3 and V4 classes include these modules independently — no cross-version inheritance.
316
370
 
317
- * *Leaf elements*: use `map_content to: :value` for text content
318
- * *Container elements*: use `mixed_content` for child elements
371
+ * *Leaf elements*: inherit `Lutaml::Model::Serializable`, include `Base::ElementName`
372
+ * *Container elements*: inherit `CommonElements`, include `Base::ElementName`
373
+
374
+ Each element self-registers in its version's built-in `GlobalContext` context.
319
375
 
320
376
  [source,ruby]
321
377
  ----
322
- # Leaf text content
323
- class Mi < Lutaml::Model::Serializable
324
- attribute :value, :string
325
- xml do
326
- element "mi"
327
- map_content to: :value
378
+ # Shared attributes (lib/mml/base/mi.rb)
379
+ module Base::Mi
380
+ def self.included(klass)
381
+ klass.class_eval do
382
+ attribute :value, :string
383
+ xml do
384
+ element "mi"
385
+ map_content to: :value
386
+ end
387
+ end
328
388
  end
329
389
  end
330
390
 
331
- # Container — child elements
332
- class Mrow < Lutaml::Model::Serializable
333
- xml do
334
- element "mrow"
335
- mixed_content
336
- end
391
+ # V3 leaf
392
+ class V3::Mi < Lutaml::Model::Serializable
393
+ include Base::Mi
394
+ end
395
+
396
+ # V4 leaf — adds V4-only attributes
397
+ class V4::Mi < Lutaml::Model::Serializable
398
+ include Base::Mi
399
+ attribute :intent, :string
400
+ end
401
+
402
+ # V3 container
403
+ class V3::Mrow < CommonElements
404
+ include Base::Mrow
337
405
  end
338
406
  ----
339
407
 
340
- === CommonAttributes
408
+ === CommonElements
409
+
410
+ Container elements inherit `CommonElements`, which defines `#{tag}_value` collection
411
+ attributes for all supported child elements. Attribute types use symbols (e.g., `:mi`,
412
+ `:mfrac`) resolved through `Lutaml::Model::GlobalContext`.
413
+
414
+ V4's `CommonElements` extends the base with the `<a>` hyperlink element.
341
415
 
342
- Container elements that accept arbitrary MathML children import `CommonAttributes`,
343
- which defines `#{tag}_value` collection attributes for all supported child elements.
344
- The list of classes that receive this mixin is in
345
- `Configuration::COMMON_ATTRIBUTES_CLASSES`.
416
+ === Context and type resolution
417
+
418
+ When calling `from_xml` directly (outside of `Mml.parse` or `Mml::V3.parse`), pass
419
+ the version-specific context id for correct type resolution.
420
+
421
+ NOTE: `lutaml-model` still uses the keyword name `register:` in low-level APIs.
422
+ In MML, the value passed to that keyword should be a context id.
423
+
424
+ [source,ruby]
425
+ ----
426
+ Mml::V3::Math.from_xml(input, register: Mml::V3::Configuration.context_id)
427
+ Mml::V4::Math.from_xml(input, register: Mml::V4::Configuration.context_id)
428
+ ----
429
+
430
+ The `parse` methods handle this automatically.
431
+
432
+ When constructing container elements directly, also pass the context id on the
433
+ instance via `lutaml_register:` so symbolic child types resolve in the right
434
+ versioned context:
435
+
436
+ [source,ruby]
437
+ ----
438
+ math = Mml::V3::Math.new(
439
+ lutaml_register: Mml::V3::Configuration.context_id,
440
+ mi_value: [Mml::V3::Mi.new(value: "x")]
441
+ )
442
+
443
+ math.to_xml
444
+ ----
346
445
 
347
446
  === Namespace
348
447
 
@@ -355,19 +454,43 @@ Three input forms are supported:
355
454
 
356
455
  == Configuration
357
456
 
358
- Configuration is version-specific. Use the namespace matching your target version:
359
-
360
457
  [source,ruby]
361
458
  ----
362
- # Switch XML adapter (default: :ox, also supports :nokogiri)
459
+ # Switch XML adapter (default: :ox, :oga on Opal)
363
460
  Mml::V3::Configuration.adapter = :nokogiri
364
- Mml::V4::Configuration.adapter = :nokogiri
365
461
 
366
- # Register custom model replacements
367
- Mml::V3::Configuration.custom_models = { Mi => MyCustomMi }
368
- Mml::V4::Configuration.custom_models = { Mi => MyCustomMi }
462
+ # Access the built-in version-specific contexts
463
+ Mml::V3::Configuration.context_id # => :mml_v3
464
+ Mml::V4::Configuration.context_id # => :mml_v4
465
+ Mml::V3::Configuration.context
466
+ Mml::V4::Configuration.context
467
+
468
+ # Rebuild a built-in context after an explicit GlobalContext.reset!
469
+ Mml::V3::Configuration.populate_context!
470
+ Mml::V4::Configuration.populate_context!
471
+
472
+ # Create a derived context with substitutions
473
+ Mml::V3::Configuration.create_context(
474
+ id: :custom_v3,
475
+ substitutions: [
476
+ { from_type: Mml::V3::Mi, to_type: MyCustomMi }
477
+ ]
478
+ )
479
+
480
+ # Parse using the custom context
481
+ Mml::V3.parse(input, context: :custom_v3)
482
+
483
+ # Low-level APIs still use the upstream keyword name `register:`
484
+ Mml::V3::Math.from_xml(input, register: :custom_v3)
369
485
  ----
370
486
 
487
+ The `context:` keyword is the preferred MML API. The legacy `register:` keyword is
488
+ still accepted temporarily in MML parse methods, but it emits a deprecation warning
489
+ and is normalized to a context id internally.
490
+
491
+ If you reset global contexts and need the built-in MML contexts restored
492
+ explicitly, call `populate_context!` for the version(s) you want to restore.
493
+
371
494
  == Development
372
495
 
373
496
  [source,bash]
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mml
4
+ module Base
5
+ # Deprecated MathML 1 font attributes: fontfamily, fontweight, fontstyle,
6
+ # fontsize, color, background.
7
+ # These are removed in MathML 4 default schema but valid in mathml4-legacy.
8
+ module DeprecatedFontAttributes
9
+ def self.included(klass)
10
+ klass.class_eval do
11
+ attribute :fontfamily, :string
12
+ attribute :fontweight, :string
13
+ attribute :fontstyle, :string
14
+ attribute :fontsize, :string
15
+ attribute :color, :string
16
+ attribute :background, :string
17
+
18
+ xml do
19
+ namespace Mml::Namespace
20
+ map_attribute "fontfamily", to: :fontfamily
21
+ map_attribute "fontweight", to: :fontweight
22
+ map_attribute "fontstyle", to: :fontstyle
23
+ map_attribute "fontsize", to: :fontsize
24
+ map_attribute "color", to: :color
25
+ map_attribute "background", to: :background
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mml
4
+ module Base
5
+ module Maction
6
+ # NOTE: class_eval resolves constants in module's lexical scope.
7
+ # Use fully qualified names (e.g., Mml::Namespace).
8
+ def self.included(klass)
9
+ klass.class_eval do
10
+ attribute :mathcolor, :string
11
+ attribute :mathbackground, :string
12
+ attribute :actiontype, :string
13
+ attribute :selection, :string
14
+
15
+ xml do
16
+ namespace Mml::Namespace
17
+ element "maction"
18
+
19
+ map_attribute "mathcolor", to: :mathcolor
20
+ map_attribute "mathbackground", to: :mathbackground
21
+ map_attribute "actiontype", to: :actiontype
22
+ map_attribute "selection", to: :selection
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mml
4
+ module Base
5
+ module Maligngroup
6
+ # NOTE: class_eval resolves constants in module's lexical scope.
7
+ # Use fully qualified names (e.g., Mml::Namespace).
8
+ def self.included(klass)
9
+ klass.class_eval do
10
+ attribute :mathcolor, :string
11
+ attribute :mathbackground, :string
12
+ attribute :groupalign, :string
13
+
14
+ xml do
15
+ namespace Mml::Namespace
16
+ element "maligngroup"
17
+
18
+ map_attribute "mathcolor", to: :mathcolor
19
+ map_attribute "mathbackground", to: :mathbackground
20
+ map_attribute "groupalign", to: :groupalign
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mml
4
+ module Base
5
+ module Malignmark
6
+ # NOTE: class_eval resolves constants in module's lexical scope.
7
+ # Use fully qualified names (e.g., Mml::Namespace).
8
+ def self.included(klass)
9
+ klass.class_eval do
10
+ attribute :mathcolor, :string
11
+ attribute :mathbackground, :string
12
+ attribute :edge, :string
13
+
14
+ xml do
15
+ namespace Mml::Namespace
16
+ element "malignmark"
17
+
18
+ map_attribute "mathcolor", to: :mathcolor
19
+ map_attribute "mathbackground", to: :mathbackground
20
+ map_attribute "edge", to: :edge
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mml
4
+ module Base
5
+ module Math
6
+ # NOTE: class_eval resolves constants in module's lexical scope.
7
+ # Use fully qualified names (e.g., Mml::Namespace).
8
+ def self.included(klass)
9
+ klass.class_eval do
10
+ attribute :display, :string
11
+
12
+ xml do
13
+ namespace Mml::Namespace
14
+ element "math"
15
+ mixed_content
16
+
17
+ map_attribute :display, to: :display
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mml
4
+ module Base
5
+ module Menclose
6
+ # NOTE: class_eval resolves constants in module's lexical scope.
7
+ # Use fully qualified names (e.g., Mml::Namespace).
8
+ def self.included(klass)
9
+ klass.class_eval do
10
+ attribute :mathcolor, :string
11
+ attribute :mathbackground, :string
12
+ attribute :notation, :string
13
+
14
+ xml do
15
+ namespace Mml::Namespace
16
+ element "menclose"
17
+ mixed_content
18
+
19
+ map_attribute "mathcolor", to: :mathcolor
20
+ map_attribute "mathbackground", to: :mathbackground
21
+ map_attribute "notation", to: :notation
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mml
4
+ module Base
5
+ module Merror
6
+ # NOTE: class_eval resolves constants in module's lexical scope.
7
+ # Use fully qualified names (e.g., Mml::Namespace).
8
+ def self.included(klass)
9
+ klass.class_eval do
10
+ attribute :mathbackground, :string
11
+ attribute :mathcolor, :string
12
+
13
+ xml do
14
+ namespace Mml::Namespace
15
+ element "merror"
16
+ mixed_content
17
+
18
+ map_attribute "mathcolor", to: :mathcolor
19
+ map_attribute "mathbackground", to: :mathbackground
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mml
4
+ module Base
5
+ module Mfenced
6
+ # NOTE: class_eval resolves constants in module's lexical scope.
7
+ # Use fully qualified names (e.g., Mml::Namespace).
8
+ def self.included(klass)
9
+ klass.class_eval do
10
+ attribute :mathbackground, :string
11
+ attribute :separators, :string
12
+ attribute :mathcolor, :string
13
+ attribute :content, :string
14
+ attribute :close, :string
15
+ attribute :open, :string
16
+
17
+ xml do
18
+ namespace Mml::Namespace
19
+ element "mfenced"
20
+ mixed_content
21
+
22
+ map_content to: :content
23
+
24
+ map_attribute "mathbackground", to: :mathbackground
25
+ map_attribute "separators", to: :separators
26
+ map_attribute "mathcolor", to: :mathcolor
27
+ map_attribute "close", to: :close
28
+ map_attribute "open", to: :open
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end