mathml-to-omml 1.0.2__tar.gz
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.
- mathml_to_omml-1.0.2/LICENSE +23 -0
- mathml_to_omml-1.0.2/MANIFEST.in +6 -0
- mathml_to_omml-1.0.2/PKG-INFO +100 -0
- mathml_to_omml-1.0.2/README.md +71 -0
- mathml_to_omml-1.0.2/examples/convert_html_word.py +206 -0
- mathml_to_omml-1.0.2/examples/convert_latex_word.py +212 -0
- mathml_to_omml-1.0.2/examples/convert_linear_word.py +164 -0
- mathml_to_omml-1.0.2/examples/html_data.html +1992 -0
- mathml_to_omml-1.0.2/examples/latex_math_data.md +1020 -0
- mathml_to_omml-1.0.2/examples/linear_math_data.md +1020 -0
- mathml_to_omml-1.0.2/examples/omml_data.docx +0 -0
- mathml_to_omml-1.0.2/pyproject.toml +40 -0
- mathml_to_omml-1.0.2/setup.cfg +4 -0
- mathml_to_omml-1.0.2/setup.py +33 -0
- mathml_to_omml-1.0.2/src/math_ml2omml/__init__.py +1502 -0
- mathml_to_omml-1.0.2/src/math_ml2omml/__main__.py +8 -0
- mathml_to_omml-1.0.2/src/math_ml2omml/cli.py +214 -0
- mathml_to_omml-1.0.2/src/math_ml2omml/op_dict.py +1110 -0
- mathml_to_omml-1.0.2/src/mathml_to_omml.egg-info/PKG-INFO +100 -0
- mathml_to_omml-1.0.2/src/mathml_to_omml.egg-info/SOURCES.txt +24 -0
- mathml_to_omml-1.0.2/src/mathml_to_omml.egg-info/dependency_links.txt +1 -0
- mathml_to_omml-1.0.2/src/mathml_to_omml.egg-info/entry_points.txt +2 -0
- mathml_to_omml-1.0.2/src/mathml_to_omml.egg-info/requires.txt +3 -0
- mathml_to_omml-1.0.2/src/mathml_to_omml.egg-info/top_level.txt +1 -0
- mathml_to_omml-1.0.2/tests/__init__.py +1 -0
- mathml_to_omml-1.0.2/tests/test_math_ml2omml.py +429 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 amedama
|
|
4
|
+
Portions Copyright (c) 2026 Robert Danaraj (salorajan@gmail.com)
|
|
5
|
+
Portions Copyright (c) 2026 Daisy Project Contributors
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mathml_to_omml
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: Convert MathML to OMML (Office Math Markup Language) in pure Python
|
|
5
|
+
Home-page: https://github.com/salorajan/math_ml2omml
|
|
6
|
+
Author: amedama, w2w Contributors
|
|
7
|
+
Author-email: amedama <amedama@example.com>, w2w Contributors <contributors@w2w.org>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Text Processing :: Markup :: XML
|
|
19
|
+
Requires-Python: >=3.7
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: latex2mathml
|
|
23
|
+
Requires-Dist: lxml
|
|
24
|
+
Requires-Dist: python-docx
|
|
25
|
+
Dynamic: author
|
|
26
|
+
Dynamic: home-page
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
|
|
30
|
+
# math_ml2omml
|
|
31
|
+
|
|
32
|
+
A pure Python library to convert MathML strings into OMML (Office Math Markup Language).
|
|
33
|
+
|
|
34
|
+
This is a modern, patched version of the original `mathml2omml` library, containing key bug fixes and enhancements developed for the `w2w` (Word-to-Word Math Compiler) project to ensure 100% parity with Microsoft Word's professional equation layouts.
|
|
35
|
+
|
|
36
|
+
## Key Enhancements in this Patched Version
|
|
37
|
+
- **Accent and Group Character Rendering**: Fixed tag mismatches on `<m:groupChr>` inside `MUnder` and `MOver` classes. This ensures mathematical accents (like hats, tildes, dots) and underscript/overscript groupings compile and render correctly in Microsoft Word.
|
|
38
|
+
- **Improved Alignment and Layout Compatibility**: Programmatic mapping fixes to match standard Office OpenXML (OOXML) math specifications.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
You can install it directly from this package directory:
|
|
43
|
+
```bash
|
|
44
|
+
pip install .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or for development (editable mode):
|
|
48
|
+
```bash
|
|
49
|
+
pip install -e .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import math_ml2omml
|
|
56
|
+
|
|
57
|
+
mathml = '<math><mi>x</mi><mo>+</mo><mi>y</mi></math>'
|
|
58
|
+
omml = math_ml2omml.convert(mathml)
|
|
59
|
+
print(omml)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Custom Entities
|
|
63
|
+
If your MathML contains custom entities, pass them as a dictionary:
|
|
64
|
+
```python
|
|
65
|
+
import html.entities
|
|
66
|
+
mathml = '<math><msubsup><mo>∫</mo><mn>1</mn><mi>x</mi></msubsup></math>'
|
|
67
|
+
omml = math_ml2omml.convert(mathml, html.entities.name2codepoint)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Running Tests
|
|
71
|
+
|
|
72
|
+
To verify the installation and run the suite of unit tests, execute the following command in the project root:
|
|
73
|
+
```bash
|
|
74
|
+
python -m unittest discover -s tests
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Author
|
|
78
|
+
Patched and maintained by **Robert Danaraj** (salorajan@gmail.com).
|
|
79
|
+
|
|
80
|
+
## Acknowledgements
|
|
81
|
+
Special thanks and deepest gratitude to **George Kerscher** ([IMLS Profile](https://www.imls.gov/about-us/national-museum-and-library-services-board/george-kerscher)), Chief Innovations Officer of the DAISY Consortium and President of the International Digital Publishing Forum. As a pioneering advocate for digital document accessibility (recognized as a White House Champion of Change), his invaluable discussion, guidance, and inspiration motivated the design, development, and deployment of this project.
|
|
82
|
+
|
|
83
|
+
We also thank the contributors of the [daisy-math-a11y](https://github.com/daisy/math-a11y) project for inspiring our work.
|
|
84
|
+
|
|
85
|
+
## Keystroke Complexity Score (KCS)
|
|
86
|
+
This library includes a utility `calculate_kcs()` to compute the keyboard entry complexity cost of inputting mathematical equations into Microsoft Word's Equation Editor GUI. It is based on the **Keystroke-Level Model (KLM)** (Card, Moran, and Newell, 1980), adapted specifically for the interactive auto-build behaviors of Word's UnicodeMath input syntax:
|
|
87
|
+
- **Raw Keystrokes ($K$)**: Character count of the linear notation.
|
|
88
|
+
- **Cognitive Penalties ($C_i$)**: Additional weight for Shift keys (+1.0) and backslash escape sequences (+1.5).
|
|
89
|
+
- **Auto-Build Spacebars ($C_{spacebar}$)**: Weight for spacebar triggers required to compile structures like fractions, scripts, roots, and matrices (+2.0).
|
|
90
|
+
- **Fragility Penalty ($F$)**: Additional penalties (+10.0) for structures prone to user input errors without proper grouping.
|
|
91
|
+
|
|
92
|
+
### Citations
|
|
93
|
+
- **Keystroke-Level Model (KLM)**: Card, S. K., Moran, T. P., and Newell, A. (1980). *The Keystroke-Level Model for User Performance Time with Interactive Systems*. Communications of the ACM, 23(7), 396–410.
|
|
94
|
+
- **UnicodeMath / Linear Math**: Murray Sargent III. (2010). *Unicode Sincerely, Math (Unicode Technical Note #28)*. Microsoft Corporation. [UTN #28 Specification](https://www.unicode.org/notes/tn28/UTN28-PlainTextMath-v3.pdf)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
MIT License. See [LICENSE](LICENSE) for details. Portions copyright (c) 2026 Daisy Project Contributors.
|
|
99
|
+
|
|
100
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# math_ml2omml
|
|
2
|
+
|
|
3
|
+
A pure Python library to convert MathML strings into OMML (Office Math Markup Language).
|
|
4
|
+
|
|
5
|
+
This is a modern, patched version of the original `mathml2omml` library, containing key bug fixes and enhancements developed for the `w2w` (Word-to-Word Math Compiler) project to ensure 100% parity with Microsoft Word's professional equation layouts.
|
|
6
|
+
|
|
7
|
+
## Key Enhancements in this Patched Version
|
|
8
|
+
- **Accent and Group Character Rendering**: Fixed tag mismatches on `<m:groupChr>` inside `MUnder` and `MOver` classes. This ensures mathematical accents (like hats, tildes, dots) and underscript/overscript groupings compile and render correctly in Microsoft Word.
|
|
9
|
+
- **Improved Alignment and Layout Compatibility**: Programmatic mapping fixes to match standard Office OpenXML (OOXML) math specifications.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
You can install it directly from this package directory:
|
|
14
|
+
```bash
|
|
15
|
+
pip install .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or for development (editable mode):
|
|
19
|
+
```bash
|
|
20
|
+
pip install -e .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import math_ml2omml
|
|
27
|
+
|
|
28
|
+
mathml = '<math><mi>x</mi><mo>+</mo><mi>y</mi></math>'
|
|
29
|
+
omml = math_ml2omml.convert(mathml)
|
|
30
|
+
print(omml)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Custom Entities
|
|
34
|
+
If your MathML contains custom entities, pass them as a dictionary:
|
|
35
|
+
```python
|
|
36
|
+
import html.entities
|
|
37
|
+
mathml = '<math><msubsup><mo>∫</mo><mn>1</mn><mi>x</mi></msubsup></math>'
|
|
38
|
+
omml = math_ml2omml.convert(mathml, html.entities.name2codepoint)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Running Tests
|
|
42
|
+
|
|
43
|
+
To verify the installation and run the suite of unit tests, execute the following command in the project root:
|
|
44
|
+
```bash
|
|
45
|
+
python -m unittest discover -s tests
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Author
|
|
49
|
+
Patched and maintained by **Robert Danaraj** (salorajan@gmail.com).
|
|
50
|
+
|
|
51
|
+
## Acknowledgements
|
|
52
|
+
Special thanks and deepest gratitude to **George Kerscher** ([IMLS Profile](https://www.imls.gov/about-us/national-museum-and-library-services-board/george-kerscher)), Chief Innovations Officer of the DAISY Consortium and President of the International Digital Publishing Forum. As a pioneering advocate for digital document accessibility (recognized as a White House Champion of Change), his invaluable discussion, guidance, and inspiration motivated the design, development, and deployment of this project.
|
|
53
|
+
|
|
54
|
+
We also thank the contributors of the [daisy-math-a11y](https://github.com/daisy/math-a11y) project for inspiring our work.
|
|
55
|
+
|
|
56
|
+
## Keystroke Complexity Score (KCS)
|
|
57
|
+
This library includes a utility `calculate_kcs()` to compute the keyboard entry complexity cost of inputting mathematical equations into Microsoft Word's Equation Editor GUI. It is based on the **Keystroke-Level Model (KLM)** (Card, Moran, and Newell, 1980), adapted specifically for the interactive auto-build behaviors of Word's UnicodeMath input syntax:
|
|
58
|
+
- **Raw Keystrokes ($K$)**: Character count of the linear notation.
|
|
59
|
+
- **Cognitive Penalties ($C_i$)**: Additional weight for Shift keys (+1.0) and backslash escape sequences (+1.5).
|
|
60
|
+
- **Auto-Build Spacebars ($C_{spacebar}$)**: Weight for spacebar triggers required to compile structures like fractions, scripts, roots, and matrices (+2.0).
|
|
61
|
+
- **Fragility Penalty ($F$)**: Additional penalties (+10.0) for structures prone to user input errors without proper grouping.
|
|
62
|
+
|
|
63
|
+
### Citations
|
|
64
|
+
- **Keystroke-Level Model (KLM)**: Card, S. K., Moran, T. P., and Newell, A. (1980). *The Keystroke-Level Model for User Performance Time with Interactive Systems*. Communications of the ACM, 23(7), 396–410.
|
|
65
|
+
- **UnicodeMath / Linear Math**: Murray Sargent III. (2010). *Unicode Sincerely, Math (Unicode Technical Note #28)*. Microsoft Corporation. [UTN #28 Specification](https://www.unicode.org/notes/tn28/UTN28-PlainTextMath-v3.pdf)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
MIT License. See [LICENSE](LICENSE) for details. Portions copyright (c) 2026 Daisy Project Contributors.
|
|
70
|
+
|
|
71
|
+
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
# Gracefully check dependencies
|
|
6
|
+
missing_deps = []
|
|
7
|
+
try:
|
|
8
|
+
import lxml.etree
|
|
9
|
+
import lxml.html
|
|
10
|
+
except ImportError:
|
|
11
|
+
missing_deps.append("lxml")
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import docx
|
|
15
|
+
from docx.oxml import parse_xml
|
|
16
|
+
except ImportError:
|
|
17
|
+
missing_deps.append("python-docx")
|
|
18
|
+
|
|
19
|
+
if missing_deps:
|
|
20
|
+
print(f"Error: Missing required dependencies: {', '.join(missing_deps)}", file=sys.stderr)
|
|
21
|
+
print(f"Please install them using: pip install {' '.join(missing_deps)}", file=sys.stderr)
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
# Ensure we can import math_ml2omml from the local src/ directory
|
|
25
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
26
|
+
src_path = os.path.abspath(os.path.join(script_dir, "..", "src"))
|
|
27
|
+
sys.path.insert(0, src_path)
|
|
28
|
+
|
|
29
|
+
import math_ml2omml
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def convert_html_to_docx(html_path: str, docx_path: str):
|
|
33
|
+
"""
|
|
34
|
+
Parses an HTML file containing MathML equations, converts the equations
|
|
35
|
+
to OMML using math_ml2omml, and builds a WCAG-compliant structured Word document.
|
|
36
|
+
"""
|
|
37
|
+
print(f"Reading HTML file: {html_path}")
|
|
38
|
+
if not os.path.exists(html_path):
|
|
39
|
+
raise FileNotFoundError(f"Source HTML file not found: {html_path}")
|
|
40
|
+
|
|
41
|
+
# 1. Parse the HTML document
|
|
42
|
+
with open(html_path, "r", encoding="utf-8") as f:
|
|
43
|
+
html_content = f.read()
|
|
44
|
+
|
|
45
|
+
parser = lxml.html.HTMLParser(encoding="utf-8")
|
|
46
|
+
doc_tree = lxml.html.fromstring(html_content, parser=parser)
|
|
47
|
+
|
|
48
|
+
# 2. Initialize a new Word Document
|
|
49
|
+
doc = docx.Document()
|
|
50
|
+
|
|
51
|
+
# 3. Add Document Title (Heading 1) - WCAG 2.2 Page structure
|
|
52
|
+
doc.add_heading("University Mathematics Equation Reference Database", level=1)
|
|
53
|
+
doc.add_paragraph(
|
|
54
|
+
"This document contains 200 mathematical equations spanning algebra, "
|
|
55
|
+
"calculus, differential equations, and discrete mathematics. All equations "
|
|
56
|
+
"are rendered as native Office Math Markup Language (OMML) blocks and accompanied "
|
|
57
|
+
"by alternative text descriptions to meet WCAG 2.2 accessibility guidelines."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# 4. Find all categories and process their problems
|
|
61
|
+
categories = doc_tree.xpath("//section[@class='category-section']")
|
|
62
|
+
print(f"Found {len(categories)} math categories. Processing equations...")
|
|
63
|
+
|
|
64
|
+
problem_count = 0
|
|
65
|
+
for category in categories:
|
|
66
|
+
# Category Heading (Heading 2)
|
|
67
|
+
cat_heading_elem = category.find("./h2")
|
|
68
|
+
cat_title = (
|
|
69
|
+
cat_heading_elem.text.strip()
|
|
70
|
+
if cat_heading_elem is not None
|
|
71
|
+
else "Untitled Category"
|
|
72
|
+
)
|
|
73
|
+
doc.add_heading(cat_title, level=2)
|
|
74
|
+
|
|
75
|
+
# Process each problem card
|
|
76
|
+
problems = category.xpath(".//article[@class='problem-card']")
|
|
77
|
+
for problem in problems:
|
|
78
|
+
# Problem Title (Heading 3)
|
|
79
|
+
prob_title_elem = problem.find("./h3")
|
|
80
|
+
prob_title = (
|
|
81
|
+
prob_title_elem.text.strip()
|
|
82
|
+
if prob_title_elem is not None
|
|
83
|
+
else "Untitled Problem"
|
|
84
|
+
)
|
|
85
|
+
doc.add_heading(prob_title, level=3)
|
|
86
|
+
|
|
87
|
+
# Get description
|
|
88
|
+
desc_elem = problem.xpath(".//p[@class='description']")
|
|
89
|
+
description_text = ""
|
|
90
|
+
if desc_elem:
|
|
91
|
+
full_desc = desc_elem[0].text_content().strip()
|
|
92
|
+
if "Accessibility Description:" in full_desc:
|
|
93
|
+
description_text = full_desc.split("Accessibility Description:")[1].strip()
|
|
94
|
+
else:
|
|
95
|
+
description_text = full_desc
|
|
96
|
+
|
|
97
|
+
# Get Unicode linear math
|
|
98
|
+
linear_elem = problem.xpath(".//p[@class='linear-math']")
|
|
99
|
+
linear_text = ""
|
|
100
|
+
if linear_elem:
|
|
101
|
+
full_linear = linear_elem[0].text_content().strip()
|
|
102
|
+
if "Unicode Linear Math Equivalent:" in full_linear:
|
|
103
|
+
linear_text = full_linear.split("Unicode Linear Math Equivalent:")[1].strip()
|
|
104
|
+
else:
|
|
105
|
+
linear_text = full_linear
|
|
106
|
+
|
|
107
|
+
# Get KCS score
|
|
108
|
+
kcs_elem = problem.xpath(".//p[@class='kcs-score']")
|
|
109
|
+
kcs_text = ""
|
|
110
|
+
if kcs_elem:
|
|
111
|
+
full_kcs = kcs_elem[0].text_content().strip()
|
|
112
|
+
if "Keyboard Complexity Score (KCS):" in full_kcs:
|
|
113
|
+
kcs_text = full_kcs.split("Keyboard Complexity Score (KCS):")[1].strip()
|
|
114
|
+
else:
|
|
115
|
+
kcs_text = full_kcs
|
|
116
|
+
|
|
117
|
+
# Locate the MathML element
|
|
118
|
+
math_elements = problem.xpath(".//math")
|
|
119
|
+
if math_elements:
|
|
120
|
+
math_elem = math_elements[0]
|
|
121
|
+
# Convert the lxml element to a MathML string
|
|
122
|
+
mathml_str = lxml.etree.tostring(math_elem, encoding="unicode")
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
# Convert Presentation MathML to OMML XML string
|
|
126
|
+
omml_str = math_ml2omml.convert(mathml_str)
|
|
127
|
+
|
|
128
|
+
# Ensure OpenXML math namespaces are explicitly declared
|
|
129
|
+
if "xmlns:m=" not in omml_str:
|
|
130
|
+
omml_str = omml_str.replace(
|
|
131
|
+
"<m:oMath",
|
|
132
|
+
'<m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"',
|
|
133
|
+
1,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Parse into a python-docx OxmlElement
|
|
137
|
+
omml_xml_element = parse_xml(omml_str)
|
|
138
|
+
|
|
139
|
+
# Create a paragraph for the equation
|
|
140
|
+
eq_para = doc.add_paragraph()
|
|
141
|
+
eq_para.paragraph_format.space_before = docx.shared.Pt(6)
|
|
142
|
+
eq_para.paragraph_format.space_after = docx.shared.Pt(6)
|
|
143
|
+
# Append the OMML math block directly to the paragraph's XML element
|
|
144
|
+
eq_para._p.append(omml_xml_element)
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
print(
|
|
148
|
+
f"Warning: Failed to convert math block for '{prob_title}': {e}"
|
|
149
|
+
)
|
|
150
|
+
# Fallback to plain text LaTeX representations if conversion fails
|
|
151
|
+
doc.add_paragraph(
|
|
152
|
+
f"[Conversion Error: MathML could not be compiled. {e}]"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Add the descriptive text for screen readers/learning support (WCAG 2.2)
|
|
156
|
+
if description_text:
|
|
157
|
+
desc_para = doc.add_paragraph()
|
|
158
|
+
run = desc_para.add_run(f"Accessibility Description: {description_text}")
|
|
159
|
+
run.italic = True
|
|
160
|
+
run.font.color.rgb = docx.shared.RGBColor(100, 116, 139)
|
|
161
|
+
|
|
162
|
+
# Add Unicode linear math equivalent
|
|
163
|
+
if linear_text:
|
|
164
|
+
lin_para = doc.add_paragraph()
|
|
165
|
+
run = lin_para.add_run(f"Unicode Linear Math: {linear_text}")
|
|
166
|
+
run.font.color.rgb = docx.shared.RGBColor(30, 58, 138)
|
|
167
|
+
run.bold = True
|
|
168
|
+
|
|
169
|
+
# Add KCS score
|
|
170
|
+
if kcs_text:
|
|
171
|
+
kcs_para = doc.add_paragraph()
|
|
172
|
+
run = kcs_para.add_run(f"Keyboard Complexity Score (KCS): {kcs_text}")
|
|
173
|
+
run.font.color.rgb = docx.shared.RGBColor(185, 28, 28)
|
|
174
|
+
kcs_para.paragraph_format.space_after = docx.shared.Pt(18)
|
|
175
|
+
|
|
176
|
+
problem_count += 1
|
|
177
|
+
|
|
178
|
+
# 5. Save the compiled document
|
|
179
|
+
print(f"Saving Word document: {docx_path}")
|
|
180
|
+
doc.save(docx_path)
|
|
181
|
+
print(
|
|
182
|
+
f"Successfully converted {problem_count} equations and saved to {docx_path}"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
parser = argparse.ArgumentParser(
|
|
188
|
+
description="Convert HTML containing MathML equations to a Word document."
|
|
189
|
+
)
|
|
190
|
+
parser.add_argument(
|
|
191
|
+
"--input", "-i",
|
|
192
|
+
default=os.path.join(script_dir, "html_data.html"),
|
|
193
|
+
help="Path to the input HTML file (default: html_data.html in script directory)"
|
|
194
|
+
)
|
|
195
|
+
parser.add_argument(
|
|
196
|
+
"--output", "-o",
|
|
197
|
+
default=os.path.join(script_dir, "omml_data.docx"),
|
|
198
|
+
help="Path to the output Word document (default: omml_data.docx in script directory)"
|
|
199
|
+
)
|
|
200
|
+
args = parser.parse_args()
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
convert_html_to_docx(args.input, args.output)
|
|
204
|
+
except Exception as err:
|
|
205
|
+
print(f"Error executing conversion: {err}", file=sys.stderr)
|
|
206
|
+
sys.exit(1)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import re
|
|
4
|
+
import argparse
|
|
5
|
+
|
|
6
|
+
# Gracefully check dependencies
|
|
7
|
+
missing_deps = []
|
|
8
|
+
try:
|
|
9
|
+
import latex2mathml.converter
|
|
10
|
+
except ImportError:
|
|
11
|
+
missing_deps.append("latex2mathml")
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import docx
|
|
15
|
+
from docx.oxml import parse_xml
|
|
16
|
+
except ImportError:
|
|
17
|
+
missing_deps.append("python-docx")
|
|
18
|
+
|
|
19
|
+
if missing_deps:
|
|
20
|
+
print(f"Error: Missing required dependencies: {', '.join(missing_deps)}", file=sys.stderr)
|
|
21
|
+
print(f"Please install them using: pip install {' '.join(missing_deps)}", file=sys.stderr)
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
# Ensure we can import math_ml2omml from the local src/ directory
|
|
25
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
26
|
+
src_path = os.path.abspath(os.path.join(script_dir, "..", "src"))
|
|
27
|
+
sys.path.insert(0, src_path)
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
import math_ml2omml
|
|
31
|
+
except ImportError:
|
|
32
|
+
print("Error: Could not import 'math_ml2omml' from src directory.", file=sys.stderr)
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main():
|
|
37
|
+
parser = argparse.ArgumentParser(
|
|
38
|
+
description="Convert Markdown containing LaTeX math equations to a Word document."
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--input", "-i",
|
|
42
|
+
default=os.path.join(script_dir, "latex_math_data.md"),
|
|
43
|
+
help="Path to the input Markdown file (default: latex_math_data.md in script directory)"
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--output", "-o",
|
|
47
|
+
default=os.path.join(script_dir, "omml_latex_data.docx"),
|
|
48
|
+
help="Path to the output Word document (default: omml_latex_data.docx in script directory)"
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--reference-linear", "-r",
|
|
52
|
+
default=os.path.join(script_dir, "linear_math_data.md"),
|
|
53
|
+
help="Optional path to reference linear math file to ensure exact metadata matching"
|
|
54
|
+
)
|
|
55
|
+
args = parser.parse_args()
|
|
56
|
+
|
|
57
|
+
md_path = args.input
|
|
58
|
+
docx_path = args.output
|
|
59
|
+
ref_path = args.reference_linear
|
|
60
|
+
|
|
61
|
+
print(f"Reading Markdown file: {md_path}")
|
|
62
|
+
if not os.path.exists(md_path):
|
|
63
|
+
print(f"Error: Markdown file does not exist at {md_path}")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
with open(md_path, "r", encoding="utf-8") as f:
|
|
67
|
+
lines = f.readlines()
|
|
68
|
+
|
|
69
|
+
# Load reference linear math equations if available to guarantee exact metadata and KCS matching
|
|
70
|
+
ref_equations = []
|
|
71
|
+
if ref_path and os.path.exists(ref_path):
|
|
72
|
+
print(f"Loading reference linear math from: {ref_path}")
|
|
73
|
+
with open(ref_path, "r", encoding="utf-8") as f:
|
|
74
|
+
ref_lines = f.readlines()
|
|
75
|
+
for line in ref_lines:
|
|
76
|
+
line = line.strip()
|
|
77
|
+
if line.startswith("**Equation:**"):
|
|
78
|
+
m = re.search(r'`(.*?)`', line)
|
|
79
|
+
if m:
|
|
80
|
+
ref_equations.append(m.group(1).strip())
|
|
81
|
+
|
|
82
|
+
categories = []
|
|
83
|
+
current_category = None
|
|
84
|
+
current_problem = None
|
|
85
|
+
|
|
86
|
+
for line in lines:
|
|
87
|
+
line = line.strip()
|
|
88
|
+
if line.startswith("## "):
|
|
89
|
+
category_name = line[3:].strip()
|
|
90
|
+
current_category = {"name": category_name, "problems": []}
|
|
91
|
+
categories.append(current_category)
|
|
92
|
+
elif line.startswith("### "):
|
|
93
|
+
title_part = line[4:].strip()
|
|
94
|
+
current_problem = {"title": title_part, "equation": "", "description": ""}
|
|
95
|
+
if current_category is not None:
|
|
96
|
+
current_category["problems"].append(current_problem)
|
|
97
|
+
elif line.startswith("**Equation:**"):
|
|
98
|
+
m = re.search(r'`(.*?)`', line)
|
|
99
|
+
if m and current_problem is not None:
|
|
100
|
+
current_problem["equation"] = m.group(1).strip()
|
|
101
|
+
elif line.startswith("**Description:**"):
|
|
102
|
+
desc_text = line[16:].strip()
|
|
103
|
+
if current_problem is not None:
|
|
104
|
+
current_problem["description"] = desc_text
|
|
105
|
+
|
|
106
|
+
print(f"Parsed {len(categories)} math categories. Initializing Word document compiler...")
|
|
107
|
+
|
|
108
|
+
# Initialize python-docx document
|
|
109
|
+
doc = docx.Document()
|
|
110
|
+
|
|
111
|
+
# Apply Standard Margins (1 inch)
|
|
112
|
+
sections = doc.sections
|
|
113
|
+
for section in sections:
|
|
114
|
+
section.top_margin = docx.shared.Inches(1)
|
|
115
|
+
section.bottom_margin = docx.shared.Inches(1)
|
|
116
|
+
section.left_margin = docx.shared.Inches(1)
|
|
117
|
+
section.right_margin = docx.shared.Inches(1)
|
|
118
|
+
|
|
119
|
+
# Document Header Title
|
|
120
|
+
doc.add_heading("University Mathematics Equation Reference Database (Compiled from LaTeX Math)", level=1)
|
|
121
|
+
doc.add_paragraph("This document serves as an empirical verification suite containing 200 university equations compiled directly from LaTeX math strings into native Word equation blocks (OMML) using the math_ml2omml package.")
|
|
122
|
+
|
|
123
|
+
problem_count = 0
|
|
124
|
+
for category in categories:
|
|
125
|
+
# Category Heading (Heading 2)
|
|
126
|
+
doc.add_heading(category["name"], level=2)
|
|
127
|
+
|
|
128
|
+
for problem in category["problems"]:
|
|
129
|
+
prob_title = problem["title"]
|
|
130
|
+
latex_str = problem["equation"]
|
|
131
|
+
description_text = problem["description"]
|
|
132
|
+
|
|
133
|
+
# Problem Title (Heading 3)
|
|
134
|
+
doc.add_heading(prob_title, level=3)
|
|
135
|
+
|
|
136
|
+
# Compile LaTeX directly to OMML using math_ml2omml (via MathML)
|
|
137
|
+
if latex_str:
|
|
138
|
+
try:
|
|
139
|
+
# Convert LaTeX to MathML
|
|
140
|
+
mathml_str = latex2mathml.converter.convert(latex_str)
|
|
141
|
+
# Convert MathML to OMML
|
|
142
|
+
omml_str = math_ml2omml.convert(mathml_str)
|
|
143
|
+
|
|
144
|
+
# Ensure OpenXML math namespaces are explicitly declared
|
|
145
|
+
if "xmlns:m=" not in omml_str:
|
|
146
|
+
omml_str = omml_str.replace(
|
|
147
|
+
"<m:oMath",
|
|
148
|
+
'<m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"',
|
|
149
|
+
1,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
omml_xml_element = parse_xml(omml_str)
|
|
153
|
+
|
|
154
|
+
# Create a paragraph for the equation
|
|
155
|
+
eq_para = doc.add_paragraph()
|
|
156
|
+
eq_para.paragraph_format.space_before = docx.shared.Pt(6)
|
|
157
|
+
eq_para.paragraph_format.space_after = docx.shared.Pt(6)
|
|
158
|
+
eq_para._p.append(omml_xml_element)
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
print(f"Warning: Failed to convert LaTeX math for '{prob_title}': {e}")
|
|
162
|
+
doc.add_paragraph(f"[Conversion Error: LaTeX Math could not be compiled. {e}]")
|
|
163
|
+
|
|
164
|
+
# Determine the Unicode linear math representation
|
|
165
|
+
linear_str = ""
|
|
166
|
+
if problem_count < len(ref_equations):
|
|
167
|
+
# Use matching equation from reference linear math database
|
|
168
|
+
linear_str = ref_equations[problem_count]
|
|
169
|
+
else:
|
|
170
|
+
# Fallback to translating dynamically
|
|
171
|
+
if latex_str:
|
|
172
|
+
try:
|
|
173
|
+
linear_str = math_ml2omml.to_linear_math(latex_str)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
print(f"Warning: Failed to translate to linear math dynamically for '{prob_title}': {e}")
|
|
176
|
+
|
|
177
|
+
# Calculate Keyboard Complexity Score (KCS)
|
|
178
|
+
kcs_score = 0.0
|
|
179
|
+
if linear_str:
|
|
180
|
+
try:
|
|
181
|
+
kcs_score = math_ml2omml.calculate_kcs(linear_str)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
print(f"Warning: Failed to calculate KCS for '{prob_title}': {e}")
|
|
184
|
+
|
|
185
|
+
# Add Accessibility Description (WCAG 2.2)
|
|
186
|
+
if description_text:
|
|
187
|
+
desc_para = doc.add_paragraph()
|
|
188
|
+
run = desc_para.add_run(f"Accessibility Description: {description_text}")
|
|
189
|
+
run.italic = True
|
|
190
|
+
run.font.color.rgb = docx.shared.RGBColor(100, 116, 139)
|
|
191
|
+
|
|
192
|
+
# Add Unicode linear math equivalent (to match other documents)
|
|
193
|
+
if linear_str:
|
|
194
|
+
lin_para = doc.add_paragraph()
|
|
195
|
+
run = lin_para.add_run(f"Unicode Linear Math: {linear_str}")
|
|
196
|
+
run.font.color.rgb = docx.shared.RGBColor(30, 58, 138)
|
|
197
|
+
run.bold = True
|
|
198
|
+
|
|
199
|
+
# Add KCS score
|
|
200
|
+
kcs_para = doc.add_paragraph()
|
|
201
|
+
run = kcs_para.add_run(f"Keyboard Complexity Score (KCS): {kcs_score:.1f}")
|
|
202
|
+
run.font.color.rgb = docx.shared.RGBColor(185, 28, 28)
|
|
203
|
+
kcs_para.paragraph_format.space_after = docx.shared.Pt(18)
|
|
204
|
+
|
|
205
|
+
problem_count += 1
|
|
206
|
+
|
|
207
|
+
print(f"Saving compiled Word document: {docx_path}")
|
|
208
|
+
doc.save(docx_path)
|
|
209
|
+
print(f"Successfully converted and compiled {problem_count} equations to {docx_path}")
|
|
210
|
+
|
|
211
|
+
if __name__ == "__main__":
|
|
212
|
+
main()
|