"""
XML processing utilities for MathML and OMML handling.
This module provides helper functions for working with XML/MathML/OMML documents,
including namespace handling, text extraction, and HTML entity decoding.
"""
import re
import html
from typing import List
from lxml import etree as ET
def local_name(tag: str) -> str:
"""Extract local tag name without namespace.
Args:
tag: XML tag name, possibly with namespace prefix like "{http://...}tagname"
Returns:
Local tag name without namespace
Example:
>>> local_name("{http://www.w3.org/1998/Math/MathML}mi")
'mi'
>>> local_name("mi")
'mi'
"""
if "}" in tag:
return tag.split("}", 1)[1]
return tag
def text_content(node: ET._Element) -> str:
"""Flatten text under a node (best-effort).
NOTE: Does NOT strip() to preserve space-only content like NBSP and regular spaces.
Spaces are semantically meaningful in mathematical notation.
However, DOES remove invisible formatting characters like function application (\u2061)
and normalizes whitespace (converts newlines/tabs to single spaces).
Args:
node: XML element to extract text from
Returns:
Concatenated text content from node and all its children
Example:
>>> from lxml import etree as ET
>>> node = ET.fromstring('sin')
>>> text_content(node)
'sin'
"""
texts: List[str] = []
if node.text:
texts.append(node.text)
for child in node:
texts.append(text_content(child))
if child.tail:
texts.append(child.tail)
result = "".join(texts)
# Remove invisible formatting characters that interfere with conversion
# but preserve meaningful spaces (regular space, nbsp)
invisible_chars = [
'\u2061', # FUNCTION APPLICATION (causes sin → \mathrm{sin})
'\u2062', # INVISIBLE TIMES
'\u2063', # INVISIBLE SEPARATOR
'\u2064', # INVISIBLE PLUS
'\uFEFF', # ZERO WIDTH NO-BREAK SPACE
'\u200B', # ZERO WIDTH SPACE
'\u200C', # ZERO WIDTH NON-JOINER
'\u200D', # ZERO WIDTH JOINER
]
for char in invisible_chars:
result = result.replace(char, '')
# Normalize whitespace: Remove newlines and tabs completely
# This fixes both multiline fractions like \dfrac{\n 2\n}
# AND prevents formatting whitespace from JSON/XML from being converted to LaTeX spacing
# Preserve NBSP (\xa0) as it's semantically meaningful
result = re.sub(r'[\n\r\t]+', '', result)
# Collapse multiple consecutive regular spaces into one
# This prevents " 2 " → should be " 2 " or just "2"
# But don't collapse NBSP (\xa0) as it's semantically meaningful
result = re.sub(r' +', ' ', result)
# Strip leading/trailing spaces that come from XML formatting
# These are not semantically meaningful - they're just from pretty-printed XML
# True semantic spaces (like in operators) are preserved within the content
result = result.strip()
return result
def decode_html_entities(text: str) -> str:
"""Decode HTML entities in text, particularly " from web page copies.
IMPORTANT: This function is designed for HTML content, NOT for XML/MathML!
DO NOT use this on XML/MathML strings before XML parsing, as it will break
valid XML numeric character references like < which XML parsers
handle natively.
Args:
text: Text possibly containing HTML entities
Returns:
Text with HTML named entities decoded (numeric entities preserved for XML)
Example:
>>> decode_html_entities('<math> & "test"')
'