diff options
| author | milde <milde@929543f6-e4f2-0310-98a6-ba3bd3dd1d04> | 2011-04-13 22:13:55 +0000 |
|---|---|---|
| committer | milde <milde@929543f6-e4f2-0310-98a6-ba3bd3dd1d04> | 2011-04-13 22:13:55 +0000 |
| commit | 254396fb8e12348452e4cbaf4fe0f9c92ceae585 (patch) | |
| tree | 0cb71319fde1d2431883c981ab6d04e132bbe833 /docutils | |
| parent | 81342e2ec4bd203e386a60c2e3a7a392bcf97b1d (diff) | |
| download | docutils-254396fb8e12348452e4cbaf4fe0f9c92ceae585.tar.gz | |
Math support update
* new version of math2html (elyxer) by Alex Fernandez
(bugfixes, improved rendering)
* small fixes to latex2mathml
* accept Unicode characters in the input
* updated test file, new test cases html_math and mathml_math
git-svn-id: http://svn.code.sf.net/p/docutils/code/trunk/docutils@7008 929543f6-e4f2-0310-98a6-ba3bd3dd1d04
Diffstat (limited to 'docutils')
| -rw-r--r-- | docutils/math/latex2mathml.py | 35 | ||||
| -rw-r--r-- | docutils/math/math2html.py | 1180 | ||||
| -rw-r--r-- | docutils/writers/html4css1/__init__.py | 45 | ||||
| -rw-r--r-- | docutils/writers/html4css1/math.css | 5 | ||||
| -rw-r--r-- | docutils/writers/latex2e/__init__.py | 54 |
5 files changed, 749 insertions, 570 deletions
diff --git a/docutils/math/latex2mathml.py b/docutils/math/latex2mathml.py index 7be124e93..53b329219 100644 --- a/docutils/math/latex2mathml.py +++ b/docutils/math/latex2mathml.py @@ -293,6 +293,7 @@ special = { 'Downarrow':u'\u21d3', 'leftrightarrow':u'\u2194', 'Longrightarrow':u'\u27f9', 'swarrow':u'\u2199', 'hookrightarrow':u'\u21aa', 'Rightarrow':u'\u21d2', + 'to':u'\u2192', # Miscellaneous symbols: 'infty':u'\u221e', 'surd':u'\u221a', 'partial':u'\u2202', 'ddots':u'\u22f1', 'exists':u'\u2203', @@ -362,6 +363,9 @@ def parse_latex_math(string, inline=True): elif c2 == ' ': node = node.append(mspace()) skip = 2 + elif c2 == ',': # TODO: small space + node = node.append(mspace()) + skip = 2 elif c2.isalpha(): # We have a LaTeX-name: i = 2 @@ -378,12 +382,12 @@ def parse_latex_math(string, inline=True): node = entry skip = 2 else: - raise SyntaxError(r'Syntax error: "%s%s"' % (c, c2)) + raise SyntaxError(ur'Syntax error: "%s%s"' % (c, c2)) elif c.isalpha(): node = node.append(mi(c)) elif c.isdigit(): node = node.append(mn(c)) - elif c in "+-/()[]|=<>,.!':": + elif c in "+-*/=()[]|<>,.!?':;@": node = node.append(mo(c)) elif c == '_': child = node.delete_child() @@ -419,7 +423,7 @@ def parse_latex_math(string, inline=True): node.close().append(entry) node = entry else: - raise SyntaxError(r'Illegal character: "%s"' % c) + raise SyntaxError(ur'Illegal character: "%s"' % c) string = string[skip:] return tree @@ -520,7 +524,8 @@ def handle_keyword(name, node, string): skip = 1 if name == 'begin': if not string.startswith('{matrix}'): - raise SyntaxError(r'Expected "\begin{matrix}"!') + raise SyntaxError(u'Environment not supported! ' + u'Supported environment: "matrix".') skip += 8 entry = mtd() table = mtable(mtr(entry)) @@ -528,15 +533,15 @@ def handle_keyword(name, node, string): node = entry elif name == 'end': if not string.startswith('{matrix}'): - raise SyntaxError(r'Expected "\end{matrix}"!') + raise SyntaxError(ur'Expected "\end{matrix}"!') skip += 8 node = node.close().close().close() - elif name == 'text': + elif name in ('text', 'mathrm'): if string[0] != '{': - raise SyntaxError(r'Expected "\text{...}"!') + raise SyntaxError(ur'Expected "\text{...}"!') i = string.find('}') if i == -1: - raise SyntaxError(r'Expected "\text{...}"!') + raise SyntaxError(ur'Expected "\text{...}"!') node = node.append(mtext(string[1:i])) skip += i + 1 elif name == 'sqrt': @@ -552,7 +557,7 @@ def handle_keyword(name, node, string): if string.startswith(par): break else: - raise SyntaxError(r'Missing left-brace!') + raise SyntaxError(u'Missing left-brace!') fenced = mfenced(par) node.append(fenced) row = mrow() @@ -564,7 +569,7 @@ def handle_keyword(name, node, string): if string.startswith(par): break else: - raise SyntaxError(r'Missing right-brace!') + raise SyntaxError(u'Missing right-brace!') node = node.close() node.closepar = par node = node.close() @@ -574,7 +579,7 @@ def handle_keyword(name, node, string): if string.startswith(operator): break else: - raise SyntaxError(r'Expected something to negate: "\not ..."!') + raise SyntaxError(ur'Expected something to negate: "\not ..."!') node = node.append(mo(negatables[operator])) skip += len(operator) elif name == 'mathbf': @@ -583,14 +588,16 @@ def handle_keyword(name, node, string): node = style elif name == 'mathbb': if string[0] != '{' or not string[1].isupper() or string[2] != '}': - raise SyntaxError(r'Expected something like "\mathbb{A}"!') + raise SyntaxError(ur'Expected something like "\mathbb{A}"!') node = node.append(mi(mathbb[string[1]])) skip += 3 elif name in ('mathscr', 'mathcal'): if string[0] != '{' or string[2] != '}': - raise SyntaxError(r'Expected something like "\mathscr{A}"!') + raise SyntaxError(ur'Expected something like "\mathscr{A}"!') node = node.append(mi(mathscr[string[1]])) skip += 3 + elif name == 'colon': # "normal" colon, not binary operator + node = node.append(mo(':')) # TODO: add ``lspace="0pt"`` elif name in letters: node = node.append(mi(letters[name])) elif name in Greek: @@ -606,6 +613,6 @@ def handle_keyword(name, node, string): node.append(ovr) node = ovr else: - raise SyntaxError(r'Unknown LaTeX command: ' + name) + raise SyntaxError(u'Unknown LaTeX command: ' + name) return node, skip diff --git a/docutils/math/math2html.py b/docutils/math/math2html.py index c37b296c7..272d20fad 100644 --- a/docutils/math/math2html.py +++ b/docutils/math/math2html.py @@ -69,77 +69,6 @@ class Trace(object): - - - - - - - - -class Cloner(object): - "An object used to clone other objects." - - def clone(cls, original): - "Return an exact copy of an object." - "The original object must have an empty constructor." - return cls.create(original.__class__) - - def create(cls, type): - "Create an object of a given class." - clone = type.__new__(type) - clone.__init__() - return clone - - clone = classmethod(clone) - create = classmethod(create) - -class ContainerExtractor(object): - "A class to extract certain containers." - - def __init__(self, config): - "The config parameter is a map containing three lists: allowed, copied and extracted." - "Each of the three is a list of class names for containers." - "Allowed containers are included as is into the result." - "Cloned containers are cloned and placed into the result." - "Extracted containers are looked into." - "All other containers are silently ignored." - self.allowed = config['allowed'] - self.cloned = config['cloned'] - self.extracted = config['extracted'] - - def extract(self, container): - "Extract a group of selected containers from elyxer.a container." - list = [] - locate = lambda c: c.__class__.__name__ in self.allowed + self.cloned - recursive = lambda c: c.__class__.__name__ in self.extracted - process = lambda c: self.process(c, list) - container.recursivesearch(locate, recursive, process) - return list - - def process(self, container, list): - "Add allowed containers, clone cloned containers and add the clone." - name = container.__class__.__name__ - if name in self.allowed: - list.append(container) - elif name in self.cloned: - list.append(self.safeclone(container)) - else: - Trace.error('Unknown container class ' + name) - - def safeclone(self, container): - "Return a new container with contents only in a safe list, recursively." - clone = Cloner.clone(container) - clone.output = container.output - clone.contents = self.extract(container) - return clone - - - - - - - import os.path import sys @@ -244,7 +173,7 @@ class ContainerConfig(object): extracttext = { u'allowed':[u'StringContainer',u'Constant',u'FormulaConstant',], u'cloned':[u'',], - u'extracted':[u'PlainLayout',u'TaggedText',u'Align',u'Caption',u'TextFamily',u'EmphaticText',u'VersalitasText',u'BarredText',u'SizeText',u'ColorText',u'LangLine',u'Formula',u'Bracket',u'RawText',u'BibTag',u'FormulaNumber',u'AlphaCommand',u'EmptyCommand',u'OneParamFunction',u'SymbolFunction',u'TextFunction',u'FontFunction',u'CombiningFunction',u'DecoratingFunction',u'FormulaSymbol',u'BracketCommand',], + u'extracted':[u'PlainLayout',u'TaggedText',u'Align',u'Caption',u'TextFamily',u'EmphaticText',u'VersalitasText',u'BarredText',u'SizeText',u'ColorText',u'LangLine',u'Formula',u'Bracket',u'RawText',u'BibTag',u'FormulaNumber',u'AlphaCommand',u'EmptyCommand',u'OneParamFunction',u'SymbolFunction',u'TextFunction',u'FontFunction',u'CombiningFunction',u'DecoratingFunction',u'FormulaSymbol',u'BracketCommand',u'TeXCode',], } startendings = { @@ -388,18 +317,19 @@ class FormulaConfig(object): "Configuration class from elyxer.config file" alphacommands = { - u'\\AA':u'Å', u'\\AE':u'Æ', u'\\DH':u'Ð', u'\\L':u'Ł', u'\\O':u'Ø', - u'\\OE':u'Œ', u'\\TH':u'Þ', u'\\aa':u'å', u'\\ae':u'æ', u'\\alpha':u'α', - u'\\beta':u'β', u'\\delta':u'δ', u'\\dh':u'ð', u'\\epsilon':u'ϵ', - u'\\eta':u'η', u'\\gamma':u'γ', u'\\i':u'ı', u'\\imath':u'ı', - u'\\iota':u'ι', u'\\j':u'ȷ', u'\\jmath':u'ȷ', u'\\kappa':u'κ', - u'\\l':u'ł', u'\\lambda':u'λ', u'\\mu':u'μ', u'\\nu':u'ν', u'\\o':u'ø', - u'\\oe':u'œ', u'\\omega':u'ω', u'\\phi':u'φ', u'\\pi':u'π', - u'\\psi':u'ψ', u'\\rho':u'ρ', u'\\sigma':u'σ', u'\\ss':u'ß', - u'\\tau':u'τ', u'\\textcrh':u'ħ', u'\\th':u'þ', u'\\theta':u'θ', - u'\\upsilon':u'υ', u'\\varDelta':u'∆', u'\\varGamma':u'Γ', - u'\\varLambda':u'Λ', u'\\varOmega':u'Ω', u'\\varPhi':u'Φ', - u'\\varPi':u'Π', u'\\varPsi':u'Ψ', u'\\varSigma':u'Σ', + u'\\AA':u'Å', u'\\AE':u'Æ', + u'\\AmS':u'<span class="versalitas">AmS</span>', u'\\DH':u'Ð', + u'\\L':u'Ł', u'\\O':u'Ø', u'\\OE':u'Œ', u'\\TH':u'Þ', u'\\aa':u'å', + u'\\ae':u'æ', u'\\alpha':u'α', u'\\beta':u'β', u'\\delta':u'δ', + u'\\dh':u'ð', u'\\epsilon':u'ϵ', u'\\eta':u'η', u'\\gamma':u'γ', + u'\\i':u'ı', u'\\imath':u'ı', u'\\iota':u'ι', u'\\j':u'ȷ', + u'\\jmath':u'ȷ', u'\\kappa':u'κ', u'\\l':u'ł', u'\\lambda':u'λ', + u'\\mu':u'μ', u'\\nu':u'ν', u'\\o':u'ø', u'\\oe':u'œ', u'\\omega':u'ω', + u'\\phi':u'φ', u'\\pi':u'π', u'\\psi':u'ψ', u'\\rho':u'ρ', + u'\\sigma':u'σ', u'\\ss':u'ß', u'\\tau':u'τ', u'\\textcrh':u'ħ', + u'\\th':u'þ', u'\\theta':u'θ', u'\\upsilon':u'υ', u'\\varDelta':u'∆', + u'\\varGamma':u'Γ', u'\\varLambda':u'Λ', u'\\varOmega':u'Ω', + u'\\varPhi':u'Φ', u'\\varPi':u'Π', u'\\varPsi':u'Ψ', u'\\varSigma':u'Σ', u'\\varTheta':u'Θ', u'\\varUpsilon':u'Υ', u'\\varXi':u'Ξ', u'\\varepsilon':u'ε', u'\\varkappa':u'ϰ', u'\\varphi':u'φ', u'\\varpi':u'ϖ', u'\\varrho':u'ϱ', u'\\varsigma':u'ς', @@ -413,7 +343,8 @@ class FormulaConfig(object): bigbrackets = { u'(':[u'⎛',u'⎜',u'⎝',], u')':[u'⎞',u'⎟',u'⎠',], u'[':[u'⎡',u'⎢',u'⎣',], - u']':[u'⎤',u'⎥',u'⎦',], + u']':[u'⎤',u'⎥',u'⎦',], u'{':[u'⎧',u'⎪',u'⎨',u'⎩',], u'|':[u'|',], + u'}':[u'⎫',u'⎪',u'⎬',u'⎭',], u'∥':[u'∥',], } bigsymbols = { @@ -432,8 +363,9 @@ class FormulaConfig(object): u'\\bar':u'̄', u'\\breve':u'̆', u'\\c':u'̧', u'\\check':u'̌', u'\\dddot':u'⃛', u'\\ddot':u'̈', u'\\dot':u'̇', u'\\grave':u'̀', u'\\hat':u'̂', u'\\mathring':u'̊', u'\\overleftarrow':u'⃖', - u'\\overrightarrow':u'⃗', u'\\r':u'̥', u'\\s':u'̩', - u'\\textsubring':u'̥', u'\\tilde':u'̃', u'\\vec':u'⃗', u'\\~':u'̃', + u'\\overrightarrow':u'⃗', u'\\r':u'̊', u'\\s':u'̩', + u'\\textcircled':u'⃝', u'\\textsubring':u'̥', u'\\tilde':u'̃', + u'\\vec':u'⃗', u'\\~':u'̃', } commands = { @@ -482,14 +414,13 @@ class FormulaConfig(object): u'\\dagger':u'†', u'\\daleth':u'ℸ', u'\\dashleftarrow':u'⇠', u'\\dashv':u'⊣', u'\\ddag':u'‡', u'\\ddagger':u'‡', u'\\ddots':u'⋱', u'\\deg':u'deg', u'\\det':u'det', u'\\diagdown':u'╲', u'\\diagup':u'╱', - u'\\diamond':u'◇', u'\\diamondsuit':u'♦', u'\\dim':u'dim', - u'\\displaystyle':u'', u'\\div':u'÷', u'\\divideontimes':u'⋇', - u'\\dotdiv':u'∸', u'\\doteq':u'≐', u'\\doteqdot':u'≑', u'\\dotplus':u'∔', - u'\\dots':u'…', u'\\doublebarwedge':u'⌆', u'\\downarrow':u'↓', - u'\\downdownarrows':u'⇊', u'\\downharpoonleft':u'⇃', - u'\\downharpoonright':u'⇂', u'\\earth':u'♁', u'\\ell':u'ℓ', - u'\\emptyset':u'∅', u'\\eqcirc':u'≖', u'\\eqcolon':u'≕', u'\\eqsim':u'≂', - u'\\euro':u'€', u'\\exists':u'∃', u'\\exp':u'exp', + u'\\diamond':u'◇', u'\\diamondsuit':u'♦', u'\\dim':u'dim', u'\\div':u'÷', + u'\\divideontimes':u'⋇', u'\\dotdiv':u'∸', u'\\doteq':u'≐', + u'\\doteqdot':u'≑', u'\\dotplus':u'∔', u'\\dots':u'…', + u'\\doublebarwedge':u'⌆', u'\\downarrow':u'↓', u'\\downdownarrows':u'⇊', + u'\\downharpoonleft':u'⇃', u'\\downharpoonright':u'⇂', u'\\earth':u'♁', + u'\\ell':u'ℓ', u'\\emptyset':u'∅', u'\\eqcirc':u'≖', u'\\eqcolon':u'≕', + u'\\eqsim':u'≂', u'\\euro':u'€', u'\\exists':u'∃', u'\\exp':u'exp', u'\\fallingdotseq':u'≒', u'\\female':u'♀', u'\\flat':u'♭', u'\\forall':u'∀', u'\\frown':u'⌢', u'\\frownie':u'☹', u'\\gcd':u'gcd', u'\\gemini':u'♊', u'\\geq)':u'≥', u'\\geqq':u'≧', u'\\geqslant':u'≥', @@ -533,10 +464,10 @@ class FormulaConfig(object): u'\\ngeqslant':u'≱', u'\\ngtr':u'≯', u'\\ngtrless':u'≹', u'\\ni':u'∋', u'\\ni)':u'∋', u'\\nleftarrow':u'↚', u'\\nleftrightarrow':u'↮', u'\\nleqslant':u'≰', u'\\nless':u'≮', u'\\nlessgtr':u'≸', u'\\nmid':u'∤', - u'\\nonumber':u'', u'\\not':u'¬', u'\\not<':u'≮', u'\\not=':u'≠', - u'\\not>':u'≯', u'\\notbackslash':u'⍀', u'\\notin':u'∉', u'\\notni':u'∌', - u'\\notslash':u'⌿', u'\\nparallel':u'∦', u'\\nprec':u'⊀', - u'\\nrightarrow':u'↛', u'\\nsim':u'≁', u'\\nsimeq':u'≄', + u'\\nolimits':u'', u'\\nonumber':u'', u'\\not':u'¬', u'\\not<':u'≮', + u'\\not=':u'≠', u'\\not>':u'≯', u'\\notbackslash':u'⍀', u'\\notin':u'∉', + u'\\notni':u'∌', u'\\notslash':u'⌿', u'\\nparallel':u'∦', + u'\\nprec':u'⊀', u'\\nrightarrow':u'↛', u'\\nsim':u'≁', u'\\nsimeq':u'≄', u'\\nsqsubset':u'⊏̸', u'\\nsubseteq':u'⊈', u'\\nsucc':u'⊁', u'\\nsucccurlyeq':u'⋡', u'\\nsupset':u'⊅', u'\\nsupseteq':u'⊉', u'\\ntriangleleft':u'⋪', u'\\ntrianglelefteq':u'⋬', @@ -563,23 +494,22 @@ class FormulaConfig(object): u'\\rightrightarrows':u'⇉', u'\\rightrightharpoons':u'⥤', u'\\rightthreetimes':u'⋌', u'\\risingdotseq':u'≓', u'\\rtimes':u'⋊', u'\\sagittarius':u'♐', u'\\saturn':u'♄', u'\\scorpio':u'♏', - u'\\scriptscriptstyle':u'', u'\\scriptstyle':u'', u'\\searrow':u'↘', - u'\\sec':u'sec', u'\\setminus':u'∖', u'\\sharp':u'♯', u'\\simeq':u'≃', - u'\\sin':u'sin', u'\\sinh':u'sinh', u'\\slash':u'∕', u'\\smile':u'⌣', - u'\\smiley':u'☺', u'\\spadesuit':u'♠', u'\\sphericalangle':u'∢', - u'\\sqcap':u'⊓', u'\\sqcup':u'⊔', u'\\sqsubset':u'⊏', - u'\\sqsubseteq':u'⊑', u'\\sqsupset':u'⊐', u'\\sqsupseteq':u'⊒', - u'\\square':u'□', u'\\star':u'⋆', u'\\subseteqq':u'⫅', - u'\\subsetneqq':u'⫋', u'\\succ':u'≻', u'\\succcurlyeq':u'≽', - u'\\succeq':u'≽', u'\\succnsim':u'⋩', u'\\succsim':u'≿', u'\\sun':u'☼', - u'\\sup':u'sup', u'\\supseteqq':u'⫆', u'\\supsetneqq':u'⫌', - u'\\surd':u'√', u'\\swarrow':u'↙', u'\\tan':u'tan', u'\\tanh':u'tanh', - u'\\taurus':u'♉', u'\\textasciicircum':u'^', u'\\textasciitilde':u'~', - u'\\textbackslash':u'\\', u'\\textendash':u'—', u'\\textgreater':u'>', - u'\\textless':u'<', u'\\textordfeminine':u'ª', + u'\\searrow':u'↘', u'\\sec':u'sec', u'\\setminus':u'∖', u'\\sharp':u'♯', + u'\\simeq':u'≃', u'\\sin':u'sin', u'\\sinh':u'sinh', u'\\slash':u'∕', + u'\\smile':u'⌣', u'\\smiley':u'☺', u'\\spadesuit':u'♠', + u'\\sphericalangle':u'∢', u'\\sqcap':u'⊓', u'\\sqcup':u'⊔', + u'\\sqsubset':u'⊏', u'\\sqsubseteq':u'⊑', u'\\sqsupset':u'⊐', + u'\\sqsupseteq':u'⊒', u'\\square':u'□', u'\\star':u'⋆', + u'\\subseteqq':u'⫅', u'\\subsetneqq':u'⫋', u'\\succ':u'≻', + u'\\succcurlyeq':u'≽', u'\\succeq':u'≽', u'\\succnsim':u'⋩', + u'\\succsim':u'≿', u'\\sun':u'☼', u'\\sup':u'sup', u'\\supseteqq':u'⫆', + u'\\supsetneqq':u'⫌', u'\\surd':u'√', u'\\swarrow':u'↙', u'\\tan':u'tan', + u'\\tanh':u'tanh', u'\\taurus':u'♉', u'\\textasciicircum':u'^', + u'\\textasciitilde':u'~', u'\\textbackslash':u'\\', u'\\textendash':u'—', + u'\\textgreater':u'>', u'\\textless':u'<', u'\\textordfeminine':u'ª', u'\\textordmasculine':u'º', u'\\textquotedblleft':u'“', u'\\textquotedblright':u'”', u'\\textregistered':u'®', - u'\\textstyle':u'', u'\\texttrademark':u'™', u'\\therefore':u'∴', + u'\\texttrademark':u'™', u'\\textvisiblespace':u' ', u'\\therefore':u'∴', u'\\top':u'⊤', u'\\triangle':u'△', u'\\triangleleft':u'⊲', u'\\trianglelefteq':u'⊴', u'\\triangleq':u'≜', u'\\triangleright':u'▷', u'\\trianglerighteq':u'⊵', u'\\twoheadleftarrow':u'↞', @@ -603,11 +533,6 @@ class FormulaConfig(object): u'\\overleftarrow':u'⟵', u'\\overrightarrow':u'⟶', u'\\widehat':u'^', } - definingfunctions = { - u'\\newcommand':u'[$n!][$1][$2][$3][$4][$5][$6][$7][$8][$9]{$d}', - u'\\renewcommand':u'[$n!][$1][$2][$3][$4][$5][$6][$7][$8][$9]{$d}', - } - endings = { u'bracket':u'}', u'complex':u'\\]', u'endafter':u'}', u'endbefore':u'\\end{', u'squarebracket':u']', @@ -627,17 +552,20 @@ class FormulaConfig(object): u'\\mathbb{O}':u'𝕆', u'\\mathbb{P}':u'ℙ', u'\\mathbb{Q}':u'ℚ', u'\\mathbb{R}':u'ℝ', u'\\mathbb{S}':u'𝕊', u'\\mathbb{T}':u'𝕋', u'\\mathbb{W}':u'𝕎', u'\\mathbb{Z}':u'ℤ', u'\\mathbf':u'b', - u'\\mathcal':u'span class="script"', - u'\\mathfrak':u'span class="fraktur"', u'\\mathfrak{C}':u'ℭ', - u'\\mathfrak{F}':u'𝔉', u'\\mathfrak{H}':u'ℌ', u'\\mathfrak{I}':u'ℑ', - u'\\mathfrak{R}':u'ℜ', u'\\mathfrak{Z}':u'ℨ', u'\\mathit':u'i', - u'\\mathring{A}':u'Å', u'\\mathring{U}':u'Ů', u'\\mathring{a}':u'å', - u'\\mathring{u}':u'ů', u'\\mathring{w}':u'ẘ', u'\\mathring{y}':u'ẙ', - u'\\mathrm':u'span class="mathrm"', u'\\mathscr':u'span class="script"', - u'\\mathscr{B}':u'ℬ', u'\\mathscr{E}':u'ℰ', u'\\mathscr{F}':u'ℱ', - u'\\mathscr{H}':u'ℋ', u'\\mathscr{I}':u'ℐ', u'\\mathscr{L}':u'ℒ', - u'\\mathscr{M}':u'ℳ', u'\\mathscr{R}':u'ℛ', - u'\\mathsf':u'span class="mathsf"', u'\\mathtt':u'tt', + u'\\mathcal':u'span class="scriptfont"', u'\\mathcal{B}':u'ℬ', + u'\\mathcal{E}':u'ℰ', u'\\mathcal{F}':u'ℱ', u'\\mathcal{H}':u'ℋ', + u'\\mathcal{I}':u'ℐ', u'\\mathcal{L}':u'ℒ', u'\\mathcal{M}':u'ℳ', + u'\\mathcal{R}':u'ℛ', u'\\mathfrak':u'span class="fraktur"', + u'\\mathfrak{C}':u'ℭ', u'\\mathfrak{F}':u'𝔉', u'\\mathfrak{H}':u'ℌ', + u'\\mathfrak{I}':u'ℑ', u'\\mathfrak{R}':u'ℜ', u'\\mathfrak{Z}':u'ℨ', + u'\\mathit':u'i', u'\\mathring{A}':u'Å', u'\\mathring{U}':u'Ů', + u'\\mathring{a}':u'å', u'\\mathring{u}':u'ů', u'\\mathring{w}':u'ẘ', + u'\\mathring{y}':u'ẙ', u'\\mathrm':u'span class="mathrm"', + u'\\mathscr':u'span class="scriptfont"', u'\\mathscr{B}':u'ℬ', + u'\\mathscr{E}':u'ℰ', u'\\mathscr{F}':u'ℱ', u'\\mathscr{H}':u'ℋ', + u'\\mathscr{I}':u'ℐ', u'\\mathscr{L}':u'ℒ', u'\\mathscr{M}':u'ℳ', + u'\\mathscr{R}':u'ℛ', u'\\mathsf':u'span class="mathsf"', + u'\\mathtt':u'tt', } hybridfunctions = { @@ -649,7 +577,10 @@ class FormulaConfig(object): u'\\colorbox':[u'{$p!}{$1}',u'f0{$1}',u'span class="colorbox" style="background: $p;"',], u'\\dbinom':[u'{$1}{$2}',u'(f0{f1{f2{$1}}f1{f2{ }}f1{f2{$2}}})',u'span class="binomial"',u'span class="binomrow"',u'span class="binomcell"',], u'\\dfrac':[u'{$1}{$2}',u'f0{f3{(}f1{$1}f3{)/(}f2{$2}f3{)}}',u'span class="fullfraction"',u'span class="numerator"',u'span class="denominator"',u'span class="ignored"',], + u'\\displaystyle':[u'{$1}',u'f0{$1}',u'span class="displaystyle"',], u'\\fbox':[u'{$1}',u'f0{$1}',u'span class="fbox"',], + u'\\fboxrule':[u'{$p!}',u'f0{}',u'ignored',], + u'\\fboxsep':[u'{$p!}',u'f0{}',u'ignored',], u'\\fcolorbox':[u'{$p!}{$q!}{$1}',u'f0{$1}',u'span class="boxed" style="border-color: $p; background: $q;"',], u'\\frac':[u'{$1}{$2}',u'f0{f3{(}f1{$1}f3{)/(}f2{$2}f3{)}}',u'span class="fraction"',u'span class="numerator"',u'span class="denominator"',u'span class="ignored"',], u'\\framebox':[u'[$p!][$q!]{$1}',u'f0{$1}',u'span class="framebox align-$q" style="width: $p;"',], @@ -657,12 +588,17 @@ class FormulaConfig(object): u'\\hspace':[u'{$p!}',u'f0{ }',u'span class="hspace" style="width: $p;"',], u'\\leftroot':[u'{$p!}',u'f0{ }',u'span class="leftroot" style="width: $p;px"',], u'\\nicefrac':[u'{$1}{$2}',u'f0{f1{$1}⁄f2{$2}}',u'span class="fraction"',u'sup class="numerator"',u'sub class="denominator"',u'span class="ignored"',], - u'\\raisebox':[u'{$p!}{$1}',u'f0{$1}',u'span class="raisebox" style="vertical-align: $p;"',], + u'\\parbox':[u'[$p!]{$w!}{$1}',u'f0{1}',u'div class="Boxed" style="width: $w;"',], + u'\\raisebox':[u'{$p!}{$1}',u'f0{$1.font}',u'span class="raisebox" style="vertical-align: $p;"',], u'\\renewenvironment':[u'{$1!}{$2!}{$3!}',u'',], + u'\\rule':[u'[$v!]{$w!}{$h!}',u'f0/',u'hr class="line" style="width: $w; height: $h;"',], + u'\\scriptscriptstyle':[u'{$1}',u'f0{$1}',u'span class="scriptscriptstyle"',], + u'\\scriptstyle':[u'{$1}',u'f0{$1}',u'span class="scriptstyle"',], u'\\sqrt':[u'[$0]{$1}',u'f0{f1{$0}f2{√}f4{(}f3{$1}f4{)}}',u'span class="sqrt"',u'sup class="root"',u'span class="radical"',u'span class="root"',u'span class="ignored"',], u'\\stackrel':[u'{$1}{$2}',u'f0{f1{$1}f2{$2}}',u'span class="stackrel"',u'span class="upstackrel"',u'span class="downstackrel"',], u'\\tbinom':[u'{$1}{$2}',u'(f0{f1{f2{$1}}f1{f2{ }}f1{f2{$2}}})',u'span class="binomial"',u'span class="binomrow"',u'span class="binomcell"',], u'\\textcolor':[u'{$p!}{$1}',u'f0{$1}',u'span style="color: $p;"',], + u'\\textstyle':[u'{$1}',u'f0{$1}',u'span class="textstyle"',], u'\\unit':[u'[$0]{$1}',u'$0f0{$1.font}',u'span class="unit"',], u'\\unitfrac':[u'[$0]{$1}{$2}',u'$0f0{f1{$1.font}⁄f2{$2.font}}',u'span class="fraction"',u'sup class="unit"',u'sub class="unit"',], u'\\uproot':[u'{$p!}',u'f0{ }',u'span class="uproot" style="width: $p;px"',], @@ -680,13 +616,19 @@ class FormulaConfig(object): } limitcommands = { - u'\\int':u'∫', u'\\intop':u'∫', u'\\lim':u'lim', - u'\\prod':u'<span class="bigsymbol">∏</span>', - u'\\smallint':u'<span class="symbol">∫</span>', u'\\sum':u'∑', + u'\\int':u'∫', u'\\intop':u'∫', u'\\lim':u'lim', u'\\prod':u'∏', + u'\\smallint':u'∫', u'\\sum':u'∑', + } + + misccommands = { + u'\\limits':u'LimitPreviousCommand', u'\\newcommand':u'MacroDefinition', + u'\\renewcommand':u'MacroDefinition', + u'\\setcounter':u'SetCounterFunction', u'\\tag':u'FormulaTag', + u'\\tag*':u'FormulaTag', } modified = { - u'\n':u'', u' ':u'', u'$':u'', u'&':u' ', u'\'':u'’', u'+':u' + ', + u'\n':u'', u' ':u'', u'$':u'', u'&':u' ', u'\'':u'’', u'+':u' + ', u',':u', ', u'-':u' − ', u'/':u' ⁄ ', u'<':u' < ', u'=':u' = ', u'>':u' > ', u'@':u'', u'~':u'', } @@ -705,10 +647,6 @@ class FormulaConfig(object): u'\\vphantom':u'span class="phantom"', } - preamblefunctions = { - u'\\setcounter':[u'{$p!}{$n!}',u'setcounter',], - } - spacedcommands = { u'\\Leftrightarrow':u'⇔', u'\\Rightarrow':u'⇒', u'\\approx':u'≈', u'\\dashrightarrow':u'⇢', u'\\equiv':u'≡', u'\\ge':u'≥', u'\\geq':u'≥', @@ -744,11 +682,15 @@ class FormulaConfig(object): u'characters':[u'.',u'*',u'€',u'(',u')',u'[',u']',u':',u'·',u'!',u';',u'|',u'§',u'"',], } + urls = { + u'googlecharts':u'http://chart.googleapis.com/chart?cht=tx&chl=', + } + class GeneralConfig(object): "Configuration class from elyxer.config file" version = { - u'date':u'2011-01-03', u'lyxformat':u'410', u'number':u'1.2.0', + u'date':u'2011-04-13', u'lyxformat':u'410', u'number':u'1.2.2', } class HeaderConfig(object): @@ -835,8 +777,8 @@ class StyleConfig(object): } referenceformats = { - u'eqref':u'(@↕)', u'pageref':u'#↕', u'ref':u'@↕', - u'vpageref':u'on-page#↕', u'vref':u'@on-page#↕', + u'eqref':u'(@↕)', u'formatted':u'¶↕', u'nameref':u'$↕', u'pageref':u'#↕', + u'ref':u'@↕', u'vpageref':u'on-page#↕', u'vref':u'@on-page#↕', } size = { @@ -925,9 +867,8 @@ class TranslationConfig(object): u'list-figure':u'List of Figures', u'list-table':u'List of Tables', u'list-tableau':u'List of Tableaux', u'main-page':u'Main page', u'next':u'Next', u'nomenclature':u'Nomenclature', - u'on-page':u' on page ', u'prev':u'Previous', - u'references':u'References', u'toc':u'Table of Contents', - u'toc-for':u'Contents for ', u'up':u'Up', + u'on-page':u' on page ', u'prev':u'Prev', u'references':u'References', + u'toc':u'Table of Contents', u'toc-for':u'Contents for ', u'up':u'Up', } languages = { @@ -1033,6 +974,7 @@ class Options(object): destdirectory = None toc = False toctarget = '' + tocfor = None forceformat = None lyxformat = False target = None @@ -1060,6 +1002,7 @@ class Options(object): footnotes = None imageformat = None copyimages = False + googlecharts = False embedcss = [] branches = dict() @@ -1073,6 +1016,10 @@ class Options(object): if result: Trace.error(result) self.usage() + self.processoptions() + + def processoptions(self): + "Process all options parsed." if Options.help: self.usage() if Options.version: @@ -1092,7 +1039,7 @@ class Options(object): except: Trace.error('--splitpart needs a numeric argument, not ' + Options.splitpart) self.usage() - if Options.lowmem or Options.toc: + if Options.lowmem or Options.toc or Options.tocfor: Options.memory = False self.parsefootnotes() if Options.forceformat and not Options.imageformat: @@ -1103,10 +1050,15 @@ class Options(object): Options.css = ['http://elyxer.nongnu.org/lyx.css'] if Options.html: Options.simplemath = True + if Options.toc and not Options.tocfor: + Trace.error('Option --toc is deprecated; use --tocfor "page" instead') + Options.tocfor = Options.toctarget + if Options.nocopy: + Trace.error('Option --nocopy is deprecated; it is no longer needed') # set in Trace if necessary - for param in dir(Options): - if hasattr(Trace, param + 'mode'): - setattr(Trace, param + 'mode', getattr(self, param)) + for param in dir(Trace): + if param.endswith('mode'): + setattr(Trace, param, getattr(self, param[:-4])) def usage(self): "Show correct usage" @@ -1172,17 +1124,20 @@ class Options(object): Trace.error(' "sup", "align"') Trace.error(' Advanced output options:') Trace.error(' --splitpart "depth": split the resulting webpage at the given depth') - Trace.error(' --toc: create a table of contents') + Trace.error(' --tocfor "page": generate a TOC that points to the given page') Trace.error(' --target "frame": make all links point to the given frame') - Trace.error(' --toctarget "page": generate a TOC that points to the given page') Trace.error(' --notoclabels: omit the part labels in the TOC, such as Chapter') Trace.error(' --lowmem: do the conversion on the fly (conserve memory)') Trace.error(' --raw: generate HTML without header or footer.') Trace.error(' --jsmath "URL": use jsMath from elyxer.the given URL to display equations') Trace.error(' --mathjax "URL": use MathJax from elyxer.the given URL to display equations') + Trace.error(' --googlecharts: use Google Charts to generate formula images') Trace.error(' --template "file": use a template, put everything in <!--$content-->') Trace.error(' --copyright: add a copyright notice at the bottom') - Trace.error(' --nocopy (deprecated): no effect, maintained for backwards compatibility') + Trace.error(' Deprecated options:') + Trace.error(' --toc: (deprecated) create a table of contents') + Trace.error(' --toctarget "page": (deprecated) generate a TOC for the given page') + Trace.error(' --nocopy: (deprecated) maintained for backwards compatibility') sys.exit() def showversion(self): @@ -1234,6 +1189,78 @@ class BranchOptions(object): + +import urllib + + + + + + + + +class Cloner(object): + "An object used to clone other objects." + + def clone(cls, original): + "Return an exact copy of an object." + "The original object must have an empty constructor." + return cls.create(original.__class__) + + def create(cls, type): + "Create an object of a given class." + clone = type.__new__(type) + clone.__init__() + return clone + + clone = classmethod(clone) + create = classmethod(create) + +class ContainerExtractor(object): + "A class to extract certain containers." + + def __init__(self, config): + "The config parameter is a map containing three lists: allowed, copied and extracted." + "Each of the three is a list of class names for containers." + "Allowed containers are included as is into the result." + "Cloned containers are cloned and placed into the result." + "Extracted containers are looked into." + "All other containers are silently ignored." + self.allowed = config['allowed'] + self.cloned = config['cloned'] + self.extracted = config['extracted'] + + def extract(self, container): + "Extract a group of selected containers from elyxer.a container." + list = [] + locate = lambda c: c.__class__.__name__ in self.allowed + self.cloned + recursive = lambda c: c.__class__.__name__ in self.extracted + process = lambda c: self.process(c, list) + container.recursivesearch(locate, recursive, process) + return list + + def process(self, container, list): + "Add allowed containers, clone cloned containers and add the clone." + name = container.__class__.__name__ + if name in self.allowed: + list.append(container) + elif name in self.cloned: + list.append(self.safeclone(container)) + else: + Trace.error('Unknown container class ' + name) + + def safeclone(self, container): + "Return a new container with contents only in a safe list, recursively." + clone = Cloner.clone(container) + clone.output = container.output + clone.contents = self.extract(container) + return clone + + + + + + class Parser(object): "A generic parser" @@ -1635,12 +1662,13 @@ class LineReader(object): class LineWriter(object): "Writes a file as a series of lists" + file = False + def __init__(self, filename): if isinstance(filename, file): self.file = filename self.filename = None else: - self.file = codecs.open(filename, 'w', "utf-8") self.filename = filename def write(self, strings): @@ -1653,6 +1681,8 @@ class LineWriter(object): def writestring(self, string): "Write a string" + if not self.file: + self.file = codecs.open(self.filename, 'w', "utf-8") if self.file == sys.stdout: string = string.encode('utf-8') self.file.write(string) @@ -1666,8 +1696,15 @@ class LineWriter(object): -class Position(object): - "A position in a text to parse" + + + +class Globable(object): + """A bit of text which can be globbed (lumped together in bits). + Methods current(), skipcurrent(), checkfor() and isout() have to be + implemented by subclasses.""" + + leavepending = False def __init__(self): self.endinglist = EndingList() @@ -1679,109 +1716,85 @@ class Position(object): if ord(self.current()) == 0xfeff: self.skipcurrent() - def skip(self, string): - "Skip a string" - Trace.error('Unimplemented skip()') - - def identifier(self): - "Return an identifier for the current position." - Trace.error('Unimplemented identifier()') - return 'Error' - def isout(self): "Find out if we are out of the position yet." Trace.error('Unimplemented isout()') return True def current(self): - "Return the current character" + "Return the current character." Trace.error('Unimplemented current()') return '' - def extract(self, length): - "Extract the next string of the given length, or None if not enough text." - Trace.error('Unimplemented extract()') - return None - def checkfor(self, string): - "Check for a string at the given position." - return string == self.extract(len(string)) - - def checkforlower(self, string): - "Check for a string in lower case." - extracted = self.extract(len(string)) - if not extracted: - return False - return string.lower() == self.extract(len(string)).lower() + "Check for the given string in the current position." + Trace.error('Unimplemented checkfor()') + return False def finished(self): - "Find out if the current formula has finished" + "Find out if the current text has finished." if self.isout(): - self.endinglist.checkpending() + if not self.leavepending: + self.endinglist.checkpending() return True return self.endinglist.checkin(self) def skipcurrent(self): "Return the current character and skip it." - current = self.current() - self.skip(current) - return current - - def next(self): - "Advance the position and return the next character." - self.skipcurrent() - return self.current() - - def checkskip(self, string): - "Check for a string at the given position; if there, skip it" - if not self.checkfor(string): - return False - self.skip(string) - return True + Trace.error('Unimplemented skipcurrent()') + return '' def glob(self, currentcheck): - "Glob a bit of text that satisfies a check" + "Glob a bit of text that satisfies a check on the current char." glob = '' - while not self.finished() and currentcheck(self.current()): - glob += self.current() - self.skip(self.current()) + while not self.finished() and currentcheck(): + glob += self.skipcurrent() return glob def globalpha(self): "Glob a bit of alpha text" - return self.glob(lambda current: current.isalpha()) + return self.glob(lambda: self.current().isalpha()) def globnumber(self): "Glob a row of digits." - return self.glob(lambda current: current.isdigit()) + return self.glob(lambda: self.current().isdigit()) - def checkidentifier(self): - "Check if the current character belongs to an identifier." - return self.isidentifier(self.current()) - - def isidentifier(self, char): - "Return if the given character is alphanumeric or _." - if char.isalnum() or char == '_': + def isidentifier(self): + "Return if the current character is alphanumeric or _." + if self.current().isalnum() or self.current() == '_': return True return False def globidentifier(self): "Glob alphanumeric and _ symbols." - return self.glob(lambda current: self.isidentifier(current)) + return self.glob(self.isidentifier) + + def isvalue(self): + "Return if the current character is a value character:" + "not a bracket or a space." + if self.current().isspace(): + return False + if self.current() in '{}()': + return False + return True + + def globvalue(self): + "Glob a value: any symbols but brackets." + return self.glob(self.isvalue) def skipspace(self): - "Skip all whitespace at current position" - return self.glob(lambda current: current.isspace()) + "Skip all whitespace at current position." + return self.glob(lambda: self.current().isspace()) def globincluding(self, magicchar): "Glob a bit of text up to (including) the magic char." - glob = self.glob(lambda current: current != magicchar) + magicchar + glob = self.glob(lambda: self.current() != magicchar) + magicchar self.skip(magicchar) return glob - def globexcluding(self, magicchar): - "Glob a bit of text up until (excluding) the magic char." - return self.glob(lambda current: current != magicchar) + def globexcluding(self, excluded): + "Glob a bit of text up until (excluding) any excluded character." + return self.glob(lambda: self.current() not in excluded) def pushending(self, ending, optional = False): "Push a new ending to the bottom" @@ -1789,12 +1802,156 @@ class Position(object): def popending(self, expected = None): "Pop the ending found at the current position" + if self.isout() and self.leavepending: + return expected ending = self.endinglist.pop(self) if expected and expected != ending: Trace.error('Expected ending ' + expected + ', got ' + ending) self.skip(ending) return ending + def nextending(self): + "Return the next ending in the queue." + nextending = self.endinglist.findending(self) + if not nextending: + return None + return nextending.ending + +class EndingList(object): + "A list of position endings" + + def __init__(self): + self.endings = [] + + def add(self, ending, optional = False): + "Add a new ending to the list" + self.endings.append(PositionEnding(ending, optional)) + + def pickpending(self, pos): + "Pick any pending endings from a parse position." + self.endings += pos.endinglist.endings + + def checkin(self, pos): + "Search for an ending" + if self.findending(pos): + return True + return False + + def pop(self, pos): + "Remove the ending at the current position" + if pos.isout(): + Trace.error('No ending out of bounds') + return '' + ending = self.findending(pos) + if not ending: + Trace.error('No ending at ' + pos.current()) + return '' + for each in reversed(self.endings): + self.endings.remove(each) + if each == ending: + return each.ending + elif not each.optional: + Trace.error('Removed non-optional ending ' + each) + Trace.error('No endings left') + return '' + + def findending(self, pos): + "Find the ending at the current position" + if len(self.endings) == 0: + return None + for index, ending in enumerate(reversed(self.endings)): + if ending.checkin(pos): + return ending + if not ending.optional: + return None + return None + + def checkpending(self): + "Check if there are any pending endings" + if len(self.endings) != 0: + Trace.error('Pending ' + unicode(self) + ' left open') + + def __unicode__(self): + "Printable representation" + string = 'endings [' + for ending in self.endings: + string += unicode(ending) + ',' + if len(self.endings) > 0: + string = string[:-1] + return string + ']' + +class PositionEnding(object): + "An ending for a parsing position" + + def __init__(self, ending, optional): + self.ending = ending + self.optional = optional + + def checkin(self, pos): + "Check for the ending" + return pos.checkfor(self.ending) + + def __unicode__(self): + "Printable representation" + string = 'Ending ' + self.ending + if self.optional: + string += ' (optional)' + return string + + + +class Position(Globable): + """A position in a text to parse. + Including those in Globable, functions to implement by subclasses are: + skip(), identifier(), extract(), isout() and current().""" + + def __init__(self): + Globable.__init__(self) + + def skip(self, string): + "Skip a string" + Trace.error('Unimplemented skip()') + + def identifier(self): + "Return an identifier for the current position." + Trace.error('Unimplemented identifier()') + return 'Error' + + def extract(self, length): + "Extract the next string of the given length, or None if not enough text," + "without advancing the parse position." + Trace.error('Unimplemented extract()') + return None + + def checkfor(self, string): + "Check for a string at the given position." + return string == self.extract(len(string)) + + def checkforlower(self, string): + "Check for a string in lower case." + extracted = self.extract(len(string)) + if not extracted: + return False + return string.lower() == self.extract(len(string)).lower() + + def skipcurrent(self): + "Return the current character and skip it." + current = self.current() + self.skip(current) + return current + + def next(self): + "Advance the position and return the next character." + self.skipcurrent() + return self.current() + + def checkskip(self, string): + "Check for a string at the given position; if there, skip it" + if not self.checkfor(string): + return False + self.skip(string) + return True + def error(self, message): "Show an error message and the position identifier." Trace.error(message + ': ' + self.identifier()) @@ -1817,8 +1974,8 @@ class TextPosition(Position): "Return a sample of the remaining text." length = 30 if self.pos + length > len(self.text): - length = len(self.text) - self.pos - 1 - return '*' + self.text[self.pos:self.pos + length] + length = len(self.text) - self.pos + return '*' + self.text[self.pos:self.pos + length] + '*' def isout(self): "Find out if we are out of the text yet." @@ -1838,10 +1995,9 @@ class FilePosition(Position): "A parse position based on an underlying file." def __init__(self, filename): - "Create the position from elyxer.a file." + "Create the position from a file." Position.__init__(self) self.reader = LineReader(filename) - self.number = 1 self.pos = 0 self.checkbytemark() @@ -1853,17 +2009,24 @@ class FilePosition(Position): self.nextline() self.pos += length + def currentline(self): + "Get the current line of the underlying file." + return self.reader.currentline() + def nextline(self): "Go to the next line." self.reader.nextline() - self.number += 1 self.pos = 0 + def linenumber(self): + "Return the line number of the file." + return self.reader.linenumber + 1 + def identifier(self): "Return the current line and line number in the file." before = self.reader.currentline()[:self.pos - 1] after = self.reader.currentline()[self.pos:] - return 'line ' + unicode(self.number) + ': ' + before + '*' + after + return 'line ' + unicode(self.getlinenumber()) + ': ' + before + '*' + after def isout(self): "Find out if we are out of the text yet." @@ -1888,83 +2051,6 @@ class FilePosition(Position): return None return self.reader.currentline()[self.pos : self.pos + length] -class EndingList(object): - "A list of position endings" - - def __init__(self): - self.endings = [] - - def add(self, ending, optional): - "Add a new ending to the list" - self.endings.append(PositionEnding(ending, optional)) - - def checkin(self, pos): - "Search for an ending" - if self.findending(pos): - return True - return False - - def pop(self, pos): - "Remove the ending at the current position" - if pos.isout(): - Trace.error('No ending out of bounds') - return '' - ending = self.findending(pos) - if not ending: - Trace.error('No ending at ' + pos.current()) - return '' - for each in reversed(self.endings): - self.endings.remove(each) - if each == ending: - return each.ending - elif not each.optional: - Trace.error('Removed non-optional ending ' + each) - Trace.error('No endings left') - return '' - - def findending(self, pos): - "Find the ending at the current position" - if len(self.endings) == 0: - return None - for index, ending in enumerate(reversed(self.endings)): - if ending.checkin(pos): - return ending - if not ending.optional: - return None - return None - - def checkpending(self): - "Check if there are any pending endings" - if len(self.endings) != 0: - Trace.error('Pending ' + unicode(self) + ' left open') - - def __unicode__(self): - "Printable representation" - string = 'endings [' - for ending in self.endings: - string += unicode(ending) + ',' - if len(self.endings) > 0: - string = string[:-1] - return string + ']' - -class PositionEnding(object): - "An ending for a parsing position" - - def __init__(self, ending, optional): - self.ending = ending - self.optional = optional - - def checkin(self, pos): - "Check for the ending" - return pos.checkfor(self.ending) - - def __unicode__(self): - "Printable representation" - string = 'Ending ' + self.ending - if self.optional: - string += ' (optional)' - return string - class Container(object): @@ -2425,6 +2511,11 @@ class TaggedBit(FormulaBit): self.output = TaggedOutput().settag(tag, breaklines) return self + def selfcomplete(self, tag): + "Set the self-closing tag, no contents (as in <hr/>)." + self.output = TaggedOutput().settag(tag, empty = True) + return self + class FormulaConstant(Constant): "A constant string in a formula" @@ -2435,6 +2526,10 @@ class FormulaConstant(Constant): self.size = 1 self.type = None + def computesize(self): + "Compute the size of the constant: always 1." + return self.size + def clone(self): "Return a copy of itself." return FormulaConstant(self.original) @@ -2494,7 +2589,7 @@ class FormulaNumber(FormulaBit): def parsebit(self, pos): "Parse a bunch of digits" - digits = pos.glob(lambda current: current.isdigit()) + digits = pos.glob(lambda: pos.current().isdigit()) self.add(FormulaConstant(digits)) self.type = 'number' @@ -2569,15 +2664,8 @@ class Bracket(FormulaBit): def innerformula(self, pos): "Parse a whole formula inside the bracket" - while self.factory.detectany(pos): + while not pos.finished(): self.add(self.factory.parseany(pos)) - for ignored in self.factory.clearignored(pos): - self.add(ignored) - if pos.finished(): - return - if pos.current() != self.ending: - Trace.error('No formula in bracket at ' + pos.identifier()) - return def innertext(self, pos): "Parse some text inside the bracket, following textual rules." @@ -2587,17 +2675,16 @@ class Bracket(FormulaBit): specialchars.append(Comment.start) while not pos.finished(): if pos.current() in specialchars: - if self.factory.detectany(pos): - self.add(self.factory.parseany(pos)) - if pos.checkskip(' '): - self.original += ' ' + self.add(self.factory.parseany(pos)) + if pos.checkskip(' '): + self.original += ' ' else: self.add(FormulaConstant(pos.skipcurrent())) def innerliteral(self, pos): "Parse a literal inside the bracket, which does not generate HTML." self.literal = '' - while not pos.current() == self.ending: + while not pos.finished() and not pos.current() == self.ending: if pos.current() == self.start: self.parseliteral(pos) else: @@ -2700,19 +2787,38 @@ class Formula(Container): DocumentParameters.displaymode = True self.output.settag('div class="formula"', True) if Options.jsmath: - if self.header[0] != 'inline': - self.output = TaggedOutput().settag('div class="math"') - else: - self.output = TaggedOutput().settag('span class="math"') - self.contents = [Constant(self.parsed)] - return - if Options.mathjax: - self.output.tag = 'span class="MathJax_Preview"' - tag = 'script type="math/tex' - if self.header[0] != 'inline': - tag += ';mode=display' - self.contents = [TaggedText().constant(self.parsed, tag + '"', True)] - return + self.jsmath() + elif Options.mathjax: + self.mathjax() + elif Options.googlecharts: + self.googlecharts() + else: + self.classic() + + def jsmath(self): + "Make the contents for jsMath." + if self.header[0] != 'inline': + self.output = TaggedOutput().settag('div class="math"') + else: + self.output = TaggedOutput().settag('span class="math"') + self.contents = [Constant(self.parsed)] + + def mathjax(self): + "Make the contents for MathJax." + self.output.tag = 'span class="MathJax_Preview"' + tag = 'script type="math/tex' + if self.header[0] != 'inline': + tag += ';mode=display' + self.contents = [TaggedText().constant(self.parsed, tag + '"', True)] + + def googlecharts(self): + "Make the contents using Google Charts http://code.google.com/apis/chart/." + url = FormulaConfig.urls['googlecharts'] + urllib.quote_plus(self.parsed) + img = '<img class="chart" src="' + url + '" alt="' + self.parsed + '"/>' + self.contents = [Constant(img)] + + def classic(self): + "Make the contents using classic output generation with XHTML and CSS." whole = FormulaFactory().parseformula(self.parsed) FormulaProcessor().process(whole) whole.parent = self @@ -2764,7 +2870,7 @@ class Formula(Container): def parseupto(self, pos, limit): "Parse a formula that ends with the given command." pos.pushending(limit) - self.parsed = pos.glob(lambda current: True) + self.parsed = pos.glob(lambda: True) pos.popending(limit) def __unicode__(self): @@ -2777,41 +2883,28 @@ class WholeFormula(FormulaBit): "Parse a whole formula" def detect(self, pos): - "Check in the factory" - return self.factory.detectany(pos) + "Not outside the formula is enough." + return not pos.finished() def parsebit(self, pos): "Parse with any formula bit" - while self.factory.detectany(pos): - bit = self.factory.parseany(pos) - self.add(bit) - for ignored in self.factory.clearignored(pos): - self.add(ignored) + while not pos.finished(): + self.add(self.factory.parseany(pos)) class FormulaFactory(object): "Construct bits of formula" # bit types will be appended later - types = [FormulaSymbol, RawText, FormulaNumber, Bracket] - ignoredtypes = [Comment, WhiteSpace] + types = [FormulaSymbol, RawText, FormulaNumber, Bracket, Comment, WhiteSpace] + skippedtypes = [Comment, WhiteSpace] defining = False def __init__(self): "Initialize the map of instances." self.instances = dict() - def detectany(self, pos): - "Detect if there is a next bit" - if pos.finished(): - return False - for type in FormulaFactory.types: - if self.detecttype(type, pos): - return True - return False - def detecttype(self, type, pos): "Detect a bit of a given type." - self.clearignored(pos) if pos.finished(): return False return self.instance(type).detect(pos) @@ -2826,26 +2919,23 @@ class FormulaFactory(object): "Create a new formula bit of the given type." return Cloner.create(type).setfactory(self) - def clearignored(self, pos): - "Clear all ignored types." - ignored = [] + def clearskipped(self, pos): + "Clear any skipped types." while not pos.finished(): - cleared = self.clearany(pos) - if not cleared: - return ignored - ignored.append(cleared) - return ignored - - def clearany(self, pos): - "Cleary any ignored type." - for type in self.ignoredtypes: + if not self.skipany(pos): + return + return + + def skipany(self, pos): + "Skip any skipped types." + for type in self.skippedtypes: if self.instance(type).detect(pos): return self.parsetype(type, pos) return None def parseany(self, pos): "Parse any formula bit at the current location." - for type in FormulaFactory.types: + for type in self.types + self.skippedtypes: if self.detecttype(type, pos): return self.parsetype(type, pos) Trace.error('Unrecognized formula at ' + pos.identifier()) @@ -2980,13 +3070,6 @@ class NumberCounter(object): "Set an initial value." self.value = value - def increase(self): - "Increase the counter value and return the counter." - if not self.value: - self.value = 0 - self.value += 1 - return self - def gettext(self): "Get the next value as a text string." return unicode(self.value) @@ -3029,8 +3112,11 @@ class NumberCounter(object): return self.gettext() def getnext(self): - "Get the next value as configured: increase() and getvalue()." - return self.increase().getvalue() + "Increase the current value and get the next value as configured." + if not self.value: + self.value = 0 + self.value += 1 + return self.getvalue() def reset(self): "Reset the counter." @@ -3052,13 +3138,13 @@ class DependentCounter(NumberCounter): self.last = self.master.getvalue() return self - def increase(self): + def getnext(self): "Increase or, if the master counter has changed, restart." if self.last != self.master.getvalue(): self.reset() - NumberCounter.increase(self) + value = NumberCounter.getnext(self) self.last = self.master.getvalue() - return self + return value def getvalue(self): "Get the value of the combined counter: master.dependent." @@ -3527,13 +3613,6 @@ class StartAppendix(BlackBox): "Activate the special numbering scheme for appendices, using letters." NumberGenerator.generator.startappendix() -class ERT(Container): - "Evil Red Text" - - def __init__(self): - self.parser = InsetParser() - self.output = EmptyOutput() - @@ -3740,11 +3819,11 @@ class Label(Link): reference.destination = self return self - def labelnumber(self): - "Get the number for the latest numbered container seen." + def findpartkey(self): + "Get the part key for the latest numbered container seen." numbered = self.numbered(self) - if numbered and numbered.partkey and numbered.partkey.number: - return numbered.partkey.number + if numbered and numbered.partkey: + return numbered.partkey return '' def numbered(self, container): @@ -3779,29 +3858,44 @@ class Reference(Link): self.direction = u'↓' label = Label().complete(' ', self.key, 'preref') self.destination = label - self.format() + self.formatcontents() if not self.key in Reference.references: Reference.references[self.key] = [] Reference.references[self.key].append(self) - def format(self): + def formatcontents(self): "Format the reference contents." - formats = StyleConfig.referenceformats formatkey = self.getparameter('LatexCommand') if not formatkey: formatkey = 'ref' - if not formatkey in formats: - Trace.error('Unknown reference format ' + formatkey) - formatstring = u'↕' + self.formatted = u'↕' + if formatkey in StyleConfig.referenceformats: + self.formatted = StyleConfig.referenceformats[formatkey] else: - formatstring = formats[formatkey] - formatstring = formatstring.replace(u'↕', self.direction) - if '@' in formatstring: - formatstring = formatstring.replace('@', self.destination.labelnumber()) - formatstring = formatstring.replace('#', '1') - if 'on-page' in formatstring: - formatstring = formatstring.replace('on-page', Translator.translate('on-page')) - self.contents = [Constant(formatstring)] + Trace.error('Unknown reference format ' + formatkey) + self.replace(u'↕', self.direction) + self.replace('#', '1') + self.replace('on-page', Translator.translate('on-page')) + partkey = self.destination.findpartkey() + # only if partkey and partkey.number are not null, send partkey.number + self.replace('@', partkey and partkey.number) + self.replace(u'¶', partkey and partkey.tocentry) + if not '$' in self.formatted or not partkey or not partkey.titlecontents: + self.contents = [Constant(self.formatted)] + return + pieces = self.formatted.split('$') + self.contents = [Constant(pieces[0])] + for piece in pieces[1:]: + self.contents += partkey.titlecontents + self.contents.append(Constant(piece)) + + def replace(self, key, value): + "Replace a key in the format template with a value." + if not key in self.formatted: + return + if not value: + value = '' + self.formatted = self.formatted.replace(key, value) def __unicode__(self): "Return a printable representation." @@ -3814,6 +3908,7 @@ class FormulaCommand(FormulaBit): types = [] start = FormulaConfig.starts['command'] + commandmap = None def detect(self, pos): "Find the current command." @@ -3846,14 +3941,18 @@ class FormulaCommand(FormulaBit): "Parse a given command type." bit = self.factory.create(type) bit.setcommand(command) - bit.parsebit(pos) + returned = bit.parsebit(pos) + if returned: + return returned return bit def extractcommand(self, pos): "Extract the command from elyxer.the current position." if not pos.checkskip(FormulaCommand.start): - Trace.error('Missing command start ' + start) + pos.error('Missing command start ' + FormulaCommand.start) return + if pos.finished(): + return self.emptycommand(pos) if pos.current().isalpha(): # alpha command command = FormulaCommand.start + pos.globalpha() @@ -3863,6 +3962,16 @@ class FormulaCommand(FormulaBit): # symbol command return FormulaCommand.start + pos.skipcurrent() + def emptycommand(self, pos): + """Check for an empty command: look for command disguised as ending. + Special case against '{ \{ \} }' situation.""" + command = '' + if not pos.isout(): + ending = pos.nextending() + if ending and pos.checkskip(ending): + command = ending + return FormulaCommand.start + command + def parseupgreek(self, command, pos): "Parse the Greek \\up command.." if len(command) < 4: @@ -3885,13 +3994,14 @@ class CommandBit(FormulaCommand): def setcommand(self, command): "Set the command in the bit" self.command = command - self.original += command - self.translated = self.commandmap[self.command] + if self.commandmap: + self.original += command + self.translated = self.commandmap[self.command] def parseparameter(self, pos): "Parse a parameter at the current position" - if not self.factory.detectany(pos): - Trace.error('No parameter found at: ' + pos.identifier()) + self.factory.clearskipped(pos) + if pos.finished(): return None parameter = self.factory.parseany(pos) self.add(parameter) @@ -3899,6 +4009,7 @@ class CommandBit(FormulaCommand): def parsesquare(self, pos): "Parse a square bracket" + self.factory.clearskipped(pos) if not self.factory.detecttype(SquareBracket, pos): return None bracket = self.factory.parsetype(SquareBracket, pos) @@ -3907,21 +4018,35 @@ class CommandBit(FormulaCommand): def parseliteral(self, pos): "Parse a literal bracket." + self.factory.clearskipped(pos) if not self.factory.detecttype(Bracket, pos): - Trace.error('No literal parameter found at: ' + pos.identifier()) - return None + if not pos.isvalue(): + Trace.error('No literal parameter found at: ' + pos.identifier()) + return None + return pos.globvalue() bracket = Bracket().setfactory(self.factory) self.add(bracket.parseliteral(pos)) return bracket.literal def parsesquareliteral(self, pos): "Parse a square bracket literally." + self.factory.clearskipped(pos) if not self.factory.detecttype(SquareBracket, pos): return None bracket = SquareBracket().setfactory(self.factory) self.add(bracket.parseliteral(pos)) return bracket.literal + def parsetext(self, pos): + "Parse a text parameter." + self.factory.clearskipped(pos) + if not self.factory.detecttype(Bracket, pos): + Trace.error('No text parameter for ' + self.command) + return None + bracket = Bracket().setfactory(self.factory).parsetext(pos) + self.add(bracket) + return bracket + class EmptyCommand(CommandBit): "An empty command (without parameters)" @@ -3993,10 +4118,7 @@ class TextFunction(CommandBit): def parsebit(self, pos): "Parse a text parameter" self.output = TaggedOutput().settag(self.translated) - if not self.factory.detecttype(Bracket, pos): - Trace.error('No parameter for ' + unicode(self)) - bracket = Bracket().setfactory(self.factory).parsetext(pos) - self.add(bracket) + self.parsetext(pos) def process(self): "Set the type to font" @@ -4060,7 +4182,7 @@ class BigSymbol(object): if not self.symbol in self.symbols: return [self.symbol] if self.smalllimit(): - return ['<span class="bigsymbol">' + self.symbol + '</span>'] + return [self.symbol] return self.symbols[self.symbol] def smalllimit(self): @@ -4074,62 +4196,63 @@ class BigSymbol(object): class BigBracket(BigSymbol): "A big bracket generator." - def __init__(self, size, bracket): + def __init__(self, size, bracket, alignment='l'): "Set the size and symbol for the bracket." self.size = size self.original = bracket + self.alignment = alignment + self.pieces = None if bracket in FormulaConfig.bigbrackets: self.pieces = FormulaConfig.bigbrackets[bracket] - else: - self.pieces = [bracket, bracket] def getpiece(self, index): "Return the nth piece for the bracket." - if len(self.pieces) == 1: - return self.pieces[0] + function = getattr(self, 'getpiece' + unicode(len(self.pieces))) + return function(index) + + def getpiece1(self, index): + "Return the only piece for a single-piece bracket." + return self.pieces[0] + + def getpiece3(self, index): + "Get the nth piece for a 3-piece bracket: parenthesis or square bracket." if index == 0: return self.pieces[0] if index == self.size - 1: return self.pieces[-1] return self.pieces[1] - def getcell(self, index, align): + def getpiece4(self, index): + "Get the nth piece for a 4-piece bracket: curly bracket." + if index == 0: + return self.pieces[0] + if index == self.size - 1: + return self.pieces[3] + if index == (self.size - 1)/2: + return self.pieces[2] + return self.pieces[1] + + def getcell(self, index): "Get the bracket piece as an array cell." piece = self.getpiece(index) - return TaggedBit().constant(piece, 'span class="bracket align-' + align + '"') + span = 'span class="bracket align-' + self.alignment + '"' + return TaggedBit().constant(piece, span) - def getarray(self, align): - "Get the bracket as an array." - if self.size == 1: + def getcontents(self): + "Get the bracket as an array or as a single bracket." + if self.size == 1 or not self.pieces: return self.getsinglebracket() rows = [] for index in range(self.size): - cell = self.getcell(index, align) + cell = self.getcell(index) rows.append(TaggedBit().complete([cell], 'span class="arrayrow"')) - return TaggedBit().complete(rows, 'span class="array"') + return [TaggedBit().complete(rows, 'span class="array"')] def getsinglebracket(self): "Return the bracket as a single sign." if self.original == '.': - return TaggedBit().constant('', 'span class="emptydot"') - return TaggedBit().constant(self.original, 'span class="symbol"') - -class CasesBrace(BigBracket): - "A big brace used for a case statement." - - def __init__(self, size): - "Set the size for the brace." - self.size = size - - def getpiece(self, index): - "Get the nth piece for the brace." - if index == 0: - return u'⎧' - if index == self.size - 1: - return u'⎩' - if index == (self.size - 1)/2: - return u'⎨' - return u'⎪' + return [TaggedBit().constant('', 'span class="emptydot"')] + return [TaggedBit().constant(self.original, 'span class="symbol"')] @@ -4155,13 +4278,9 @@ class FormulaCell(FormulaCommand): return self def parsebit(self, pos): - self.factory.clearignored(pos) + self.factory.clearskipped(pos) if pos.finished(): return - if not self.factory.detecttype(WholeFormula, pos): - Trace.error('Unexpected end of array cell at ' + pos.identifier()) - pos.skip(pos.current()) - return self.add(self.factory.parsetype(WholeFormula, pos)) class FormulaRow(FormulaCommand): @@ -4278,16 +4397,16 @@ class FormulaCases(MultiRowFormula): def parsebit(self, pos): "Parse the cases" - self.output = TaggedOutput().settag('span class="bracketcases"', True) + self.output = ContentsOutput() self.alignments = ['l', 'l'] self.parserows(pos) for row in self.contents: for cell in row.contents: cell.output.settag('span class="case align-l"', True) cell.contents.append(FormulaConstant(u' ')) - brace = CasesBrace(len(self.contents)) - for index, row in enumerate(self.rows): - row.contents.insert(0, brace.getcell(index, 'l')) + array = TaggedBit().complete(self.contents, 'span class="bracketcases"', True) + brace = BigBracket(len(self.contents), '{', 'l') + self.contents = brace.getcontents() + [array] class EquationEnvironment(MultiRowFormula): "A \\begin{}...\\end equation environment with rows and cells." @@ -4343,12 +4462,18 @@ class CombiningFunction(OneParamFunction): self.type = 'alpha' combining = self.translated parameter = self.parsesingleparameter(pos) - if len(parameter.extracttext()) != 1: + if not parameter: + Trace.error('Empty parameter for combining function ' + self.command) + elif len(parameter.extracttext()) != 1: Trace.error('Applying combining function ' + self.command + ' to invalid string "' + parameter.extracttext() + '"') self.contents.append(Constant(combining)) def parsesingleparameter(self, pos): "Parse a parameter, or a single letter." + self.factory.clearskipped(pos) + if pos.finished(): + Trace.error('Error while parsing single parameter at ' + pos.identifier()) + return None if self.factory.detecttype(Bracket, pos) \ or self.factory.detecttype(FormulaCommand, pos): return self.parseparameter(pos) @@ -4384,6 +4509,20 @@ class LimitCommand(EmptyCommand): for piece in pieces: self.contents.append(TaggedBit().constant(piece, 'span class="limit"')) +class LimitPreviousCommand(LimitCommand): + "A command to limit the previous command." + + commandmap = None + + def parsebit(self, pos): + "Do nothing." + self.output = TaggedOutput().settag('span class="limits"') + self.factory.clearskipped(pos) + + def __unicode__(self): + "Return a printable representation." + return 'Limit previous command' + class LimitsProcessor(MathsProcessor): "A processor for limits inside an element." @@ -4400,10 +4539,18 @@ class LimitsProcessor(MathsProcessor): "Check if the current position has a limits command." if not DocumentParameters.displaymode: return False + if self.checkcommand(contents, index + 1, LimitPreviousCommand): + self.limitsahead(contents, index) + return False if not isinstance(contents[index], LimitCommand): return False return self.checkscript(contents, index + 1) + def limitsahead(self, contents, index): + "Limit the current element based on the next." + contents[index + 1].add(contents[index].clone()) + contents[index].output = EmptyOutput() + def modifylimits(self, contents, index): "Modify a limits commands so that the limits appear above and below." limited = contents[index] @@ -4431,9 +4578,13 @@ class LimitsProcessor(MathsProcessor): def checkscript(self, contents, index): "Check if the current element is a sub- or superscript." + return self.checkcommand(contents, index, SymbolFunction) + + def checkcommand(self, contents, index, type): + "Check for the given type as the current element." if len(contents) <= index: return False - return isinstance(contents[index], SymbolFunction) + return isinstance(contents[index], type) def getscript(self, contents, index): "Get the sub- or superscript." @@ -4512,10 +4663,10 @@ class BracketProcessor(MathsProcessor): def resize(self, command, size): "Resize a bracket command to the given size." character = command.extracttext() - bracket = BigBracket(size, character) alignment = command.command.replace('\\', '') + bracket = BigBracket(size, character, alignment) command.output = ContentsOutput() - command.contents = [bracket.getarray(alignment)] + command.contents = bracket.getcontents() FormulaCommand.types += [ @@ -4551,7 +4702,7 @@ class ParameterDefinition(object): if opening == '[': self.optional = True if not pos.checkskip('$'): - Trace.error('Wrong parameter name ' + pos.current()) + Trace.error('Wrong parameter name, did you mean $' + pos.current() + '?') return None self.name = pos.skipcurrent() if pos.checkskip('!'): @@ -4622,13 +4773,6 @@ class ParameterFunction(CommandBit): return None return param.literalvalue - def getintvalue(self, name): - "Get the value of a literal parameter as an int." - value = self.getliteralvalue(name) - if not value: - return 0 - return int(value) - class HybridFunction(ParameterFunction): """ A parameter function where the output is also defined using a template. @@ -4642,6 +4786,9 @@ class HybridFunction(ParameterFunction): Sizes can be specified in hybridsizes, e.g. adding parameter sizes. By default the resulting size is the max of all arguments. Sizes are used to generate the right parameters. + A function followed by a single / is output as a self-closing XHTML tag: + [f0/,hr] + will generate <hr/>. """ commandmap = FormulaConfig.hybridfunctions @@ -4669,6 +4816,7 @@ class HybridFunction(ParameterFunction): elif pos.checkskip('f'): function = self.writefunction(pos) if function: + function.type = None result.append(function) elif pos.checkskip('('): result.append(self.writebracket('left', '(')) @@ -4695,6 +4843,9 @@ class HybridFunction(ParameterFunction): tag = self.readtag(pos) if not tag: return None + if pos.checkskip('/'): + # self-closing XHTML tag, such as <hr/> + return TaggedBit().selfcomplete(tag) if not pos.checkskip('{'): Trace.error('Function should be defined in {}') return None @@ -4703,9 +4854,7 @@ class HybridFunction(ParameterFunction): pos.popending() if len(contents) == 0: return None - function = TaggedBit().complete(contents, tag) - function.type = None - return function + return TaggedBit().complete(contents, tag) def readtag(self, pos): "Get the tag corresponding to the given index. Does parameter substitution." @@ -4878,16 +5027,48 @@ class LstParser(object): -class MathMacro(object): - "A math macro: command, parameters, default values, definition." +class MacroDefinition(CommandBit): + "A function that defines a new command (a macro)." macros = dict() - def __init__(self): - self.newcommand = None + def parsebit(self, pos): + "Parse the function that defines the macro." + self.output = EmptyOutput() self.parameternumber = 0 self.defaults = [] - self.definition = None + self.factory.defining = True + self.parseparameters(pos) + self.factory.defining = False + Trace.debug('New command ' + self.newcommand + ' (' + \ + unicode(self.parameternumber) + ' parameters)') + self.macros[self.newcommand] = self + + def parseparameters(self, pos): + "Parse all optional parameters (number of parameters, default values)" + "and the mandatory definition." + self.newcommand = self.parsenewcommand(pos) + # parse number of parameters + literal = self.parsesquareliteral(pos) + if literal: + self.parameternumber = int(literal) + # parse all default values + bracket = self.parsesquare(pos) + while bracket: + self.defaults.append(bracket) + bracket = self.parsesquare(pos) + # parse mandatory definition + self.definition = self.parseparameter(pos) + + def parsenewcommand(self, pos): + "Parse the name of the new command." + self.factory.clearskipped(pos) + if self.factory.detecttype(Bracket, pos): + return self.parseliteral(pos) + if self.factory.detecttype(FormulaCommand, pos): + return self.factory.create(FormulaCommand).extractcommand(pos) + Trace.error('Unknown formula bit in defining function at ' + pos.identifier()) + return 'unknown' def instantiate(self): "Return an instance of the macro." @@ -4909,55 +5090,10 @@ class MacroParameter(FormulaBit): self.original = '#' + unicode(self.number) self.contents = [TaggedBit().constant('#' + unicode(self.number), 'span class="unknown"')] -class DefiningFunction(ParameterFunction): - "Read a function that defines a new command (a macro)." - - commandmap = FormulaConfig.definingfunctions - - def parsebit(self, pos): - "Parse a function with [] and {} parameters." - if self.factory.detecttype(Bracket, pos): - newcommand = self.parseliteral(pos) - elif self.factory.detecttype(FormulaCommand, pos): - newcommand = self.factory.create(FormulaCommand).extractcommand(pos) - else: - Trace.error('Unknown formula bit in defining function at ' + pos.identifier()) - return - template = self.translated - self.factory.defining = True - self.readparams(template, pos) - self.factory.defining = False - self.contents = [] - macro = MathMacro() - macro.newcommand = newcommand - macro.parameternumber = self.getintvalue('$n') - Trace.debug('New command ' + newcommand + ' (' + \ - unicode(macro.parameternumber) + ' parameters)') - macro.definition = self.getvalue('$d') - self.extractdefaults(macro) - MathMacro.macros[newcommand] = macro - - def extractdefaults(self, macro): - "Extract the default values for existing parameters." - for index in range(9): - value = self.extractdefault(index + 1) - if not value: - return - macro.defaults.append(value) - - def extractdefault(self, index): - "Extract the default value for parameter index." - value = self.getvalue('$' + unicode(index)) - if not value: - return None - if len(value.contents) == 0: - return FormulaConstant('') - return value - class MacroFunction(CommandBit): "A function that was defined using a macro." - commandmap = MathMacro.macros + commandmap = MacroDefinition.macros def parsebit(self, pos): "Parse a number of input parameters." @@ -4971,11 +5107,6 @@ class MacroFunction(CommandBit): "Parse as many parameters as are needed." self.parseoptional(pos, list(macro.defaults)) self.parsemandatory(pos, macro.parameternumber - len(macro.defaults)) - while self.factory.detecttype(Bracket, pos): - self.values.append(self.parseparameter(pos)) - remaining = macro.parameternumber - len(self.values) - if remaining > 0: - self.parsenumbers(remaining, pos) if len(self.values) < macro.parameternumber: Trace.error('Missing parameters in macro ' + unicode(self)) @@ -5006,6 +5137,7 @@ class MacroFunction(CommandBit): "Parse a macro parameter. Could be a bracket or a single letter." "If there are just two values remaining and there is a running number," "parse as two separater numbers." + self.factory.clearskipped(pos) if pos.finished(): return None if self.factory.detecttype(FormulaNumber, pos): @@ -5060,7 +5192,7 @@ class FormulaMacro(Formula): FormulaFactory.types += [ MacroParameter ] FormulaCommand.types += [ - DefiningFunction, MacroFunction, + MacroFunction, ] @@ -5075,10 +5207,12 @@ def math2html(formula): def main(): "Main function, called if invoked from elyxer.the command line" - if len(sys.argv) <= 1: + args = sys.argv + Options().parseoptions(args) + if len(args) != 1: Trace.error('Usage: math2html.py escaped_string') exit() - result = math2html(sys.argv[1]) + result = math2html(args[0]) Trace.message(result) if __name__ == '__main__': diff --git a/docutils/writers/html4css1/__init__.py b/docutils/writers/html4css1/__init__.py index edd8fca26..421b374db 100644 --- a/docutils/writers/html4css1/__init__.py +++ b/docutils/writers/html4css1/__init__.py @@ -26,6 +26,7 @@ except ImportError: import docutils from docutils import frontend, nodes, utils, writers, languages, io from docutils.transforms import writer_aux +from docutils.math import unimathsymbols2tex from docutils.math.latex2mathml import parse_latex_math from docutils.math.math2html import math2html @@ -123,7 +124,8 @@ class Writer(writers.Writer): 'Defined styles: "borderless". Default: ""', ['--table-style'], {'default': ''}), - ('Math output format, e.g. "MathML" or "HTML". Default: "MathML"', + ('Math output format, one of "MathML", "HTML", or "LaTeX". ' + 'Default: "MathML"', ['--math-output'], {'default': 'MathML'}), ('Omit the XML declaration. Use with caution.', @@ -1107,17 +1109,37 @@ class HTMLTranslator(nodes.NodeVisitor): def depart_literal_block(self, node): self.body.append('\n</pre>\n') + def multiline_math(self, node): + """find out whether `code` is a multi-line equation + + This is a very simplified test, looking + for line-breaks (``\\``) outside environments. + """ + code = node.astext() + # cut out environment content: + chunks = code.split(r'\begin{') + toplevel_code = ''.join([chunk.split(r'\end{')[-1] + for chunk in chunks]) + return toplevel_code.find(r'\\') >= 0 + def visit_math(self, node, inline=True): - math_in = node.astext() + math_code = node.astext().translate(unimathsymbols2tex.uni2tex_table) if self.math_output == 'latex': self.body.append(self.starttag(node, 'tt', CLASS='math')) return if self.math_output == 'html': + wrapper = u'%s' + if not inline and self.multiline_math(node): + math_env = 'align*' + wrapper = u'\n'.join([r'\begin{%s}' % math_env, + '%s', + r'\end{%s}' % math_env]) + # tag={True: 'span', False: 'div'} self.body.append(self.starttag(node, tag[inline], CLASS='formula')) - self.body.append(math2html(math_in)) + self.body.append(math2html(wrapper % math_code)) self.body.append('</%s>\n' % tag[inline]) elif self.math_output == 'mathml': @@ -1132,12 +1154,21 @@ class HTMLTranslator(nodes.NodeVisitor): self.has_mathml_dtd = True # Convert from LaTeX to MathML: try: - mathml_tree = parse_latex_math(math_in, inline=inline) + mathml_tree = parse_latex_math(math_code, inline=inline) math_out = ''.join(mathml_tree.xml()) except SyntaxError, err: - self.document.reporter.error(err, base_node=node) - math_out = unicode(err) # TODO: generate system message and link. - self.body.append(math_out) + err_node = self.document.reporter.error(err, base_node=node) + self.visit_system_message(err_node) + self.body.append(self.starttag(node, 'p')) + self.body.append(u','.join(err.args)) + self.body.append('</p>\n') + self.body.append(self.starttag(node, 'pre', + CLASS='literal-block')) + self.body.append(self.encode(math_code)) + self.body.append('\n</pre>\n') + self.depart_system_message(err_node) + else: + self.body.append(math_out) if not inline: self.body.append('\n') # Content already processed: diff --git a/docutils/writers/html4css1/math.css b/docutils/writers/html4css1/math.css index 2896dacf6..cdfca65a1 100644 --- a/docutils/writers/html4css1/math.css +++ b/docutils/writers/html4css1/math.css @@ -30,6 +30,9 @@ span.unknown { span.ignored, span.arraydef { display: none; } +.formula i { + letter-spacing: 0.1ex; +} /* Alignment */ .align-left, .align-l { @@ -166,3 +169,5 @@ span.arraycell, span.bracket, span.case, span.binomcell, span.environmentcell { line-height: 99%; border: 0ex; } +@import "math-extras.css"; + diff --git a/docutils/writers/latex2e/__init__.py b/docutils/writers/latex2e/__init__.py index 3cfa84721..2f35b6ccf 100644 --- a/docutils/writers/latex2e/__init__.py +++ b/docutils/writers/latex2e/__init__.py @@ -20,6 +20,7 @@ import string import urllib from docutils import frontend, nodes, languages, writers, utils, io from docutils.transforms import writer_aux +from docutils.math import unimathsymbols2tex # compatibility module for Python 2.3 if not hasattr(string, 'Template'): @@ -2424,50 +2425,51 @@ class LaTeXTranslator(nodes.NodeVisitor): ## def depart_meta(self, node): ## self.out.append('[depart_meta]\n') - def visit_math(self, node): - """math role""" - self.requirements['amsmath'] = r'\usepackage{amsmath}' - if node['classes']: - self.visit_inline(node) - self.verbatim = True - self.out.append('$') - - def depart_math(self, node): - self.verbatim = False - self.out.append('$') - if node['classes']: - self.depart_inline(node) - - def multiline_math(self, code): + def multiline_math(self, node): """find out whether `code` is a multi-line equation This is a very simplified test, looking for line-breaks (``\\``) outside environments. """ + code = node.astext() # cut out environment content: chunks = code.split(r'\begin{') toplevel_code = ''.join([chunk.split(r'\end{')[-1] for chunk in chunks]) return toplevel_code.find(r'\\') >= 0 - def visit_math_block(self, node): - self.requirements['amsmath'] = r'\usepackage{amsmath}' + def visit_math(self, node, math_env='$'): + """math role""" if node['classes']: self.visit_inline(node) - math_code = node.astext() - if self.multiline_math(math_code): - environment = 'align*' + self.requirements['amsmath'] = r'\usepackage{amsmath}' + math_code = node.astext().translate(unimathsymbols2tex.uni2tex_table) + if math_env == '$': + wrapper = u'$%s$' else: - environment = 'equation*' - self.out.append ('%%\n\\begin{%s}\n' % environment) - self.out.append(math_code) - self.out.append('\n\\end{%s}' % environment) + wrapper = u'\n'.join(['%%', + r'\begin{%s}' % math_env, + '%s', + r'\end{%s}' % math_env]) + # print repr(wrapper), repr(math_code) + self.out.append(wrapper % math_code) + if node['classes']: + self.depart_inline(node) # Content already processed: raise nodes.SkipNode + def depart_math(self, node): + pass # never reached + + def visit_math_block(self, node): + if self.multiline_math(node): + math_env = 'align*' + else: + math_env = 'equation*' + self.visit_math(node, math_env=math_env) + def depart_math_block(self, node): - if node['classes']: - self.depart_inline(node) + pass # never reached def visit_option(self, node): if self.context[-1]: |
