5 # :Copyright: © 2010 Günter Milde.
6 # Based on rst2mathml.py from the latex_math sandbox project
7 # © 2005 Jens Jørgen Mortensen
8 # :License: Released under the terms of the `2-Clause BSD license`_, in short:
10 # Copying and distribution of this file, with or without modification,
11 # are permitted in any medium without royalty provided the copyright
12 # notice and this notice are preserved.
13 # This file is offered as-is, without any warranty.
15 # .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause
18 """Convert LaTex math code into presentational MathML"""
20 # Based on the `latex_math` sandbox project by Jens Jørgen Mortensen
22 import docutils
.math
.tex2unichar
as tex2unichar
24 # TeX spacing combining
25 over
= {'acute': u
'\u00B4', # u'\u0301',
26 'bar': u
'\u00AF', # u'\u0304',
27 'breve': u
'\u02D8', # u'\u0306',
28 'check': u
'\u02C7', # u'\u030C',
29 'dot': u
'\u02D9', # u'\u0307',
30 'ddot': u
'\u00A8', # u'\u0308',
32 'grave': u
'`', # u'\u0300',
33 'hat': u
'^', # u'\u0302',
34 'mathring': u
'\u02DA', # u'\u030A',
35 'overleftrightarrow': u
'\u20e1',
36 # 'overline': # u'\u0305',
37 'tilde': u
'\u02DC', # u'\u0303',
40 Greek
= { # Capital Greek letters: (upright in TeX style)
41 'Phi':u
'\u03a6', 'Xi':u
'\u039e', 'Sigma':u
'\u03a3',
42 'Psi':u
'\u03a8', 'Delta':u
'\u0394', 'Theta':u
'\u0398',
43 'Upsilon':u
'\u03d2', 'Pi':u
'\u03a0', 'Omega':u
'\u03a9',
44 'Gamma':u
'\u0393', 'Lambda':u
'\u039b'}
46 letters
= tex2unichar
.mathalpha
48 special
= tex2unichar
.mathbin
# Binary symbols
49 special
.update(tex2unichar
.mathrel
) # Relation symbols, arrow symbols
50 special
.update(tex2unichar
.mathord
) # Miscellaneous symbols
51 special
.update(tex2unichar
.mathop
) # Variable-sized symbols
52 special
.update(tex2unichar
.mathopen
) # Braces
53 special
.update(tex2unichar
.mathclose
) # Braces
54 special
.update(tex2unichar
.mathfence
)
56 sumintprod
= ''.join([special
[symbol
] for symbol
in
57 ['sum', 'int', 'oint', 'prod']])
59 functions
= ['arccos', 'arcsin', 'arctan', 'arg', 'cos', 'cosh',
60 'cot', 'coth', 'csc', 'deg', 'det', 'dim',
61 'exp', 'gcd', 'hom', 'inf', 'ker', 'lg',
62 'lim', 'liminf', 'limsup', 'ln', 'log', 'max',
63 'min', 'Pr', 'sec', 'sin', 'sinh', 'sup',
65 'injlim', 'varinjlim', 'varlimsup',
66 'projlim', 'varliminf', 'varprojlim']
100 'B': u
'\u212C', # bernoulli function
106 'H': u
'\u210B', # hamiltonian
110 'L': u
'\u2112', # lagrangian
111 'M': u
'\u2133', # physics m-matrix
139 'o': u
'\u2134', # order of
153 negatables
= {'=': u
'\u2260',
157 # LaTeX to MathML translation stuff:
159 """Base class for MathML elements."""
162 """Required number of children"""
164 def __init__(self
, children
=None, inline
=None):
165 """math([children]) -> MathML element
167 children can be one child or a list of children."""
170 if children
is not None:
171 if type(children
) is list:
172 for child
in children
:
176 self
.append(children
)
178 if inline
is not None:
182 if hasattr(self
, 'children'):
183 return self
.__class
__.__name
__ + '(%s)' % \
184 ','.join([repr(child
) for child
in self
.children
])
186 return self
.__class
__.__name
__
189 """Room for more children?"""
191 return len(self
.children
) >= self
.nchildren
193 def append(self
, child
):
194 """append(child) -> element
196 Appends child and returns self if self is not full or first
199 assert not self
.full()
200 self
.children
.append(child
)
207 def delete_child(self
):
208 """delete_child() -> child
210 Delete last child and return it."""
212 child
= self
.children
[-1]
213 del self
.children
[-1]
219 Close element and return first non-full element."""
223 parent
= parent
.parent
227 """xml() -> xml-string"""
229 return self
.xml_start() + self
.xml_body() + self
.xml_end()
232 if not hasattr(self
, 'inline'):
233 return ['<%s>' % self
.__class
__.__name
__]
234 xmlns
= 'http://www.w3.org/1998/Math/MathML'
236 return ['<math xmlns="%s">' % xmlns
]
238 return ['<math xmlns="%s" mode="display">' % xmlns
]
241 return ['</%s>' % self
.__class
__.__name
__]
245 for child
in self
.children
:
246 xml
.extend(child
.xml())
251 return ['\n<%s>' % self
.__class
__.__name
__]
255 return ['\n<%s>' % self
.__class
__.__name
__]
257 class mtr(mrow
): pass
258 class mtd(mrow
): pass
261 """Base class for mo, mi, and mn"""
264 def __init__(self
, data
):
271 translation
= {'<': '<', '>': '>'}
273 return [self
.translation
.get(self
.data
, self
.data
)]
295 def __init__(self
, children
=None, reversed=False):
296 self
.reversed = reversed
297 math
.__init
__(self
, children
)
301 ## self.children[1:3] = self.children[2:0:-1]
302 self
.children
[1:3] = [self
.children
[2], self
.children
[1]]
303 self
.reversed = False
304 return math
.xml(self
)
307 translation
= {'\\{': '{', '\\langle': u
'\u2329',
308 '\\}': '}', '\\rangle': u
'\u232A',
310 def __init__(self
, par
):
315 open = self
.translation
.get(self
.openpar
, self
.openpar
)
316 close
= self
.translation
.get(self
.closepar
, self
.closepar
)
317 return ['<mfenced open="%s" close="%s">' % (open, close
)]
323 def __init__(self
, children
=None, nchildren
=None, **kwargs
):
324 if nchildren
is not None:
325 self
.nchildren
= nchildren
326 math
.__init
__(self
, children
)
330 return ['<mstyle '] + ['%s="%s"' % item
331 for item
in self
.attrs
.items()] + ['>']
335 def __init__(self
, children
=None, reversed=False):
336 self
.reversed = reversed
337 math
.__init
__(self
, children
)
341 self
.children
.reverse()
342 self
.reversed = False
343 return math
.xml(self
)
348 class munderover(math
):
350 def __init__(self
, children
=None):
351 math
.__init
__(self
, children
)
355 def __init__(self
, text
):
361 def parse_latex_math(string
, inline
=True):
362 """parse_latex_math(string [,inline]) -> MathML-tree
364 Returns a MathML-tree parsed from string. inline=True is for
365 inline math and inline=False is for displayed math.
367 tree is the whole tree and node is the current element."""
369 # Normalize white-space:
370 string
= ' '.join(string
.split())
374 tree
= math(node
, inline
=True)
377 tree
= math(mtable(mtr(node
)), inline
=False)
379 while len(string
) > 0:
382 skip
= 1 # number of characters consumed
387 ## print n, string, c, c2, node.__class__.__name__
392 node
= node
.append(mo(c2
))
395 node
= node
.append(mspace())
397 elif c2
== ',': # TODO: small space
398 node
= node
.append(mspace())
401 # We have a LaTeX-name:
403 while i
< n
and string
[i
].isalpha():
406 node
, skip
= handle_keyword(name
, node
, string
[i
:])
412 node
.close().close().append(row
)
416 raise SyntaxError(ur
'Syntax error: "%s%s"' % (c
, c2
))
418 node
= node
.append(mi(c
))
420 node
= node
.append(mn(c
))
421 elif c
in "+-*/=()[]|<>,.!?':;@":
422 node
= node
.append(mo(c
))
424 child
= node
.delete_child()
425 if isinstance(child
, msup
):
426 sub
= msubsup(child
.children
, reversed=True)
427 elif isinstance(child
, mo
) and child
.data
in sumintprod
:
434 child
= node
.delete_child()
435 if isinstance(child
, msub
):
436 sup
= msubsup(child
.children
)
437 elif isinstance(child
, mo
) and child
.data
in sumintprod
:
439 elif (isinstance(child
, munder
) and
440 child
.children
[0].data
in sumintprod
):
441 sup
= munderover(child
.children
)
454 node
.close().append(entry
)
457 raise SyntaxError(ur
'Illegal character: "%s"' % c
)
458 string
= string
[skip
:]
462 def handle_keyword(name
, node
, string
):
464 if len(string
) > 0 and string
[0] == ' ':
468 if not string
.startswith('{matrix}'):
469 raise SyntaxError(u
'Environment not supported! '
470 u
'Supported environment: "matrix".')
473 table
= mtable(mtr(entry
))
477 if not string
.startswith('{matrix}'):
478 raise SyntaxError(ur
'Expected "\end{matrix}"!')
480 node
= node
.close().close().close()
481 elif name
in ('text', 'mathrm'):
483 raise SyntaxError(ur
'Expected "\text{...}"!')
486 raise SyntaxError(ur
'Expected "\text{...}"!')
487 node
= node
.append(mtext(string
[1:i
]))
498 for par
in ['(', '[', '|', '\\{', '\\langle', '.']:
499 if string
.startswith(par
):
502 raise SyntaxError(u
'Missing left-brace!')
503 fenced
= mfenced(par
)
509 elif name
== 'right':
510 for par
in [')', ']', '|', '\\}', '\\rangle', '.']:
511 if string
.startswith(par
):
514 raise SyntaxError(u
'Missing right-brace!')
520 for operator
in negatables
:
521 if string
.startswith(operator
):
524 raise SyntaxError(ur
'Expected something to negate: "\not ..."!')
525 node
= node
.append(mo(negatables
[operator
]))
526 skip
+= len(operator
)
527 elif name
== 'mathbf':
528 style
= mstyle(nchildren
=1, fontweight
='bold')
531 elif name
== 'mathbb':
532 if string
[0] != '{' or not string
[1].isupper() or string
[2] != '}':
533 raise SyntaxError(ur
'Expected something like "\mathbb{A}"!')
534 node
= node
.append(mi(mathbb
[string
[1]]))
536 elif name
in ('mathscr', 'mathcal'):
537 if string
[0] != '{' or string
[2] != '}':
538 raise SyntaxError(ur
'Expected something like "\mathscr{A}"!')
539 node
= node
.append(mi(mathscr
[string
[1]]))
541 elif name
== 'colon': # "normal" colon, not binary operator
542 node
= node
.append(mo(':')) # TODO: add ``lspace="0pt"``
543 elif name
in Greek
: # Greek capitals (upright in "TeX style")
544 node
= node
.append(mo(Greek
[name
]))
545 # TODO: "ISO style" sets them italic. Could we use a class argument
546 # to enable styling via CSS?
547 elif name
in letters
:
548 node
= node
.append(mi(letters
[name
]))
549 elif name
in special
:
550 node
= node
.append(mo(special
[name
]))
551 elif name
in functions
:
552 node
= node
.append(mo(name
))
554 ovr
= mover(mo(over
[name
]), reversed=True)
558 raise SyntaxError(u
'Unknown LaTeX command: ' + name
)