math, error_reporting, and urischemes moved to the utils package.
[docutils.git] / docutils / utils / math / math2html.py
blob4a425cea5a898e9e1db0216d222a9b73e5a26d91
1 #! /usr/bin/env python
2 # -*- coding: utf-8 -*-
4 # math2html: convert LaTeX equations to HTML output.
6 # Copyright (C) 2009-2011 Alex Fernández
8 # Released under the terms of the `2-Clause BSD license'_, in short:
9 # Copying and distribution of this file, with or without modification,
10 # are permitted in any medium without royalty provided the copyright
11 # notice and this notice are preserved.
12 # This file is offered as-is, without any warranty.
14 # .. _2-Clause BSD license: http://www.spdx.org/licenses/BSD-2-Clause
16 # Based on eLyXer: convert LyX source files to HTML output.
17 # http://elyxer.nongnu.org/
19 # --end--
20 # Alex 20101110
21 # eLyXer standalone formula conversion to HTML.
26 import sys
28 if sys.version_info < (2,4):
29 def reversed(sequence):
30 i = len(sequence)
31 while i > 0:
32 i = i - 1
33 yield sequence[i]
36 class Trace(object):
37 "A tracing class"
39 debugmode = False
40 quietmode = False
41 showlinesmode = False
43 prefix = None
45 def debug(cls, message):
46 "Show a debug message"
47 if not Trace.debugmode or Trace.quietmode:
48 return
49 Trace.show(message, sys.stdout)
51 def message(cls, message):
52 "Show a trace message"
53 if Trace.quietmode:
54 return
55 if Trace.prefix and Trace.showlinesmode:
56 message = Trace.prefix + message
57 Trace.show(message, sys.stdout)
59 def error(cls, message):
60 "Show an error message"
61 message = '* ' + message
62 if Trace.prefix and Trace.showlinesmode:
63 message = Trace.prefix + message
64 Trace.show(message, sys.stderr)
66 def fatal(cls, message):
67 "Show an error message and terminate"
68 Trace.error('FATAL: ' + message)
69 exit(-1)
71 def show(cls, message, channel):
72 "Show a message out of a channel"
73 if sys.version_info < (3,0):
74 message = message.encode('utf-8')
75 channel.write(message + '\n')
77 debug = classmethod(debug)
78 message = classmethod(message)
79 error = classmethod(error)
80 fatal = classmethod(fatal)
81 show = classmethod(show)
86 import os.path
87 import sys
90 class BibStylesConfig(object):
91 "Configuration class from elyxer.config file"
93 abbrvnat = {
95 u'@article':u'$authors. $title. <i>$journal</i>,{ {$volume:}$pages,} $month $year.{ doi: $doi.}{ URL <a href="$url">$url</a>.}{ $note.}',
96 u'cite':u'$surname($year)',
97 u'default':u'$authors. <i>$title</i>. $publisher, $year.{ URL <a href="$url">$url</a>.}{ $note.}',
100 alpha = {
102 u'@article':u'$authors. $title.{ <i>$journal</i>{, {$volume}{($number)}}{: $pages}{, $year}.}{ <a href="$url">$url</a>.}{ <a href="$filename">$filename</a>.}{ $note.}',
103 u'cite':u'$Sur$YY',
104 u'default':u'$authors. $title.{ <i>$journal</i>,} $year.{ <a href="$url">$url</a>.}{ <a href="$filename">$filename</a>.}{ $note.}',
107 authordate2 = {
109 u'@article':u'$authors. $year. $title. <i>$journal</i>, <b>$volume</b>($number), $pages.{ URL <a href="$url">$url</a>.}{ $note.}',
110 u'@book':u'$authors. $year. <i>$title</i>. $publisher.{ URL <a href="$url">$url</a>.}{ $note.}',
111 u'cite':u'$surname, $year',
112 u'default':u'$authors. $year. <i>$title</i>. $publisher.{ URL <a href="$url">$url</a>.}{ $note.}',
115 default = {
117 u'@article':u'$authors: “$title”, <i>$journal</i>,{ pp. $pages,} $year.{ URL <a href="$url">$url</a>.}{ $note.}',
118 u'@book':u'{$authors: }<i>$title</i>{ ($editor, ed.)}.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
119 u'@booklet':u'$authors: <i>$title</i>.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
120 u'@conference':u'$authors: “$title”, <i>$journal</i>,{ pp. $pages,} $year.{ URL <a href="$url">$url</a>.}{ $note.}',
121 u'@inbook':u'$authors: <i>$title</i>.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
122 u'@incollection':u'$authors: <i>$title</i>{ in <i>$booktitle</i>{ ($editor, ed.)}}.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
123 u'@inproceedings':u'$authors: “$title”, <i>$journal</i>,{ pp. $pages,} $year.{ URL <a href="$url">$url</a>.}{ $note.}',
124 u'@manual':u'$authors: <i>$title</i>.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
125 u'@mastersthesis':u'$authors: <i>$title</i>.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
126 u'@misc':u'$authors: <i>$title</i>.{{ $publisher,}{ $howpublished,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
127 u'@phdthesis':u'$authors: <i>$title</i>.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
128 u'@proceedings':u'$authors: “$title”, <i>$journal</i>,{ pp. $pages,} $year.{ URL <a href="$url">$url</a>.}{ $note.}',
129 u'@techreport':u'$authors: <i>$title</i>, $year.{ URL <a href="$url">$url</a>.}{ $note.}',
130 u'@unpublished':u'$authors: “$title”, <i>$journal</i>, $year.{ URL <a href="$url">$url</a>.}{ $note.}',
131 u'cite':u'$index',
132 u'default':u'$authors: <i>$title</i>.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
135 defaulttags = {
136 u'YY':u'??', u'authors':u'', u'surname':u'',
139 ieeetr = {
141 u'@article':u'$authors, “$title”, <i>$journal</i>, vol. $volume, no. $number, pp. $pages, $year.{ URL <a href="$url">$url</a>.}{ $note.}',
142 u'@book':u'$authors, <i>$title</i>. $publisher, $year.{ URL <a href="$url">$url</a>.}{ $note.}',
143 u'cite':u'$index',
144 u'default':u'$authors, “$title”. $year.{ URL <a href="$url">$url</a>.}{ $note.}',
147 plain = {
149 u'@article':u'$authors. $title.{ <i>$journal</i>{, {$volume}{($number)}}{:$pages}{, $year}.}{ URL <a href="$url">$url</a>.}{ $note.}',
150 u'@book':u'$authors. <i>$title</i>. $publisher,{ $month} $year.{ URL <a href="$url">$url</a>.}{ $note.}',
151 u'@incollection':u'$authors. $title.{ In <i>$booktitle</i> {($editor, ed.)}.} $publisher,{ $month} $year.{ URL <a href="$url">$url</a>.}{ $note.}',
152 u'@inproceedings':u'$authors. $title. { <i>$booktitle</i>{, {$volume}{($number)}}{:$pages}{, $year}.}{ URL <a href="$url">$url</a>.}{ $note.}',
153 u'cite':u'$index',
154 u'default':u'{$authors. }$title.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
157 vancouver = {
159 u'@article':u'$authors. $title. <i>$journal</i>, $year{;{<b>$volume</b>}{($number)}{:$pages}}.{ URL: <a href="$url">$url</a>.}{ $note.}',
160 u'@book':u'$authors. $title. {$publisher, }$year.{ URL: <a href="$url">$url</a>.}{ $note.}',
161 u'cite':u'$index',
162 u'default':u'$authors. $title; {$publisher, }$year.{ $howpublished.}{ URL: <a href="$url">$url</a>.}{ $note.}',
165 class BibTeXConfig(object):
166 "Configuration class from elyxer.config file"
168 replaced = {
169 u'--':u'—', u'..':u'.',
172 class ContainerConfig(object):
173 "Configuration class from elyxer.config file"
175 endings = {
176 u'Align':u'\\end_layout', u'BarredText':u'\\bar',
177 u'BoldText':u'\\series', u'Cell':u'</cell',
178 u'ChangeDeleted':u'\\change_unchanged',
179 u'ChangeInserted':u'\\change_unchanged', u'ColorText':u'\\color',
180 u'EmphaticText':u'\\emph', u'Hfill':u'\\hfill', u'Inset':u'\\end_inset',
181 u'Layout':u'\\end_layout', u'LyXFooter':u'\\end_document',
182 u'LyXHeader':u'\\end_header', u'Row':u'</row', u'ShapedText':u'\\shape',
183 u'SizeText':u'\\size', u'StrikeOut':u'\\strikeout',
184 u'TextFamily':u'\\family', u'VersalitasText':u'\\noun',
187 extracttext = {
188 u'allowed':[u'StringContainer',u'Constant',u'FormulaConstant',],
189 u'cloned':[u'',],
190 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',],
193 startendings = {
194 u'\\begin_deeper':u'\\end_deeper', u'\\begin_inset':u'\\end_inset',
195 u'\\begin_layout':u'\\end_layout',
198 starts = {
199 u'':u'StringContainer', u'#LyX':u'BlackBox', u'</lyxtabular':u'BlackBox',
200 u'<cell':u'Cell', u'<column':u'Column', u'<row':u'Row',
201 u'\\align':u'Align', u'\\bar':u'BarredText',
202 u'\\bar default':u'BlackBox', u'\\bar no':u'BlackBox',
203 u'\\begin_body':u'BlackBox', u'\\begin_deeper':u'DeeperList',
204 u'\\begin_document':u'BlackBox', u'\\begin_header':u'LyXHeader',
205 u'\\begin_inset Argument':u'ShortTitle',
206 u'\\begin_inset Box':u'BoxInset', u'\\begin_inset Branch':u'Branch',
207 u'\\begin_inset Caption':u'Caption',
208 u'\\begin_inset CommandInset bibitem':u'BiblioEntry',
209 u'\\begin_inset CommandInset bibtex':u'BibTeX',
210 u'\\begin_inset CommandInset citation':u'BiblioCitation',
211 u'\\begin_inset CommandInset href':u'URL',
212 u'\\begin_inset CommandInset include':u'IncludeInset',
213 u'\\begin_inset CommandInset index_print':u'PrintIndex',
214 u'\\begin_inset CommandInset label':u'Label',
215 u'\\begin_inset CommandInset line':u'LineInset',
216 u'\\begin_inset CommandInset nomencl_print':u'PrintNomenclature',
217 u'\\begin_inset CommandInset nomenclature':u'NomenclatureEntry',
218 u'\\begin_inset CommandInset ref':u'Reference',
219 u'\\begin_inset CommandInset toc':u'TableOfContents',
220 u'\\begin_inset ERT':u'ERT', u'\\begin_inset Flex':u'FlexInset',
221 u'\\begin_inset Flex Chunkref':u'NewfangledChunkRef',
222 u'\\begin_inset Flex Marginnote':u'SideNote',
223 u'\\begin_inset Flex Sidenote':u'SideNote',
224 u'\\begin_inset Flex URL':u'FlexURL', u'\\begin_inset Float':u'Float',
225 u'\\begin_inset FloatList':u'ListOf', u'\\begin_inset Foot':u'Footnote',
226 u'\\begin_inset Formula':u'Formula',
227 u'\\begin_inset FormulaMacro':u'FormulaMacro',
228 u'\\begin_inset Graphics':u'Image',
229 u'\\begin_inset Index':u'IndexReference',
230 u'\\begin_inset Info':u'InfoInset',
231 u'\\begin_inset LatexCommand bibitem':u'BiblioEntry',
232 u'\\begin_inset LatexCommand bibtex':u'BibTeX',
233 u'\\begin_inset LatexCommand cite':u'BiblioCitation',
234 u'\\begin_inset LatexCommand citealt':u'BiblioCitation',
235 u'\\begin_inset LatexCommand citep':u'BiblioCitation',
236 u'\\begin_inset LatexCommand citet':u'BiblioCitation',
237 u'\\begin_inset LatexCommand htmlurl':u'URL',
238 u'\\begin_inset LatexCommand index':u'IndexReference',
239 u'\\begin_inset LatexCommand label':u'Label',
240 u'\\begin_inset LatexCommand nomenclature':u'NomenclatureEntry',
241 u'\\begin_inset LatexCommand prettyref':u'Reference',
242 u'\\begin_inset LatexCommand printindex':u'PrintIndex',
243 u'\\begin_inset LatexCommand printnomenclature':u'PrintNomenclature',
244 u'\\begin_inset LatexCommand ref':u'Reference',
245 u'\\begin_inset LatexCommand tableofcontents':u'TableOfContents',
246 u'\\begin_inset LatexCommand url':u'URL',
247 u'\\begin_inset LatexCommand vref':u'Reference',
248 u'\\begin_inset Marginal':u'SideNote',
249 u'\\begin_inset Newline':u'NewlineInset',
250 u'\\begin_inset Newpage':u'NewPageInset', u'\\begin_inset Note':u'Note',
251 u'\\begin_inset OptArg':u'ShortTitle',
252 u'\\begin_inset Phantom':u'PhantomText',
253 u'\\begin_inset Quotes':u'QuoteContainer',
254 u'\\begin_inset Tabular':u'Table', u'\\begin_inset Text':u'InsetText',
255 u'\\begin_inset VSpace':u'VerticalSpace', u'\\begin_inset Wrap':u'Wrap',
256 u'\\begin_inset listings':u'Listing', u'\\begin_inset space':u'Space',
257 u'\\begin_layout':u'Layout', u'\\begin_layout Abstract':u'Abstract',
258 u'\\begin_layout Author':u'Author',
259 u'\\begin_layout Bibliography':u'Bibliography',
260 u'\\begin_layout Chunk':u'NewfangledChunk',
261 u'\\begin_layout Description':u'Description',
262 u'\\begin_layout Enumerate':u'ListItem',
263 u'\\begin_layout Itemize':u'ListItem', u'\\begin_layout List':u'List',
264 u'\\begin_layout LyX-Code':u'LyXCode',
265 u'\\begin_layout Plain':u'PlainLayout',
266 u'\\begin_layout Standard':u'StandardLayout',
267 u'\\begin_layout Title':u'Title', u'\\begin_preamble':u'LyXPreamble',
268 u'\\change_deleted':u'ChangeDeleted',
269 u'\\change_inserted':u'ChangeInserted',
270 u'\\change_unchanged':u'BlackBox', u'\\color':u'ColorText',
271 u'\\color inherit':u'BlackBox', u'\\color none':u'BlackBox',
272 u'\\emph default':u'BlackBox', u'\\emph off':u'BlackBox',
273 u'\\emph on':u'EmphaticText', u'\\emph toggle':u'EmphaticText',
274 u'\\end_body':u'LyXFooter', u'\\family':u'TextFamily',
275 u'\\family default':u'BlackBox', u'\\family roman':u'BlackBox',
276 u'\\hfill':u'Hfill', u'\\labelwidthstring':u'BlackBox',
277 u'\\lang':u'LangLine', u'\\length':u'InsetLength',
278 u'\\lyxformat':u'LyXFormat', u'\\lyxline':u'LyXLine',
279 u'\\newline':u'Newline', u'\\newpage':u'NewPage',
280 u'\\noindent':u'BlackBox', u'\\noun default':u'BlackBox',
281 u'\\noun off':u'BlackBox', u'\\noun on':u'VersalitasText',
282 u'\\paragraph_spacing':u'BlackBox', u'\\series bold':u'BoldText',
283 u'\\series default':u'BlackBox', u'\\series medium':u'BlackBox',
284 u'\\shape':u'ShapedText', u'\\shape default':u'BlackBox',
285 u'\\shape up':u'BlackBox', u'\\size':u'SizeText',
286 u'\\size normal':u'BlackBox', u'\\start_of_appendix':u'StartAppendix',
287 u'\\strikeout default':u'BlackBox', u'\\strikeout on':u'StrikeOut',
290 string = {
291 u'startcommand':u'\\',
294 table = {
295 u'headers':[u'<lyxtabular',u'<features',],
298 class EscapeConfig(object):
299 "Configuration class from elyxer.config file"
301 chars = {
302 u'\n':u'', u' -- ':u' — ', u'\'':u'’', u'---':u'—', u'`':u'‘',
305 commands = {
306 u'\\InsetSpace \\space{}':u' ', u'\\InsetSpace \\thinspace{}':u' ',
307 u'\\InsetSpace ~':u' ', u'\\SpecialChar \\-':u'',
308 u'\\SpecialChar \\@.':u'.', u'\\SpecialChar \\ldots{}':u'…',
309 u'\\SpecialChar \\menuseparator':u' ▷ ',
310 u'\\SpecialChar \\nobreakdash-':u'-', u'\\SpecialChar \\slash{}':u'/',
311 u'\\SpecialChar \\textcompwordmark{}':u'', u'\\backslash':u'\\',
314 entities = {
315 u'&':u'&amp;', u'<':u'&lt;', u'>':u'&gt;',
318 html = {
319 u'/>':u'>',
322 iso885915 = {
323 u' ':u'&nbsp;', u' ':u'&emsp;', u' ':u'&#8197;',
326 nonunicode = {
327 u' ':u' ',
330 class FormulaConfig(object):
331 "Configuration class from elyxer.config file"
333 alphacommands = {
334 u'\\AA':u'Å', u'\\AE':u'Æ',
335 u'\\AmS':u'<span class="versalitas">AmS</span>', u'\\DH':u'Ð',
336 u'\\L':u'Ł', u'\\O':u'Ø', u'\\OE':u'Œ', u'\\TH':u'Þ', u'\\aa':u'å',
337 u'\\ae':u'æ', u'\\alpha':u'α', u'\\beta':u'β', u'\\delta':u'δ',
338 u'\\dh':u'ð', u'\\epsilon':u'ϵ', u'\\eta':u'η', u'\\gamma':u'γ',
339 u'\\i':u'ı', u'\\imath':u'ı', u'\\iota':u'ι', u'\\j':u'ȷ',
340 u'\\jmath':u'ȷ', u'\\kappa':u'κ', u'\\l':u'ł', u'\\lambda':u'λ',
341 u'\\mu':u'μ', u'\\nu':u'ν', u'\\o':u'ø', u'\\oe':u'œ', u'\\omega':u'ω',
342 u'\\phi':u'φ', u'\\pi':u'π', u'\\psi':u'ψ', u'\\rho':u'ρ',
343 u'\\sigma':u'σ', u'\\ss':u'ß', u'\\tau':u'τ', u'\\textcrh':u'ħ',
344 u'\\th':u'þ', u'\\theta':u'θ', u'\\upsilon':u'υ', u'\\varDelta':u'∆',
345 u'\\varGamma':u'Γ', u'\\varLambda':u'Λ', u'\\varOmega':u'Ω',
346 u'\\varPhi':u'Φ', u'\\varPi':u'Π', u'\\varPsi':u'Ψ', u'\\varSigma':u'Σ',
347 u'\\varTheta':u'Θ', u'\\varUpsilon':u'Υ', u'\\varXi':u'Ξ',
348 u'\\varepsilon':u'ε', u'\\varkappa':u'ϰ', u'\\varphi':u'φ',
349 u'\\varpi':u'ϖ', u'\\varrho':u'ϱ', u'\\varsigma':u'ς',
350 u'\\vartheta':u'ϑ', u'\\xi':u'ξ', u'\\zeta':u'ζ',
353 array = {
354 u'begin':u'\\begin', u'cellseparator':u'&', u'end':u'\\end',
355 u'rowseparator':u'\\\\',
358 bigbrackets = {
359 u'(':[u'⎛',u'⎜',u'⎝',], u')':[u'⎞',u'⎟',u'⎠',], u'[':[u'⎡',u'⎢',u'⎣',],
360 u']':[u'⎤',u'⎥',u'⎦',], u'{':[u'⎧',u'⎪',u'⎨',u'⎩',], u'|':[u'|',],
361 u'}':[u'⎫',u'⎪',u'⎬',u'⎭',], u'∥':[u'∥',],
364 bigsymbols = {
365 u'∑':[u'⎲',u'⎳',], u'∫':[u'⌠',u'⌡',],
368 bracketcommands = {
369 u'\\left':u'span class="symbol"',
370 u'\\left.':u'<span class="leftdot"></span>',
371 u'\\middle':u'span class="symbol"', u'\\right':u'span class="symbol"',
372 u'\\right.':u'<span class="rightdot"></span>',
375 combiningfunctions = {
376 u'\\"':u'̈', u'\\\'':u'́', u'\\^':u'̂', u'\\`':u'̀', u'\\acute':u'́',
377 u'\\bar':u'̄', u'\\breve':u'̆', u'\\c':u'̧', u'\\check':u'̌',
378 u'\\dddot':u'⃛', u'\\ddot':u'̈', u'\\dot':u'̇', u'\\grave':u'̀',
379 u'\\hat':u'̂', u'\\mathring':u'̊', u'\\overleftarrow':u'⃖',
380 u'\\overrightarrow':u'⃗', u'\\r':u'̊', u'\\s':u'̩',
381 u'\\textcircled':u'⃝', u'\\textsubring':u'̥', u'\\tilde':u'̃',
382 u'\\v':u'̌', u'\\vec':u'⃗', u'\\~':u'̃',
385 commands = {
386 u'\\ ':u' ', u'\\!':u'', u'\\#':u'#', u'\\$':u'$', u'\\%':u'%',
387 u'\\&':u'&', u'\\,':u' ', u'\\:':u' ', u'\\;':u' ',
388 u'\\APLdownarrowbox':u'⍗', u'\\APLleftarrowbox':u'⍇',
389 u'\\APLrightarrowbox':u'⍈', u'\\APLuparrowbox':u'⍐', u'\\Box':u'□',
390 u'\\Bumpeq':u'≎', u'\\CIRCLE':u'●', u'\\Cap':u'⋒', u'\\CheckedBox':u'☑',
391 u'\\Circle':u'○', u'\\Coloneqq':u'⩴', u'\\Corresponds':u'≙',
392 u'\\Cup':u'⋓', u'\\Delta':u'Δ', u'\\Diamond':u'◇', u'\\Downarrow':u'⇓',
393 u'\\EUR':u'€', u'\\Game':u'⅁', u'\\Gamma':u'Γ', u'\\Im':u'ℑ',
394 u'\\Join':u'⨝', u'\\LEFTCIRCLE':u'◖', u'\\LEFTcircle':u'◐',
395 u'\\Lambda':u'Λ', u'\\Leftarrow':u'⇐', u'\\Lleftarrow':u'⇚',
396 u'\\Longleftarrow':u'⟸', u'\\Longleftrightarrow':u'⟺',
397 u'\\Longrightarrow':u'⟹', u'\\Lsh':u'↰', u'\\Mapsfrom':u'⇐|',
398 u'\\Mapsto':u'|⇒', u'\\Omega':u'Ω', u'\\P':u'¶', u'\\Phi':u'Φ',
399 u'\\Pi':u'Π', u'\\Pr':u'Pr', u'\\Psi':u'Ψ', u'\\RIGHTCIRCLE':u'◗',
400 u'\\RIGHTcircle':u'◑', u'\\Re':u'ℜ', u'\\Rrightarrow':u'⇛',
401 u'\\Rsh':u'↱', u'\\S':u'§', u'\\Sigma':u'Σ', u'\\Square':u'☐',
402 u'\\Subset':u'⋐', u'\\Supset':u'⋑', u'\\Theta':u'Θ', u'\\Uparrow':u'⇑',
403 u'\\Updownarrow':u'⇕', u'\\Upsilon':u'Υ', u'\\Vdash':u'⊩',
404 u'\\Vert':u'∥', u'\\Vvdash':u'⊪', u'\\XBox':u'☒', u'\\Xi':u'Ξ',
405 u'\\Yup':u'⅄', u'\\\\':u'<br/>', u'\\_':u'_', u'\\aleph':u'ℵ',
406 u'\\amalg':u'∐', u'\\angle':u'∠', u'\\aquarius':u'♒',
407 u'\\arccos':u'arccos', u'\\arcsin':u'arcsin', u'\\arctan':u'arctan',
408 u'\\arg':u'arg', u'\\aries':u'♈', u'\\ast':u'∗', u'\\asymp':u'≍',
409 u'\\backepsilon':u'∍', u'\\backprime':u'‵', u'\\backsimeq':u'⋍',
410 u'\\backslash':u'\\', u'\\barwedge':u'⊼', u'\\because':u'∵',
411 u'\\beth':u'ℶ', u'\\between':u'≬', u'\\bigcap':u'∩', u'\\bigcirc':u'○',
412 u'\\bigcup':u'∪', u'\\bigodot':u'⊙', u'\\bigoplus':u'⊕',
413 u'\\bigotimes':u'⊗', u'\\bigsqcup':u'⊔', u'\\bigstar':u'★',
414 u'\\bigtriangledown':u'▽', u'\\bigtriangleup':u'△', u'\\biguplus':u'⊎',
415 u'\\bigvee':u'∨', u'\\bigwedge':u'∧', u'\\blacklozenge':u'⧫',
416 u'\\blacksmiley':u'☻', u'\\blacksquare':u'■', u'\\blacktriangle':u'▲',
417 u'\\blacktriangledown':u'▼', u'\\blacktriangleright':u'▶', u'\\bot':u'⊥',
418 u'\\bowtie':u'⋈', u'\\box':u'▫', u'\\boxdot':u'⊡', u'\\bullet':u'•',
419 u'\\bumpeq':u'≏', u'\\cancer':u'♋', u'\\cap':u'∩', u'\\capricornus':u'♑',
420 u'\\cdot':u'⋅', u'\\cdots':u'⋯', u'\\centerdot':u'∙',
421 u'\\checkmark':u'✓', u'\\chi':u'χ', u'\\circ':u'○', u'\\circeq':u'≗',
422 u'\\circledR':u'®', u'\\circledast':u'⊛', u'\\circledcirc':u'⊚',
423 u'\\circleddash':u'⊝', u'\\clubsuit':u'♣', u'\\coloneqq':u'≔',
424 u'\\complement':u'∁', u'\\cong':u'≅', u'\\coprod':u'∐',
425 u'\\copyright':u'©', u'\\cos':u'cos', u'\\cosh':u'cosh', u'\\cot':u'cot',
426 u'\\coth':u'coth', u'\\csc':u'csc', u'\\cup':u'∪',
427 u'\\curvearrowleft':u'↶', u'\\curvearrowright':u'↷', u'\\dag':u'†',
428 u'\\dagger':u'†', u'\\daleth':u'ℸ', u'\\dashleftarrow':u'⇠',
429 u'\\dashv':u'⊣', u'\\ddag':u'‡', u'\\ddagger':u'‡', u'\\ddots':u'⋱',
430 u'\\deg':u'deg', u'\\det':u'det', u'\\diagdown':u'╲', u'\\diagup':u'╱',
431 u'\\diamond':u'◇', u'\\diamondsuit':u'♦', u'\\dim':u'dim', u'\\div':u'÷',
432 u'\\divideontimes':u'⋇', u'\\dotdiv':u'∸', u'\\doteq':u'≐',
433 u'\\doteqdot':u'≑', u'\\dotplus':u'∔', u'\\dots':u'…',
434 u'\\doublebarwedge':u'⌆', u'\\downarrow':u'↓', u'\\downdownarrows':u'⇊',
435 u'\\downharpoonleft':u'⇃', u'\\downharpoonright':u'⇂', u'\\earth':u'♁',
436 u'\\ell':u'ℓ', u'\\emptyset':u'∅', u'\\eqcirc':u'≖', u'\\eqcolon':u'≕',
437 u'\\eqsim':u'≂', u'\\euro':u'€', u'\\exists':u'∃', u'\\exp':u'exp',
438 u'\\fallingdotseq':u'≒', u'\\female':u'♀', u'\\flat':u'♭',
439 u'\\forall':u'∀', u'\\frown':u'⌢', u'\\frownie':u'☹', u'\\gcd':u'gcd',
440 u'\\gemini':u'♊', u'\\geq)':u'≥', u'\\geqq':u'≧', u'\\geqslant':u'≥',
441 u'\\gets':u'←', u'\\gg':u'≫', u'\\ggg':u'⋙', u'\\gimel':u'ℷ',
442 u'\\gneqq':u'≩', u'\\gnsim':u'⋧', u'\\gtrdot':u'⋗', u'\\gtreqless':u'⋚',
443 u'\\gtreqqless':u'⪌', u'\\gtrless':u'≷', u'\\gtrsim':u'≳',
444 u'\\guillemotleft':u'«', u'\\guillemotright':u'»', u'\\hbar':u'ℏ',
445 u'\\heartsuit':u'♥', u'\\hfill':u'<span class="hfill"> </span>',
446 u'\\hom':u'hom', u'\\hookleftarrow':u'↩', u'\\hookrightarrow':u'↪',
447 u'\\hslash':u'ℏ', u'\\idotsint':u'<span class="bigsymbol">∫⋯∫</span>',
448 u'\\iiint':u'<span class="bigsymbol">∭</span>',
449 u'\\iint':u'<span class="bigsymbol">∬</span>', u'\\imath':u'ı',
450 u'\\inf':u'inf', u'\\infty':u'∞', u'\\invneg':u'⌐', u'\\jmath':u'ȷ',
451 u'\\jupiter':u'♃', u'\\ker':u'ker', u'\\land':u'∧',
452 u'\\landupint':u'<span class="bigsymbol">∱</span>', u'\\langle':u'⟨',
453 u'\\lbrace':u'{', u'\\lbrace)':u'{', u'\\lbrack':u'[', u'\\lceil':u'⌈',
454 u'\\ldots':u'…', u'\\leadsto':u'⇝', u'\\leftarrow)':u'←',
455 u'\\leftarrowtail':u'↢', u'\\leftarrowtobar':u'⇤',
456 u'\\leftharpoondown':u'↽', u'\\leftharpoonup':u'↼',
457 u'\\leftleftarrows':u'⇇', u'\\leftleftharpoons':u'⥢', u'\\leftmoon':u'☾',
458 u'\\leftrightarrow':u'↔', u'\\leftrightarrows':u'⇆',
459 u'\\leftrightharpoons':u'⇋', u'\\leftthreetimes':u'⋋', u'\\leo':u'♌',
460 u'\\leq)':u'≤', u'\\leqq':u'≦', u'\\leqslant':u'≤', u'\\lessdot':u'⋖',
461 u'\\lesseqgtr':u'⋛', u'\\lesseqqgtr':u'⪋', u'\\lessgtr':u'≶',
462 u'\\lesssim':u'≲', u'\\lfloor':u'⌊', u'\\lg':u'lg', u'\\lhd':u'⊲',
463 u'\\libra':u'♎', u'\\lightning':u'↯', u'\\liminf':u'liminf',
464 u'\\limsup':u'limsup', u'\\ll':u'≪', u'\\lll':u'⋘', u'\\ln':u'ln',
465 u'\\lneqq':u'≨', u'\\lnot':u'¬', u'\\lnsim':u'⋦', u'\\log':u'log',
466 u'\\longleftarrow':u'⟵', u'\\longleftrightarrow':u'⟷',
467 u'\\longmapsto':u'⟼', u'\\longrightarrow':u'⟶', u'\\looparrowleft':u'↫',
468 u'\\looparrowright':u'↬', u'\\lor':u'∨', u'\\lozenge':u'◊',
469 u'\\ltimes':u'⋉', u'\\lyxlock':u'', u'\\male':u'♂', u'\\maltese':u'✠',
470 u'\\mapsfrom':u'↤', u'\\mapsto':u'↦', u'\\mathcircumflex':u'^',
471 u'\\max':u'max', u'\\measuredangle':u'∡', u'\\mercury':u'☿',
472 u'\\mho':u'℧', u'\\mid':u'∣', u'\\min':u'min', u'\\models':u'⊨',
473 u'\\mp':u'∓', u'\\multimap':u'⊸', u'\\nLeftarrow':u'⇍',
474 u'\\nLeftrightarrow':u'⇎', u'\\nRightarrow':u'⇏', u'\\nVDash':u'⊯',
475 u'\\nabla':u'∇', u'\\napprox':u'≉', u'\\natural':u'♮', u'\\ncong':u'≇',
476 u'\\nearrow':u'↗', u'\\neg':u'¬', u'\\neg)':u'¬', u'\\neptune':u'♆',
477 u'\\nequiv':u'≢', u'\\newline':u'<br/>', u'\\nexists':u'∄',
478 u'\\ngeqslant':u'≱', u'\\ngtr':u'≯', u'\\ngtrless':u'≹', u'\\ni':u'∋',
479 u'\\ni)':u'∋', u'\\nleftarrow':u'↚', u'\\nleftrightarrow':u'↮',
480 u'\\nleqslant':u'≰', u'\\nless':u'≮', u'\\nlessgtr':u'≸', u'\\nmid':u'∤',
481 u'\\nolimits':u'', u'\\nonumber':u'', u'\\not':u'¬', u'\\not<':u'≮',
482 u'\\not=':u'≠', u'\\not>':u'≯', u'\\notbackslash':u'⍀', u'\\notin':u'∉',
483 u'\\notni':u'∌', u'\\notslash':u'⌿', u'\\nparallel':u'∦',
484 u'\\nprec':u'⊀', u'\\nrightarrow':u'↛', u'\\nsim':u'≁', u'\\nsimeq':u'≄',
485 u'\\nsqsubset':u'⊏̸', u'\\nsubseteq':u'⊈', u'\\nsucc':u'⊁',
486 u'\\nsucccurlyeq':u'⋡', u'\\nsupset':u'⊅', u'\\nsupseteq':u'⊉',
487 u'\\ntriangleleft':u'⋪', u'\\ntrianglelefteq':u'⋬',
488 u'\\ntriangleright':u'⋫', u'\\ntrianglerighteq':u'⋭', u'\\nvDash':u'⊭',
489 u'\\nvdash':u'⊬', u'\\nwarrow':u'↖', u'\\odot':u'⊙',
490 u'\\officialeuro':u'€', u'\\oiiint':u'<span class="bigsymbol">∰</span>',
491 u'\\oiint':u'<span class="bigsymbol">∯</span>',
492 u'\\oint':u'<span class="bigsymbol">∮</span>',
493 u'\\ointclockwise':u'<span class="bigsymbol">∲</span>',
494 u'\\ointctrclockwise':u'<span class="bigsymbol">∳</span>',
495 u'\\ominus':u'⊖', u'\\oplus':u'⊕', u'\\oslash':u'⊘', u'\\otimes':u'⊗',
496 u'\\owns':u'∋', u'\\parallel':u'∥', u'\\partial':u'∂', u'\\perp':u'⊥',
497 u'\\pisces':u'♓', u'\\pitchfork':u'⋔', u'\\pluto':u'♇', u'\\pm':u'±',
498 u'\\pointer':u'➪', u'\\pounds':u'£', u'\\prec':u'≺',
499 u'\\preccurlyeq':u'≼', u'\\preceq':u'≼', u'\\precsim':u'≾',
500 u'\\prime':u'′', u'\\prompto':u'∝', u'\\qquad':u' ', u'\\quad':u' ',
501 u'\\quarternote':u'♩', u'\\rangle':u'⟩', u'\\rbrace':u'}',
502 u'\\rbrace)':u'}', u'\\rbrack':u']', u'\\rceil':u'⌉', u'\\rfloor':u'⌋',
503 u'\\rhd':u'⊳', u'\\rightarrow)':u'→', u'\\rightarrowtail':u'↣',
504 u'\\rightarrowtobar':u'⇥', u'\\rightharpoondown':u'⇁',
505 u'\\rightharpoonup':u'⇀', u'\\rightharpooondown':u'⇁',
506 u'\\rightharpooonup':u'⇀', u'\\rightleftarrows':u'⇄',
507 u'\\rightleftharpoons':u'⇌', u'\\rightmoon':u'☽',
508 u'\\rightrightarrows':u'⇉', u'\\rightrightharpoons':u'⥤',
509 u'\\rightthreetimes':u'⋌', u'\\risingdotseq':u'≓', u'\\rtimes':u'⋊',
510 u'\\sagittarius':u'♐', u'\\saturn':u'♄', u'\\scorpio':u'♏',
511 u'\\searrow':u'↘', u'\\sec':u'sec', u'\\setminus':u'∖', u'\\sharp':u'♯',
512 u'\\simeq':u'≃', u'\\sin':u'sin', u'\\sinh':u'sinh', u'\\slash':u'∕',
513 u'\\smile':u'⌣', u'\\smiley':u'☺', u'\\spadesuit':u'♠',
514 u'\\sphericalangle':u'∢', u'\\sqcap':u'⊓', u'\\sqcup':u'⊔',
515 u'\\sqsubset':u'⊏', u'\\sqsubseteq':u'⊑', u'\\sqsupset':u'⊐',
516 u'\\sqsupseteq':u'⊒', u'\\square':u'□', u'\\star':u'⋆',
517 u'\\subseteqq':u'⫅', u'\\subsetneqq':u'⫋', u'\\succ':u'≻',
518 u'\\succcurlyeq':u'≽', u'\\succeq':u'≽', u'\\succnsim':u'⋩',
519 u'\\succsim':u'≿', u'\\sun':u'☼', u'\\sup':u'sup', u'\\supseteqq':u'⫆',
520 u'\\supsetneqq':u'⫌', u'\\surd':u'√', u'\\swarrow':u'↙', u'\\tan':u'tan',
521 u'\\tanh':u'tanh', u'\\taurus':u'♉', u'\\textasciicircum':u'^',
522 u'\\textasciitilde':u'~', u'\\textbackslash':u'\\',
523 u'\\textcopyright':u\'', u'\\textdegree':u'°', u'\\textellipsis':u'…',
524 u'\\textemdash':u'—', u'\\textendash':u'—', u'\\texteuro':u'€',
525 u'\\textgreater':u'>', u'\\textless':u'<', u'\\textordfeminine':u'ª',
526 u'\\textordmasculine':u'º', u'\\textquotedblleft':u'“',
527 u'\\textquotedblright':u'”', u'\\textquoteright':u'’',
528 u'\\textregistered':u'®', u'\\textrightarrow':u'→',
529 u'\\textsection':u'§', u'\\texttrademark':u'™',
530 u'\\texttwosuperior':u'²', u'\\textvisiblespace':u' ',
531 u'\\therefore':u'∴', u'\\top':u'⊤', u'\\triangle':u'△',
532 u'\\triangleleft':u'⊲', u'\\trianglelefteq':u'⊴', u'\\triangleq':u'≜',
533 u'\\triangleright':u'▷', u'\\trianglerighteq':u'⊵',
534 u'\\twoheadleftarrow':u'↞', u'\\twoheadrightarrow':u'↠',
535 u'\\twonotes':u'♫', u'\\udot':u'⊍', u'\\unlhd':u'⊴', u'\\unrhd':u'⊵',
536 u'\\unrhl':u'⊵', u'\\uparrow':u'↑', u'\\updownarrow':u'↕',
537 u'\\upharpoonleft':u'↿', u'\\upharpoonright':u'↾', u'\\uplus':u'⊎',
538 u'\\upuparrows':u'⇈', u'\\uranus':u'♅', u'\\vDash':u'⊨',
539 u'\\varclubsuit':u'♧', u'\\vardiamondsuit':u'♦', u'\\varheartsuit':u'♥',
540 u'\\varnothing':u'∅', u'\\varspadesuit':u'♤', u'\\vdash':u'⊢',
541 u'\\vdots':u'⋮', u'\\vee':u'∨', u'\\vee)':u'∨', u'\\veebar':u'⊻',
542 u'\\vert':u'∣', u'\\virgo':u'♍', u'\\wedge':u'∧', u'\\wedge)':u'∧',
543 u'\\wp':u'℘', u'\\wr':u'≀', u'\\yen':u'¥', u'\\{':u'{', u'\\|':u'∥',
544 u'\\}':u'}',
547 decoratedcommand = {
551 decoratingfunctions = {
552 u'\\overleftarrow':u'⟵', u'\\overrightarrow':u'⟶', u'\\widehat':u'^',
555 endings = {
556 u'bracket':u'}', u'complex':u'\\]', u'endafter':u'}',
557 u'endbefore':u'\\end{', u'squarebracket':u']',
560 environments = {
561 u'align':[u'r',u'l',], u'eqnarray':[u'r',u'c',u'l',],
562 u'gathered':[u'l',u'l',],
565 fontfunctions = {
566 u'\\boldsymbol':u'b', u'\\mathbb':u'span class="blackboard"',
567 u'\\mathbb{A}':u'𝔸', u'\\mathbb{B}':u'𝔹', u'\\mathbb{C}':u'ℂ',
568 u'\\mathbb{D}':u'𝔻', u'\\mathbb{E}':u'𝔼', u'\\mathbb{F}':u'𝔽',
569 u'\\mathbb{G}':u'𝔾', u'\\mathbb{H}':u'ℍ', u'\\mathbb{J}':u'𝕁',
570 u'\\mathbb{K}':u'𝕂', u'\\mathbb{L}':u'𝕃', u'\\mathbb{N}':u'ℕ',
571 u'\\mathbb{O}':u'𝕆', u'\\mathbb{P}':u'ℙ', u'\\mathbb{Q}':u'ℚ',
572 u'\\mathbb{R}':u'ℝ', u'\\mathbb{S}':u'𝕊', u'\\mathbb{T}':u'𝕋',
573 u'\\mathbb{W}':u'𝕎', u'\\mathbb{Z}':u'ℤ', u'\\mathbf':u'b',
574 u'\\mathcal':u'span class="scriptfont"', u'\\mathcal{B}':u'ℬ',
575 u'\\mathcal{E}':u'ℰ', u'\\mathcal{F}':u'ℱ', u'\\mathcal{H}':u'ℋ',
576 u'\\mathcal{I}':u'ℐ', u'\\mathcal{L}':u'ℒ', u'\\mathcal{M}':u'ℳ',
577 u'\\mathcal{R}':u'ℛ', u'\\mathfrak':u'span class="fraktur"',
578 u'\\mathfrak{C}':u'ℭ', u'\\mathfrak{F}':u'𝔉', u'\\mathfrak{H}':u'ℌ',
579 u'\\mathfrak{I}':u'ℑ', u'\\mathfrak{R}':u'ℜ', u'\\mathfrak{Z}':u'ℨ',
580 u'\\mathit':u'i', u'\\mathring{A}':u'Å', u'\\mathring{U}':u'Ů',
581 u'\\mathring{a}':u'å', u'\\mathring{u}':u'ů', u'\\mathring{w}':u'ẘ',
582 u'\\mathring{y}':u'ẙ', u'\\mathrm':u'span class="mathrm"',
583 u'\\mathscr':u'span class="scriptfont"', u'\\mathscr{B}':u'ℬ',
584 u'\\mathscr{E}':u'ℰ', u'\\mathscr{F}':u'ℱ', u'\\mathscr{H}':u'ℋ',
585 u'\\mathscr{I}':u'ℐ', u'\\mathscr{L}':u'ℒ', u'\\mathscr{M}':u'ℳ',
586 u'\\mathscr{R}':u'ℛ', u'\\mathsf':u'span class="mathsf"',
587 u'\\mathtt':u'tt',
590 hybridfunctions = {
592 u'\\binom':[u'{$1}{$2}',u'f2{(}f0{f1{$1}f1{$2}}f2{)}',u'span class="binom"',u'span class="binomstack"',u'span class="bigsymbol"',],
593 u'\\boxed':[u'{$1}',u'f0{$1}',u'span class="boxed"',],
594 u'\\cfrac':[u'[$p!]{$1}{$2}',u'f0{f3{(}f1{$1}f3{)/(}f2{$2}f3{)}}',u'span class="fullfraction"',u'span class="numerator align-$p"',u'span class="denominator"',u'span class="ignored"',],
595 u'\\color':[u'{$p!}{$1}',u'f0{$1}',u'span style="color: $p;"',],
596 u'\\colorbox':[u'{$p!}{$1}',u'f0{$1}',u'span class="colorbox" style="background: $p;"',],
597 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"',],
598 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"',],
599 u'\\displaystyle':[u'{$1}',u'f0{$1}',u'span class="displaystyle"',],
600 u'\\fbox':[u'{$1}',u'f0{$1}',u'span class="fbox"',],
601 u'\\fboxrule':[u'{$p!}',u'f0{}',u'ignored',],
602 u'\\fboxsep':[u'{$p!}',u'f0{}',u'ignored',],
603 u'\\fcolorbox':[u'{$p!}{$q!}{$1}',u'f0{$1}',u'span class="boxed" style="border-color: $p; background: $q;"',],
604 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"',],
605 u'\\framebox':[u'[$p!][$q!]{$1}',u'f0{$1}',u'span class="framebox align-$q" style="width: $p;"',],
606 u'\\href':[u'[$o]{$u!}{$t!}',u'f0{$t}',u'a href="$u"',],
607 u'\\hspace':[u'{$p!}',u'f0{ }',u'span class="hspace" style="width: $p;"',],
608 u'\\leftroot':[u'{$p!}',u'f0{ }',u'span class="leftroot" style="width: $p;px"',],
609 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"',],
610 u'\\parbox':[u'[$p!]{$w!}{$1}',u'f0{1}',u'div class="Boxed" style="width: $w;"',],
611 u'\\raisebox':[u'{$p!}{$1}',u'f0{$1.font}',u'span class="raisebox" style="vertical-align: $p;"',],
612 u'\\renewenvironment':[u'{$1!}{$2!}{$3!}',u'',],
613 u'\\rule':[u'[$v!]{$w!}{$h!}',u'f0/',u'hr class="line" style="width: $w; height: $h;"',],
614 u'\\scriptscriptstyle':[u'{$1}',u'f0{$1}',u'span class="scriptscriptstyle"',],
615 u'\\scriptstyle':[u'{$1}',u'f0{$1}',u'span class="scriptstyle"',],
616 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"',],
617 u'\\stackrel':[u'{$1}{$2}',u'f0{f1{$1}f2{$2}}',u'span class="stackrel"',u'span class="upstackrel"',u'span class="downstackrel"',],
618 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"',],
619 u'\\textcolor':[u'{$p!}{$1}',u'f0{$1}',u'span style="color: $p;"',],
620 u'\\textstyle':[u'{$1}',u'f0{$1}',u'span class="textstyle"',],
621 u'\\unit':[u'[$0]{$1}',u'$0f0{$1.font}',u'span class="unit"',],
622 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"',],
623 u'\\uproot':[u'{$p!}',u'f0{ }',u'span class="uproot" style="width: $p;px"',],
624 u'\\url':[u'{$u!}',u'f0{$u}',u'a href="$u"',],
625 u'\\vspace':[u'{$p!}',u'f0{ }',u'span class="vspace" style="height: $p;"',],
628 hybridsizes = {
629 u'\\binom':u'$1+$2', u'\\cfrac':u'$1+$2', u'\\dbinom':u'$1+$2+1',
630 u'\\dfrac':u'$1+$2', u'\\frac':u'$1+$2', u'\\tbinom':u'$1+$2+1',
633 labelfunctions = {
634 u'\\label':u'a name="#"',
637 limitcommands = {
638 u'\\int':u'∫', u'\\intop':u'∫', u'\\lim':u'lim', u'\\prod':u'∏',
639 u'\\smallint':u'∫', u'\\sum':u'∑',
642 misccommands = {
643 u'\\limits':u'LimitPreviousCommand', u'\\newcommand':u'MacroDefinition',
644 u'\\renewcommand':u'MacroDefinition',
645 u'\\setcounter':u'SetCounterFunction', u'\\tag':u'FormulaTag',
646 u'\\tag*':u'FormulaTag',
649 modified = {
650 u'\n':u'', u' ':u'', u'$':u'', u'&':u' ', u'\'':u'’', u'+':u' + ',
651 u',':u', ', u'-':u' − ', u'/':u' ⁄ ', u'<':u' &lt; ', u'=':u' = ',
652 u'>':u' &gt; ', u'@':u'', u'~':u'',
655 onefunctions = {
656 u'\\Big':u'span class="bigsymbol"', u'\\Bigg':u'span class="hugesymbol"',
657 u'\\bar':u'span class="bar"', u'\\begin{array}':u'span class="arraydef"',
658 u'\\big':u'span class="symbol"', u'\\bigg':u'span class="largesymbol"',
659 u'\\bigl':u'span class="bigsymbol"', u'\\bigr':u'span class="bigsymbol"',
660 u'\\centering':u'span class="align-center"',
661 u'\\ensuremath':u'span class="ensuremath"',
662 u'\\hphantom':u'span class="phantom"',
663 u'\\noindent':u'span class="noindent"',
664 u'\\overbrace':u'span class="overbrace"',
665 u'\\overline':u'span class="overline"',
666 u'\\phantom':u'span class="phantom"',
667 u'\\underbrace':u'span class="underbrace"', u'\\underline':u'u',
668 u'\\vphantom':u'span class="phantom"',
671 spacedcommands = {
672 u'\\Leftrightarrow':u'⇔', u'\\Rightarrow':u'⇒', u'\\approx':u'≈',
673 u'\\dashrightarrow':u'⇢', u'\\equiv':u'≡', u'\\ge':u'≥', u'\\geq':u'≥',
674 u'\\implies':u' ⇒ ', u'\\in':u'∈', u'\\le':u'≤', u'\\leftarrow':u'←',
675 u'\\leq':u'≤', u'\\ne':u'≠', u'\\neq':u'≠', u'\\not\\in':u'∉',
676 u'\\propto':u'∝', u'\\rightarrow':u'→', u'\\rightsquigarrow':u'⇝',
677 u'\\sim':u'~', u'\\subset':u'⊂', u'\\subseteq':u'⊆', u'\\supset':u'⊃',
678 u'\\supseteq':u'⊇', u'\\times':u'×', u'\\to':u'→',
681 starts = {
682 u'beginafter':u'}', u'beginbefore':u'\\begin{', u'bracket':u'{',
683 u'command':u'\\', u'comment':u'%', u'complex':u'\\[', u'simple':u'$',
684 u'squarebracket':u'[', u'unnumbered':u'*',
687 symbolfunctions = {
688 u'^':u'sup', u'_':u'sub',
691 textfunctions = {
692 u'\\mbox':u'span class="mbox"', u'\\text':u'span class="text"',
693 u'\\textbf':u'b', u'\\textipa':u'span class="textipa"', u'\\textit':u'i',
694 u'\\textnormal':u'span class="textnormal"',
695 u'\\textrm':u'span class="textrm"',
696 u'\\textsc':u'span class="versalitas"',
697 u'\\textsf':u'span class="textsf"', u'\\textsl':u'i', u'\\texttt':u'tt',
698 u'\\textup':u'span class="normal"',
701 unmodified = {
703 u'characters':[u'.',u'*',u'€',u'(',u')',u'[',u']',u':',u'·',u'!',u';',u'|',u'§',u'"',],
706 urls = {
707 u'googlecharts':u'http://chart.googleapis.com/chart?cht=tx&chl=',
710 class GeneralConfig(object):
711 "Configuration class from elyxer.config file"
713 version = {
714 u'date':u'2011-06-27', u'lyxformat':u'413', u'number':u'1.2.3',
717 class HeaderConfig(object):
718 "Configuration class from elyxer.config file"
720 parameters = {
721 u'beginpreamble':u'\\begin_preamble', u'branch':u'\\branch',
722 u'documentclass':u'\\textclass', u'endbranch':u'\\end_branch',
723 u'endpreamble':u'\\end_preamble', u'language':u'\\language',
724 u'lstset':u'\\lstset', u'outputchanges':u'\\output_changes',
725 u'paragraphseparation':u'\\paragraph_separation',
726 u'pdftitle':u'\\pdf_title', u'secnumdepth':u'\\secnumdepth',
727 u'tocdepth':u'\\tocdepth',
730 styles = {
732 u'article':[u'article',u'aastex',u'aapaper',u'acmsiggraph',u'sigplanconf',u'achemso',u'amsart',u'apa',u'arab-article',u'armenian-article',u'article-beamer',u'chess',u'dtk',u'elsarticle',u'heb-article',u'IEEEtran',u'iopart',u'kluwer',u'scrarticle-beamer',u'scrartcl',u'extarticle',u'paper',u'mwart',u'revtex4',u'spie',u'svglobal3',u'ltugboat',u'agu-dtd',u'jgrga',u'agums',u'entcs',u'egs',u'ijmpc',u'ijmpd',u'singlecol-new',u'doublecol-new',u'isprs',u'tarticle',u'jsarticle',u'jarticle',u'jss',u'literate-article',u'siamltex',u'cl2emult',u'llncs',u'svglobal',u'svjog',u'svprobth',],
733 u'book':[u'book',u'amsbook',u'scrbook',u'extbook',u'tufte-book',u'report',u'extreport',u'scrreprt',u'memoir',u'tbook',u'jsbook',u'jbook',u'mwbk',u'svmono',u'svmult',u'treport',u'jreport',u'mwrep',],
736 class ImageConfig(object):
737 "Configuration class from elyxer.config file"
739 converters = {
741 u'imagemagick':u'convert[ -density $scale][ -define $format:use-cropbox=true] "$input" "$output"',
742 u'inkscape':u'inkscape "$input" --export-png="$output"',
745 cropboxformats = {
746 u'.eps':u'ps', u'.pdf':u'pdf', u'.ps':u'ps',
749 formats = {
750 u'default':u'.png', u'vector':[u'.svg',u'.eps',],
753 class LayoutConfig(object):
754 "Configuration class from elyxer.config file"
756 groupable = {
758 u'allowed':[u'StringContainer',u'Constant',u'TaggedText',u'Align',u'TextFamily',u'EmphaticText',u'VersalitasText',u'BarredText',u'SizeText',u'ColorText',u'LangLine',u'Formula',],
761 class NewfangleConfig(object):
762 "Configuration class from elyxer.config file"
764 constants = {
765 u'chunkref':u'chunkref{', u'endcommand':u'}', u'endmark':u'&gt;',
766 u'startcommand':u'\\', u'startmark':u'=&lt;',
769 class NumberingConfig(object):
770 "Configuration class from elyxer.config file"
772 layouts = {
774 u'ordered':[u'Chapter',u'Section',u'Subsection',u'Subsubsection',u'Paragraph',],
775 u'roman':[u'Part',u'Book',],
778 sequence = {
779 u'symbols':[u'*',u'**',u'†',u'‡',u'§',u'§§',u'¶',u'¶¶',u'#',u'##',],
782 class StyleConfig(object):
783 "Configuration class from elyxer.config file"
785 hspaces = {
786 u'\\enskip{}':u' ', u'\\hfill{}':u'<span class="hfill"> </span>',
787 u'\\hspace*{\\fill}':u' ', u'\\hspace*{}':u'', u'\\hspace{}':u' ',
788 u'\\negthinspace{}':u'', u'\\qquad{}':u'  ', u'\\quad{}':u' ',
789 u'\\space{}':u' ', u'\\thinspace{}':u' ', u'~':u' ',
792 quotes = {
793 u'ald':u'»', u'als':u'›', u'ard':u'«', u'ars':u'‹', u'eld':u'&ldquo;',
794 u'els':u'&lsquo;', u'erd':u'&rdquo;', u'ers':u'&rsquo;', u'fld':u'«',
795 u'fls':u'‹', u'frd':u'»', u'frs':u'›', u'gld':u'„', u'gls':u'‚',
796 u'grd':u'“', u'grs':u'‘', u'pld':u'„', u'pls':u'‚', u'prd':u'”',
797 u'prs':u'’', u'sld':u'”', u'srd':u'”',
800 referenceformats = {
801 u'eqref':u'(@↕)', u'formatted':u'¶↕', u'nameref':u'$↕', u'pageref':u'#↕',
802 u'ref':u'@↕', u'vpageref':u'on-page#↕', u'vref':u'@on-page#↕',
805 size = {
806 u'ignoredtexts':[u'col',u'text',u'line',u'page',u'theight',u'pheight',],
809 vspaces = {
810 u'bigskip':u'<div class="bigskip"> </div>',
811 u'defskip':u'<div class="defskip"> </div>',
812 u'medskip':u'<div class="medskip"> </div>',
813 u'smallskip':u'<div class="smallskip"> </div>',
814 u'vfill':u'<div class="vfill"> </div>',
817 class TOCConfig(object):
818 "Configuration class from elyxer.config file"
820 extractplain = {
822 u'allowed':[u'StringContainer',u'Constant',u'TaggedText',u'Align',u'TextFamily',u'EmphaticText',u'VersalitasText',u'BarredText',u'SizeText',u'ColorText',u'LangLine',u'Formula',],
823 u'cloned':[u'',], u'extracted':[u'',],
826 extracttitle = {
827 u'allowed':[u'StringContainer',u'Constant',u'Space',],
828 u'cloned':[u'TextFamily',u'EmphaticText',u'VersalitasText',u'BarredText',u'SizeText',u'ColorText',u'LangLine',u'Formula',],
829 u'extracted':[u'PlainLayout',u'TaggedText',u'Align',u'Caption',u'StandardLayout',u'FlexInset',],
832 class TagConfig(object):
833 "Configuration class from elyxer.config file"
835 barred = {
836 u'under':u'u',
839 family = {
840 u'sans':u'span class="sans"', u'typewriter':u'tt',
843 flex = {
844 u'CharStyle:Code':u'span class="code"',
845 u'CharStyle:MenuItem':u'span class="menuitem"',
846 u'Code':u'span class="code"', u'MenuItem':u'span class="menuitem"',
847 u'Noun':u'span class="noun"', u'Strong':u'span class="strong"',
850 group = {
851 u'layouts':[u'Quotation',u'Quote',],
854 layouts = {
855 u'Center':u'div', u'Chapter':u'h?', u'Date':u'h2', u'Paragraph':u'div',
856 u'Part':u'h1', u'Quotation':u'blockquote', u'Quote':u'blockquote',
857 u'Section':u'h?', u'Subsection':u'h?', u'Subsubsection':u'h?',
860 listitems = {
861 u'Enumerate':u'ol', u'Itemize':u'ul',
864 notes = {
865 u'Comment':u'', u'Greyedout':u'span class="greyedout"', u'Note':u'',
868 shaped = {
869 u'italic':u'i', u'slanted':u'i', u'smallcaps':u'span class="versalitas"',
872 class TranslationConfig(object):
873 "Configuration class from elyxer.config file"
875 constants = {
876 u'Appendix':u'Appendix', u'Book':u'Book', u'Chapter':u'Chapter',
877 u'Paragraph':u'Paragraph', u'Part':u'Part', u'Section':u'Section',
878 u'Subsection':u'Subsection', u'Subsubsection':u'Subsubsection',
879 u'abstract':u'Abstract', u'bibliography':u'Bibliography',
880 u'figure':u'figure', u'float-algorithm':u'Algorithm ',
881 u'float-figure':u'Figure ', u'float-listing':u'Listing ',
882 u'float-table':u'Table ', u'float-tableau':u'Tableau ',
883 u'footnotes':u'Footnotes', u'generated-by':u'Document generated by ',
884 u'generated-on':u' on ', u'index':u'Index',
885 u'jsmath-enable':u'Please enable JavaScript on your browser.',
886 u'jsmath-requires':u' requires JavaScript to correctly process the mathematics on this page. ',
887 u'jsmath-warning':u'Warning: ', u'list-algorithm':u'List of Algorithms',
888 u'list-figure':u'List of Figures', u'list-table':u'List of Tables',
889 u'list-tableau':u'List of Tableaux', u'main-page':u'Main page',
890 u'next':u'Next', u'nomenclature':u'Nomenclature',
891 u'on-page':u' on page ', u'prev':u'Prev', u'references':u'References',
892 u'toc':u'Table of Contents', u'toc-for':u'Contents for ', u'up':u'Up',
895 languages = {
896 u'american':u'en', u'british':u'en', u'deutsch':u'de', u'dutch':u'nl',
897 u'english':u'en', u'french':u'fr', u'ngerman':u'de', u'spanish':u'es',
905 class CommandLineParser(object):
906 "A parser for runtime options"
908 def __init__(self, options):
909 self.options = options
911 def parseoptions(self, args):
912 "Parse command line options"
913 if len(args) == 0:
914 return None
915 while len(args) > 0 and args[0].startswith('--'):
916 key, value = self.readoption(args)
917 if not key:
918 return 'Option ' + value + ' not recognized'
919 if not value:
920 return 'Option ' + key + ' needs a value'
921 setattr(self.options, key, value)
922 return None
924 def readoption(self, args):
925 "Read the key and value for an option"
926 arg = args[0][2:]
927 del args[0]
928 if '=' in arg:
929 key = self.readequalskey(arg, args)
930 else:
931 key = arg.replace('-', '')
932 if not hasattr(self.options, key):
933 return None, key
934 current = getattr(self.options, key)
935 if isinstance(current, bool):
936 return key, True
937 # read value
938 if len(args) == 0:
939 return key, None
940 if args[0].startswith('"'):
941 initial = args[0]
942 del args[0]
943 return key, self.readquoted(args, initial)
944 value = args[0]
945 del args[0]
946 if isinstance(current, list):
947 current.append(value)
948 return key, current
949 return key, value
951 def readquoted(self, args, initial):
952 "Read a value between quotes"
953 value = initial[1:]
954 while len(args) > 0 and not args[0].endswith('"') and not args[0].startswith('--'):
955 value += ' ' + args[0]
956 del args[0]
957 if len(args) == 0 or args[0].startswith('--'):
958 return None
959 value += ' ' + args[0:-1]
960 return value
962 def readequalskey(self, arg, args):
963 "Read a key using equals"
964 split = arg.split('=', 1)
965 key = split[0]
966 value = split[1]
967 args.insert(0, value)
968 return key
972 class Options(object):
973 "A set of runtime options"
975 instance = None
977 location = None
978 nocopy = False
979 copyright = False
980 debug = False
981 quiet = False
982 version = False
983 hardversion = False
984 versiondate = False
985 html = False
986 help = False
987 showlines = True
988 unicode = False
989 iso885915 = False
990 css = []
991 title = None
992 directory = None
993 destdirectory = None
994 toc = False
995 toctarget = ''
996 tocfor = None
997 forceformat = None
998 lyxformat = False
999 target = None
1000 splitpart = None
1001 memory = True
1002 lowmem = False
1003 nobib = False
1004 converter = 'imagemagick'
1005 raw = False
1006 jsmath = None
1007 mathjax = None
1008 nofooter = False
1009 simplemath = False
1010 template = None
1011 noconvert = False
1012 notoclabels = False
1013 letterfoot = True
1014 numberfoot = False
1015 symbolfoot = False
1016 hoverfoot = True
1017 marginfoot = False
1018 endfoot = False
1019 supfoot = True
1020 alignfoot = False
1021 footnotes = None
1022 imageformat = None
1023 copyimages = False
1024 googlecharts = False
1025 embedcss = []
1027 branches = dict()
1029 def parseoptions(self, args):
1030 "Parse command line options"
1031 Options.location = args[0]
1032 del args[0]
1033 parser = CommandLineParser(Options)
1034 result = parser.parseoptions(args)
1035 if result:
1036 Trace.error(result)
1037 self.usage()
1038 self.processoptions()
1040 def processoptions(self):
1041 "Process all options parsed."
1042 if Options.help:
1043 self.usage()
1044 if Options.version:
1045 self.showversion()
1046 if Options.hardversion:
1047 self.showhardversion()
1048 if Options.versiondate:
1049 self.showversiondate()
1050 if Options.lyxformat:
1051 self.showlyxformat()
1052 if Options.splitpart:
1053 try:
1054 Options.splitpart = int(Options.splitpart)
1055 if Options.splitpart <= 0:
1056 Trace.error('--splitpart requires a number bigger than zero')
1057 self.usage()
1058 except:
1059 Trace.error('--splitpart needs a numeric argument, not ' + Options.splitpart)
1060 self.usage()
1061 if Options.lowmem or Options.toc or Options.tocfor:
1062 Options.memory = False
1063 self.parsefootnotes()
1064 if Options.forceformat and not Options.imageformat:
1065 Options.imageformat = Options.forceformat
1066 if Options.imageformat == 'copy':
1067 Options.copyimages = True
1068 if Options.css == []:
1069 Options.css = ['http://elyxer.nongnu.org/lyx.css']
1070 if Options.html:
1071 Options.simplemath = True
1072 if Options.toc and not Options.tocfor:
1073 Trace.error('Option --toc is deprecated; use --tocfor "page" instead')
1074 Options.tocfor = Options.toctarget
1075 if Options.nocopy:
1076 Trace.error('Option --nocopy is deprecated; it is no longer needed')
1077 # set in Trace if necessary
1078 for param in dir(Trace):
1079 if param.endswith('mode'):
1080 setattr(Trace, param, getattr(self, param[:-4]))
1082 def usage(self):
1083 "Show correct usage"
1084 Trace.error('Usage: ' + os.path.basename(Options.location) + ' [options] [filein] [fileout]')
1085 Trace.error('Convert LyX input file "filein" to HTML file "fileout".')
1086 Trace.error('If filein (or fileout) is not given use standard input (or output).')
1087 Trace.error('Main program of the eLyXer package (http://elyxer.nongnu.org/).')
1088 self.showoptions()
1090 def parsefootnotes(self):
1091 "Parse footnotes options."
1092 if not Options.footnotes:
1093 return
1094 Options.marginfoot = False
1095 Options.letterfoot = False
1096 options = Options.footnotes.split(',')
1097 for option in options:
1098 footoption = option + 'foot'
1099 if hasattr(Options, footoption):
1100 setattr(Options, footoption, True)
1101 else:
1102 Trace.error('Unknown footnotes option: ' + option)
1103 if not Options.endfoot and not Options.marginfoot and not Options.hoverfoot:
1104 Options.hoverfoot = True
1105 if not Options.numberfoot and not Options.symbolfoot:
1106 Options.letterfoot = True
1108 def showoptions(self):
1109 "Show all possible options"
1110 Trace.error(' Common options:')
1111 Trace.error(' --help: show this online help')
1112 Trace.error(' --quiet: disables all runtime messages')
1113 Trace.error('')
1114 Trace.error(' Advanced options:')
1115 Trace.error(' --debug: enable debugging messages (for developers)')
1116 Trace.error(' --version: show version number and release date')
1117 Trace.error(' --lyxformat: return the highest LyX version supported')
1118 Trace.error(' Options for HTML output:')
1119 Trace.error(' --title "title": set the generated page title')
1120 Trace.error(' --css "file.css": use a custom CSS file')
1121 Trace.error(' --embedcss "file.css": embed styles from elyxer.a CSS file into the output')
1122 Trace.error(' --html: output HTML 4.0 instead of the default XHTML')
1123 Trace.error(' --unicode: full Unicode output')
1124 Trace.error(' --iso885915: output a document with ISO-8859-15 encoding')
1125 Trace.error(' --nofooter: remove the footer "generated by eLyXer"')
1126 Trace.error(' --simplemath: do not generate fancy math constructions')
1127 Trace.error(' Options for image output:')
1128 Trace.error(' --directory "img_dir": look for images in the specified directory')
1129 Trace.error(' --destdirectory "dest": put converted images into this directory')
1130 Trace.error(' --imageformat ".ext": image output format, or "copy" to copy images')
1131 Trace.error(' --noconvert: do not convert images, use in original locations')
1132 Trace.error(' --converter "inkscape": use an alternative program to convert images')
1133 Trace.error(' Options for footnote display:')
1134 Trace.error(' --numberfoot: mark footnotes with numbers instead of letters')
1135 Trace.error(' --symbolfoot: mark footnotes with symbols (*, **...)')
1136 Trace.error(' --hoverfoot: show footnotes as hovering text (default)')
1137 Trace.error(' --marginfoot: show footnotes on the page margin')
1138 Trace.error(' --endfoot: show footnotes at the end of the page')
1139 Trace.error(' --supfoot: use superscript for footnote markers (default)')
1140 Trace.error(' --alignfoot: use aligned text for footnote markers')
1141 Trace.error(' --footnotes "options": specify several comma-separated footnotes options')
1142 Trace.error(' Available options are: "number", "symbol", "hover", "margin", "end",')
1143 Trace.error(' "sup", "align"')
1144 Trace.error(' Advanced output options:')
1145 Trace.error(' --splitpart "depth": split the resulting webpage at the given depth')
1146 Trace.error(' --tocfor "page": generate a TOC that points to the given page')
1147 Trace.error(' --target "frame": make all links point to the given frame')
1148 Trace.error(' --notoclabels: omit the part labels in the TOC, such as Chapter')
1149 Trace.error(' --lowmem: do the conversion on the fly (conserve memory)')
1150 Trace.error(' --raw: generate HTML without header or footer.')
1151 Trace.error(' --jsmath "URL": use jsMath from elyxer.the given URL to display equations')
1152 Trace.error(' --mathjax "URL": use MathJax from elyxer.the given URL to display equations')
1153 Trace.error(' --googlecharts: use Google Charts to generate formula images')
1154 Trace.error(' --template "file": use a template, put everything in <!--$content-->')
1155 Trace.error(' --copyright: add a copyright notice at the bottom')
1156 Trace.error(' Deprecated options:')
1157 Trace.error(' --toc: (deprecated) create a table of contents')
1158 Trace.error(' --toctarget "page": (deprecated) generate a TOC for the given page')
1159 Trace.error(' --nocopy: (deprecated) maintained for backwards compatibility')
1160 sys.exit()
1162 def showversion(self):
1163 "Return the current eLyXer version string"
1164 string = 'eLyXer version ' + GeneralConfig.version['number']
1165 string += ' (' + GeneralConfig.version['date'] + ')'
1166 Trace.error(string)
1167 sys.exit()
1169 def showhardversion(self):
1170 "Return just the version string"
1171 Trace.message(GeneralConfig.version['number'])
1172 sys.exit()
1174 def showversiondate(self):
1175 "Return just the version dte"
1176 Trace.message(GeneralConfig.version['date'])
1177 sys.exit()
1179 def showlyxformat(self):
1180 "Return just the lyxformat parameter"
1181 Trace.message(GeneralConfig.version['lyxformat'])
1182 sys.exit()
1184 class BranchOptions(object):
1185 "A set of options for a branch"
1187 def __init__(self, name):
1188 self.name = name
1189 self.options = {'color':'#ffffff'}
1191 def set(self, key, value):
1192 "Set a branch option"
1193 if not key.startswith(ContainerConfig.string['startcommand']):
1194 Trace.error('Invalid branch option ' + key)
1195 return
1196 key = key.replace(ContainerConfig.string['startcommand'], '')
1197 self.options[key] = value
1199 def isselected(self):
1200 "Return if the branch is selected"
1201 if not 'selected' in self.options:
1202 return False
1203 return self.options['selected'] == '1'
1205 def __unicode__(self):
1206 "String representation"
1207 return 'options for ' + self.name + ': ' + unicode(self.options)
1212 import urllib
1221 class Cloner(object):
1222 "An object used to clone other objects."
1224 def clone(cls, original):
1225 "Return an exact copy of an object."
1226 "The original object must have an empty constructor."
1227 return cls.create(original.__class__)
1229 def create(cls, type):
1230 "Create an object of a given class."
1231 clone = type.__new__(type)
1232 clone.__init__()
1233 return clone
1235 clone = classmethod(clone)
1236 create = classmethod(create)
1238 class ContainerExtractor(object):
1239 "A class to extract certain containers."
1241 def __init__(self, config):
1242 "The config parameter is a map containing three lists: allowed, copied and extracted."
1243 "Each of the three is a list of class names for containers."
1244 "Allowed containers are included as is into the result."
1245 "Cloned containers are cloned and placed into the result."
1246 "Extracted containers are looked into."
1247 "All other containers are silently ignored."
1248 self.allowed = config['allowed']
1249 self.cloned = config['cloned']
1250 self.extracted = config['extracted']
1252 def extract(self, container):
1253 "Extract a group of selected containers from elyxer.a container."
1254 list = []
1255 locate = lambda c: c.__class__.__name__ in self.allowed + self.cloned
1256 recursive = lambda c: c.__class__.__name__ in self.extracted
1257 process = lambda c: self.process(c, list)
1258 container.recursivesearch(locate, recursive, process)
1259 return list
1261 def process(self, container, list):
1262 "Add allowed containers, clone cloned containers and add the clone."
1263 name = container.__class__.__name__
1264 if name in self.allowed:
1265 list.append(container)
1266 elif name in self.cloned:
1267 list.append(self.safeclone(container))
1268 else:
1269 Trace.error('Unknown container class ' + name)
1271 def safeclone(self, container):
1272 "Return a new container with contents only in a safe list, recursively."
1273 clone = Cloner.clone(container)
1274 clone.output = container.output
1275 clone.contents = self.extract(container)
1276 return clone
1283 class Parser(object):
1284 "A generic parser"
1286 def __init__(self):
1287 self.begin = 0
1288 self.parameters = dict()
1290 def parseheader(self, reader):
1291 "Parse the header"
1292 header = reader.currentline().split()
1293 reader.nextline()
1294 self.begin = reader.linenumber
1295 return header
1297 def parseparameter(self, reader):
1298 "Parse a parameter"
1299 if reader.currentline().strip().startswith('<'):
1300 key, value = self.parsexml(reader)
1301 self.parameters[key] = value
1302 return
1303 split = reader.currentline().strip().split(' ', 1)
1304 reader.nextline()
1305 if len(split) == 0:
1306 return
1307 key = split[0]
1308 if len(split) == 1:
1309 self.parameters[key] = True
1310 return
1311 if not '"' in split[1]:
1312 self.parameters[key] = split[1].strip()
1313 return
1314 doublesplit = split[1].split('"')
1315 self.parameters[key] = doublesplit[1]
1317 def parsexml(self, reader):
1318 "Parse a parameter in xml form: <param attr1=value...>"
1319 strip = reader.currentline().strip()
1320 reader.nextline()
1321 if not strip.endswith('>'):
1322 Trace.error('XML parameter ' + strip + ' should be <...>')
1323 split = strip[1:-1].split()
1324 if len(split) == 0:
1325 Trace.error('Empty XML parameter <>')
1326 return None, None
1327 key = split[0]
1328 del split[0]
1329 if len(split) == 0:
1330 return key, dict()
1331 attrs = dict()
1332 for attr in split:
1333 if not '=' in attr:
1334 Trace.error('Erroneous attribute for ' + key + ': ' + attr)
1335 attr += '="0"'
1336 parts = attr.split('=')
1337 attrkey = parts[0]
1338 value = parts[1].split('"')[1]
1339 attrs[attrkey] = value
1340 return key, attrs
1342 def parseending(self, reader, process):
1343 "Parse until the current ending is found"
1344 if not self.ending:
1345 Trace.error('No ending for ' + unicode(self))
1346 return
1347 while not reader.currentline().startswith(self.ending):
1348 process()
1350 def parsecontainer(self, reader, contents):
1351 container = self.factory.createcontainer(reader)
1352 if container:
1353 container.parent = self.parent
1354 contents.append(container)
1356 def __unicode__(self):
1357 "Return a description"
1358 return self.__class__.__name__ + ' (' + unicode(self.begin) + ')'
1360 class LoneCommand(Parser):
1361 "A parser for just one command line"
1363 def parse(self,reader):
1364 "Read nothing"
1365 return []
1367 class TextParser(Parser):
1368 "A parser for a command and a bit of text"
1370 stack = []
1372 def __init__(self, container):
1373 Parser.__init__(self)
1374 self.ending = None
1375 if container.__class__.__name__ in ContainerConfig.endings:
1376 self.ending = ContainerConfig.endings[container.__class__.__name__]
1377 self.endings = []
1379 def parse(self, reader):
1380 "Parse lines as long as they are text"
1381 TextParser.stack.append(self.ending)
1382 self.endings = TextParser.stack + [ContainerConfig.endings['Layout'],
1383 ContainerConfig.endings['Inset'], self.ending]
1384 contents = []
1385 while not self.isending(reader):
1386 self.parsecontainer(reader, contents)
1387 return contents
1389 def isending(self, reader):
1390 "Check if text is ending"
1391 current = reader.currentline().split()
1392 if len(current) == 0:
1393 return False
1394 if current[0] in self.endings:
1395 if current[0] in TextParser.stack:
1396 TextParser.stack.remove(current[0])
1397 else:
1398 TextParser.stack = []
1399 return True
1400 return False
1402 class ExcludingParser(Parser):
1403 "A parser that excludes the final line"
1405 def parse(self, reader):
1406 "Parse everything up to (and excluding) the final line"
1407 contents = []
1408 self.parseending(reader, lambda: self.parsecontainer(reader, contents))
1409 return contents
1411 class BoundedParser(ExcludingParser):
1412 "A parser bound by a final line"
1414 def parse(self, reader):
1415 "Parse everything, including the final line"
1416 contents = ExcludingParser.parse(self, reader)
1417 # skip last line
1418 reader.nextline()
1419 return contents
1421 class BoundedDummy(Parser):
1422 "A bound parser that ignores everything"
1424 def parse(self, reader):
1425 "Parse the contents of the container"
1426 self.parseending(reader, lambda: reader.nextline())
1427 # skip last line
1428 reader.nextline()
1429 return []
1431 class StringParser(Parser):
1432 "Parses just a string"
1434 def parseheader(self, reader):
1435 "Do nothing, just take note"
1436 self.begin = reader.linenumber + 1
1437 return []
1439 def parse(self, reader):
1440 "Parse a single line"
1441 contents = reader.currentline()
1442 reader.nextline()
1443 return contents
1445 class InsetParser(BoundedParser):
1446 "Parses a LyX inset"
1448 def parse(self, reader):
1449 "Parse inset parameters into a dictionary"
1450 startcommand = ContainerConfig.string['startcommand']
1451 while reader.currentline() != '' and not reader.currentline().startswith(startcommand):
1452 self.parseparameter(reader)
1453 return BoundedParser.parse(self, reader)
1460 class ContainerOutput(object):
1461 "The generic HTML output for a container."
1463 def gethtml(self, container):
1464 "Show an error."
1465 Trace.error('gethtml() not implemented for ' + unicode(self))
1467 def isempty(self):
1468 "Decide if the output is empty: by default, not empty."
1469 return False
1471 class EmptyOutput(ContainerOutput):
1473 def gethtml(self, container):
1474 "Return empty HTML code."
1475 return []
1477 def isempty(self):
1478 "This output is particularly empty."
1479 return True
1481 class FixedOutput(ContainerOutput):
1482 "Fixed output"
1484 def gethtml(self, container):
1485 "Return constant HTML code"
1486 return container.html
1488 class ContentsOutput(ContainerOutput):
1489 "Outputs the contents converted to HTML"
1491 def gethtml(self, container):
1492 "Return the HTML code"
1493 html = []
1494 if container.contents == None:
1495 return html
1496 for element in container.contents:
1497 if not hasattr(element, 'gethtml'):
1498 Trace.error('No html in ' + element.__class__.__name__ + ': ' + unicode(element))
1499 return html
1500 html += element.gethtml()
1501 return html
1503 class TaggedOutput(ContentsOutput):
1504 "Outputs an HTML tag surrounding the contents."
1506 tag = None
1507 breaklines = False
1508 empty = False
1510 def settag(self, tag, breaklines=False, empty=False):
1511 "Set the value for the tag and other attributes."
1512 self.tag = tag
1513 if breaklines:
1514 self.breaklines = breaklines
1515 if empty:
1516 self.empty = empty
1517 return self
1519 def setbreaklines(self, breaklines):
1520 "Set the value for breaklines."
1521 self.breaklines = breaklines
1522 return self
1524 def gethtml(self, container):
1525 "Return the HTML code."
1526 if self.empty:
1527 return [self.selfclosing(container)]
1528 html = [self.open(container)]
1529 html += ContentsOutput.gethtml(self, container)
1530 html.append(self.close(container))
1531 return html
1533 def open(self, container):
1534 "Get opening line."
1535 if not self.checktag():
1536 return ''
1537 open = '<' + self.tag + '>'
1538 if self.breaklines:
1539 return open + '\n'
1540 return open
1542 def close(self, container):
1543 "Get closing line."
1544 if not self.checktag():
1545 return ''
1546 close = '</' + self.tag.split()[0] + '>'
1547 if self.breaklines:
1548 return '\n' + close + '\n'
1549 return close
1551 def selfclosing(self, container):
1552 "Get self-closing line."
1553 if not self.checktag():
1554 return ''
1555 selfclosing = '<' + self.tag + '/>'
1556 if self.breaklines:
1557 return selfclosing + '\n'
1558 return selfclosing
1560 def checktag(self):
1561 "Check that the tag is valid."
1562 if not self.tag:
1563 Trace.error('No tag in ' + unicode(container))
1564 return False
1565 if self.tag == '':
1566 return False
1567 return True
1569 class FilteredOutput(ContentsOutput):
1570 "Returns the output in the contents, but filtered:"
1571 "some strings are replaced by others."
1573 def __init__(self):
1574 "Initialize the filters."
1575 self.filters = []
1577 def addfilter(self, original, replacement):
1578 "Add a new filter: replace the original by the replacement."
1579 self.filters.append((original, replacement))
1581 def gethtml(self, container):
1582 "Return the HTML code"
1583 result = []
1584 html = ContentsOutput.gethtml(self, container)
1585 for line in html:
1586 result.append(self.filter(line))
1587 return result
1589 def filter(self, line):
1590 "Filter a single line with all available filters."
1591 for original, replacement in self.filters:
1592 if original in line:
1593 line = line.replace(original, replacement)
1594 return line
1596 class StringOutput(ContainerOutput):
1597 "Returns a bare string as output"
1599 def gethtml(self, container):
1600 "Return a bare string"
1601 return [container.string]
1609 import sys
1610 import codecs
1613 class LineReader(object):
1614 "Reads a file line by line"
1616 def __init__(self, filename):
1617 if isinstance(filename, file):
1618 self.file = filename
1619 else:
1620 self.file = codecs.open(filename, 'rU', 'utf-8')
1621 self.linenumber = 1
1622 self.lastline = None
1623 self.current = None
1624 self.mustread = True
1625 self.depleted = False
1626 try:
1627 self.readline()
1628 except UnicodeDecodeError:
1629 # try compressed file
1630 import gzip
1631 self.file = gzip.open(filename, 'rb')
1632 self.readline()
1634 def setstart(self, firstline):
1635 "Set the first line to read."
1636 for i in range(firstline):
1637 self.file.readline()
1638 self.linenumber = firstline
1640 def setend(self, lastline):
1641 "Set the last line to read."
1642 self.lastline = lastline
1644 def currentline(self):
1645 "Get the current line"
1646 if self.mustread:
1647 self.readline()
1648 return self.current
1650 def nextline(self):
1651 "Go to next line"
1652 if self.depleted:
1653 Trace.fatal('Read beyond file end')
1654 self.mustread = True
1656 def readline(self):
1657 "Read a line from elyxer.file"
1658 self.current = self.file.readline()
1659 if not isinstance(self.file, codecs.StreamReaderWriter):
1660 self.current = self.current.decode('utf-8')
1661 if len(self.current) == 0:
1662 self.depleted = True
1663 self.current = self.current.rstrip('\n\r')
1664 self.linenumber += 1
1665 self.mustread = False
1666 Trace.prefix = 'Line ' + unicode(self.linenumber) + ': '
1667 if self.linenumber % 1000 == 0:
1668 Trace.message('Parsing')
1670 def finished(self):
1671 "Find out if the file is finished"
1672 if self.lastline and self.linenumber == self.lastline:
1673 return True
1674 if self.mustread:
1675 self.readline()
1676 return self.depleted
1678 def close(self):
1679 self.file.close()
1681 class LineWriter(object):
1682 "Writes a file as a series of lists"
1684 file = False
1686 def __init__(self, filename):
1687 if isinstance(filename, file):
1688 self.file = filename
1689 self.filename = None
1690 else:
1691 self.filename = filename
1693 def write(self, strings):
1694 "Write a list of strings"
1695 for string in strings:
1696 if not isinstance(string, basestring):
1697 Trace.error('Not a string: ' + unicode(string) + ' in ' + unicode(strings))
1698 return
1699 self.writestring(string)
1701 def writestring(self, string):
1702 "Write a string"
1703 if not self.file:
1704 self.file = codecs.open(self.filename, 'w', "utf-8")
1705 if self.file == sys.stdout and sys.version_info < (3,0):
1706 string = string.encode('utf-8')
1707 self.file.write(string)
1709 def writeline(self, line):
1710 "Write a line to file"
1711 self.writestring(line + '\n')
1713 def close(self):
1714 self.file.close()
1721 class Globable(object):
1722 """A bit of text which can be globbed (lumped together in bits).
1723 Methods current(), skipcurrent(), checkfor() and isout() have to be
1724 implemented by subclasses."""
1726 leavepending = False
1728 def __init__(self):
1729 self.endinglist = EndingList()
1731 def checkbytemark(self):
1732 "Check for a Unicode byte mark and skip it."
1733 if self.finished():
1734 return
1735 if ord(self.current()) == 0xfeff:
1736 self.skipcurrent()
1738 def isout(self):
1739 "Find out if we are out of the position yet."
1740 Trace.error('Unimplemented isout()')
1741 return True
1743 def current(self):
1744 "Return the current character."
1745 Trace.error('Unimplemented current()')
1746 return ''
1748 def checkfor(self, string):
1749 "Check for the given string in the current position."
1750 Trace.error('Unimplemented checkfor()')
1751 return False
1753 def finished(self):
1754 "Find out if the current text has finished."
1755 if self.isout():
1756 if not self.leavepending:
1757 self.endinglist.checkpending()
1758 return True
1759 return self.endinglist.checkin(self)
1761 def skipcurrent(self):
1762 "Return the current character and skip it."
1763 Trace.error('Unimplemented skipcurrent()')
1764 return ''
1766 def glob(self, currentcheck):
1767 "Glob a bit of text that satisfies a check on the current char."
1768 glob = ''
1769 while not self.finished() and currentcheck():
1770 glob += self.skipcurrent()
1771 return glob
1773 def globalpha(self):
1774 "Glob a bit of alpha text"
1775 return self.glob(lambda: self.current().isalpha())
1777 def globnumber(self):
1778 "Glob a row of digits."
1779 return self.glob(lambda: self.current().isdigit())
1781 def isidentifier(self):
1782 "Return if the current character is alphanumeric or _."
1783 if self.current().isalnum() or self.current() == '_':
1784 return True
1785 return False
1787 def globidentifier(self):
1788 "Glob alphanumeric and _ symbols."
1789 return self.glob(self.isidentifier)
1791 def isvalue(self):
1792 "Return if the current character is a value character:"
1793 "not a bracket or a space."
1794 if self.current().isspace():
1795 return False
1796 if self.current() in '{}()':
1797 return False
1798 return True
1800 def globvalue(self):
1801 "Glob a value: any symbols but brackets."
1802 return self.glob(self.isvalue)
1804 def skipspace(self):
1805 "Skip all whitespace at current position."
1806 return self.glob(lambda: self.current().isspace())
1808 def globincluding(self, magicchar):
1809 "Glob a bit of text up to (including) the magic char."
1810 glob = self.glob(lambda: self.current() != magicchar) + magicchar
1811 self.skip(magicchar)
1812 return glob
1814 def globexcluding(self, excluded):
1815 "Glob a bit of text up until (excluding) any excluded character."
1816 return self.glob(lambda: self.current() not in excluded)
1818 def pushending(self, ending, optional = False):
1819 "Push a new ending to the bottom"
1820 self.endinglist.add(ending, optional)
1822 def popending(self, expected = None):
1823 "Pop the ending found at the current position"
1824 if self.isout() and self.leavepending:
1825 return expected
1826 ending = self.endinglist.pop(self)
1827 if expected and expected != ending:
1828 Trace.error('Expected ending ' + expected + ', got ' + ending)
1829 self.skip(ending)
1830 return ending
1832 def nextending(self):
1833 "Return the next ending in the queue."
1834 nextending = self.endinglist.findending(self)
1835 if not nextending:
1836 return None
1837 return nextending.ending
1839 class EndingList(object):
1840 "A list of position endings"
1842 def __init__(self):
1843 self.endings = []
1845 def add(self, ending, optional = False):
1846 "Add a new ending to the list"
1847 self.endings.append(PositionEnding(ending, optional))
1849 def pickpending(self, pos):
1850 "Pick any pending endings from a parse position."
1851 self.endings += pos.endinglist.endings
1853 def checkin(self, pos):
1854 "Search for an ending"
1855 if self.findending(pos):
1856 return True
1857 return False
1859 def pop(self, pos):
1860 "Remove the ending at the current position"
1861 if pos.isout():
1862 Trace.error('No ending out of bounds')
1863 return ''
1864 ending = self.findending(pos)
1865 if not ending:
1866 Trace.error('No ending at ' + pos.current())
1867 return ''
1868 for each in reversed(self.endings):
1869 self.endings.remove(each)
1870 if each == ending:
1871 return each.ending
1872 elif not each.optional:
1873 Trace.error('Removed non-optional ending ' + each)
1874 Trace.error('No endings left')
1875 return ''
1877 def findending(self, pos):
1878 "Find the ending at the current position"
1879 if len(self.endings) == 0:
1880 return None
1881 for index, ending in enumerate(reversed(self.endings)):
1882 if ending.checkin(pos):
1883 return ending
1884 if not ending.optional:
1885 return None
1886 return None
1888 def checkpending(self):
1889 "Check if there are any pending endings"
1890 if len(self.endings) != 0:
1891 Trace.error('Pending ' + unicode(self) + ' left open')
1893 def __unicode__(self):
1894 "Printable representation"
1895 string = 'endings ['
1896 for ending in self.endings:
1897 string += unicode(ending) + ','
1898 if len(self.endings) > 0:
1899 string = string[:-1]
1900 return string + ']'
1902 class PositionEnding(object):
1903 "An ending for a parsing position"
1905 def __init__(self, ending, optional):
1906 self.ending = ending
1907 self.optional = optional
1909 def checkin(self, pos):
1910 "Check for the ending"
1911 return pos.checkfor(self.ending)
1913 def __unicode__(self):
1914 "Printable representation"
1915 string = 'Ending ' + self.ending
1916 if self.optional:
1917 string += ' (optional)'
1918 return string
1922 class Position(Globable):
1923 """A position in a text to parse.
1924 Including those in Globable, functions to implement by subclasses are:
1925 skip(), identifier(), extract(), isout() and current()."""
1927 def __init__(self):
1928 Globable.__init__(self)
1930 def skip(self, string):
1931 "Skip a string"
1932 Trace.error('Unimplemented skip()')
1934 def identifier(self):
1935 "Return an identifier for the current position."
1936 Trace.error('Unimplemented identifier()')
1937 return 'Error'
1939 def extract(self, length):
1940 "Extract the next string of the given length, or None if not enough text,"
1941 "without advancing the parse position."
1942 Trace.error('Unimplemented extract()')
1943 return None
1945 def checkfor(self, string):
1946 "Check for a string at the given position."
1947 return string == self.extract(len(string))
1949 def checkforlower(self, string):
1950 "Check for a string in lower case."
1951 extracted = self.extract(len(string))
1952 if not extracted:
1953 return False
1954 return string.lower() == self.extract(len(string)).lower()
1956 def skipcurrent(self):
1957 "Return the current character and skip it."
1958 current = self.current()
1959 self.skip(current)
1960 return current
1962 def next(self):
1963 "Advance the position and return the next character."
1964 self.skipcurrent()
1965 return self.current()
1967 def checkskip(self, string):
1968 "Check for a string at the given position; if there, skip it"
1969 if not self.checkfor(string):
1970 return False
1971 self.skip(string)
1972 return True
1974 def error(self, message):
1975 "Show an error message and the position identifier."
1976 Trace.error(message + ': ' + self.identifier())
1978 class TextPosition(Position):
1979 "A parse position based on a raw text."
1981 def __init__(self, text):
1982 "Create the position from elyxer.some text."
1983 Position.__init__(self)
1984 self.pos = 0
1985 self.text = text
1986 self.checkbytemark()
1988 def skip(self, string):
1989 "Skip a string of characters."
1990 self.pos += len(string)
1992 def identifier(self):
1993 "Return a sample of the remaining text."
1994 length = 30
1995 if self.pos + length > len(self.text):
1996 length = len(self.text) - self.pos
1997 return '*' + self.text[self.pos:self.pos + length] + '*'
1999 def isout(self):
2000 "Find out if we are out of the text yet."
2001 return self.pos >= len(self.text)
2003 def current(self):
2004 "Return the current character, assuming we are not out."
2005 return self.text[self.pos]
2007 def extract(self, length):
2008 "Extract the next string of the given length, or None if not enough text."
2009 if self.pos + length > len(self.text):
2010 return None
2011 return self.text[self.pos : self.pos + length]
2013 class FilePosition(Position):
2014 "A parse position based on an underlying file."
2016 def __init__(self, filename):
2017 "Create the position from a file."
2018 Position.__init__(self)
2019 self.reader = LineReader(filename)
2020 self.pos = 0
2021 self.checkbytemark()
2023 def skip(self, string):
2024 "Skip a string of characters."
2025 length = len(string)
2026 while self.pos + length > len(self.reader.currentline()):
2027 length -= len(self.reader.currentline()) - self.pos + 1
2028 self.nextline()
2029 self.pos += length
2031 def currentline(self):
2032 "Get the current line of the underlying file."
2033 return self.reader.currentline()
2035 def nextline(self):
2036 "Go to the next line."
2037 self.reader.nextline()
2038 self.pos = 0
2040 def linenumber(self):
2041 "Return the line number of the file."
2042 return self.reader.linenumber + 1
2044 def identifier(self):
2045 "Return the current line and line number in the file."
2046 before = self.reader.currentline()[:self.pos - 1]
2047 after = self.reader.currentline()[self.pos:]
2048 return 'line ' + unicode(self.getlinenumber()) + ': ' + before + '*' + after
2050 def isout(self):
2051 "Find out if we are out of the text yet."
2052 if self.pos > len(self.reader.currentline()):
2053 if self.pos > len(self.reader.currentline()) + 1:
2054 Trace.error('Out of the line ' + self.reader.currentline() + ': ' + unicode(self.pos))
2055 self.nextline()
2056 return self.reader.finished()
2058 def current(self):
2059 "Return the current character, assuming we are not out."
2060 if self.pos == len(self.reader.currentline()):
2061 return '\n'
2062 if self.pos > len(self.reader.currentline()):
2063 Trace.error('Out of the line ' + self.reader.currentline() + ': ' + unicode(self.pos))
2064 return '*'
2065 return self.reader.currentline()[self.pos]
2067 def extract(self, length):
2068 "Extract the next string of the given length, or None if not enough text."
2069 if self.pos + length > len(self.reader.currentline()):
2070 return None
2071 return self.reader.currentline()[self.pos : self.pos + length]
2075 class Container(object):
2076 "A container for text and objects in a lyx file"
2078 partkey = None
2079 parent = None
2080 begin = None
2082 def __init__(self):
2083 self.contents = list()
2085 def process(self):
2086 "Process contents"
2087 pass
2089 def gethtml(self):
2090 "Get the resulting HTML"
2091 html = self.output.gethtml(self)
2092 if isinstance(html, basestring):
2093 Trace.error('Raw string ' + html)
2094 html = [html]
2095 return self.escapeall(html)
2097 def escapeall(self, lines):
2098 "Escape all lines in an array according to the output options."
2099 result = []
2100 for line in lines:
2101 if Options.html:
2102 line = self.escape(line, EscapeConfig.html)
2103 if Options.iso885915:
2104 line = self.escape(line, EscapeConfig.iso885915)
2105 line = self.escapeentities(line)
2106 elif not Options.unicode:
2107 line = self.escape(line, EscapeConfig.nonunicode)
2108 result.append(line)
2109 return result
2111 def escape(self, line, replacements = EscapeConfig.entities):
2112 "Escape a line with replacements from elyxer.a map"
2113 pieces = replacements.keys()
2114 # do them in order
2115 pieces.sort()
2116 for piece in pieces:
2117 if piece in line:
2118 line = line.replace(piece, replacements[piece])
2119 return line
2121 def escapeentities(self, line):
2122 "Escape all Unicode characters to HTML entities."
2123 result = ''
2124 pos = TextPosition(line)
2125 while not pos.finished():
2126 if ord(pos.current()) > 128:
2127 codepoint = hex(ord(pos.current()))
2128 if codepoint == '0xd835':
2129 codepoint = hex(ord(pos.next()) + 0xf800)
2130 result += '&#' + codepoint[1:] + ';'
2131 else:
2132 result += pos.current()
2133 pos.skipcurrent()
2134 return result
2136 def searchall(self, type):
2137 "Search for all embedded containers of a given type"
2138 list = []
2139 self.searchprocess(type, lambda container: list.append(container))
2140 return list
2142 def searchremove(self, type):
2143 "Search for all containers of a type and remove them"
2144 list = self.searchall(type)
2145 for container in list:
2146 container.parent.contents.remove(container)
2147 return list
2149 def searchprocess(self, type, process):
2150 "Search for elements of a given type and process them"
2151 self.locateprocess(lambda container: isinstance(container, type), process)
2153 def locateprocess(self, locate, process):
2154 "Search for all embedded containers and process them"
2155 for container in self.contents:
2156 container.locateprocess(locate, process)
2157 if locate(container):
2158 process(container)
2160 def recursivesearch(self, locate, recursive, process):
2161 "Perform a recursive search in the container."
2162 for container in self.contents:
2163 if recursive(container):
2164 container.recursivesearch(locate, recursive, process)
2165 if locate(container):
2166 process(container)
2168 def extracttext(self):
2169 "Extract all text from elyxer.allowed containers."
2170 result = ''
2171 constants = ContainerExtractor(ContainerConfig.extracttext).extract(self)
2172 for constant in constants:
2173 result += constant.string
2174 return result
2176 def group(self, index, group, isingroup):
2177 "Group some adjoining elements into a group"
2178 if index >= len(self.contents):
2179 return
2180 if hasattr(self.contents[index], 'grouped'):
2181 return
2182 while index < len(self.contents) and isingroup(self.contents[index]):
2183 self.contents[index].grouped = True
2184 group.contents.append(self.contents[index])
2185 self.contents.pop(index)
2186 self.contents.insert(index, group)
2188 def remove(self, index):
2189 "Remove a container but leave its contents"
2190 container = self.contents[index]
2191 self.contents.pop(index)
2192 while len(container.contents) > 0:
2193 self.contents.insert(index, container.contents.pop())
2195 def tree(self, level = 0):
2196 "Show in a tree"
2197 Trace.debug(" " * level + unicode(self))
2198 for container in self.contents:
2199 container.tree(level + 1)
2201 def getparameter(self, name):
2202 "Get the value of a parameter, if present."
2203 if not name in self.parameters:
2204 return None
2205 return self.parameters[name]
2207 def getparameterlist(self, name):
2208 "Get the value of a comma-separated parameter as a list."
2209 paramtext = self.getparameter(name)
2210 if not paramtext:
2211 return []
2212 return paramtext.split(',')
2214 def hasemptyoutput(self):
2215 "Check if the parent's output is empty."
2216 current = self.parent
2217 while current:
2218 if current.output.isempty():
2219 return True
2220 current = current.parent
2221 return False
2223 def __unicode__(self):
2224 "Get a description"
2225 if not self.begin:
2226 return self.__class__.__name__
2227 return self.__class__.__name__ + '@' + unicode(self.begin)
2229 class BlackBox(Container):
2230 "A container that does not output anything"
2232 def __init__(self):
2233 self.parser = LoneCommand()
2234 self.output = EmptyOutput()
2235 self.contents = []
2237 class LyXFormat(BlackBox):
2238 "Read the lyxformat command"
2240 def process(self):
2241 "Show warning if version < 276"
2242 version = int(self.header[1])
2243 if version < 276:
2244 Trace.error('Warning: unsupported old format version ' + str(version))
2245 if version > int(GeneralConfig.version['lyxformat']):
2246 Trace.error('Warning: unsupported new format version ' + str(version))
2248 class StringContainer(Container):
2249 "A container for a single string"
2251 parsed = None
2253 def __init__(self):
2254 self.parser = StringParser()
2255 self.output = StringOutput()
2256 self.string = ''
2258 def process(self):
2259 "Replace special chars from elyxer.the contents."
2260 if self.parsed:
2261 self.string = self.replacespecial(self.parsed)
2262 self.parsed = None
2264 def replacespecial(self, line):
2265 "Replace all special chars from elyxer.a line"
2266 replaced = self.escape(line, EscapeConfig.entities)
2267 replaced = self.changeline(replaced)
2268 if ContainerConfig.string['startcommand'] in replaced and len(replaced) > 1:
2269 # unprocessed commands
2270 if self.begin:
2271 message = 'Unknown command at ' + unicode(self.begin) + ': '
2272 else:
2273 message = 'Unknown command: '
2274 Trace.error(message + replaced.strip())
2275 return replaced
2277 def changeline(self, line):
2278 line = self.escape(line, EscapeConfig.chars)
2279 if not ContainerConfig.string['startcommand'] in line:
2280 return line
2281 line = self.escape(line, EscapeConfig.commands)
2282 return line
2284 def extracttext(self):
2285 "Return all text."
2286 return self.string
2288 def __unicode__(self):
2289 "Return a printable representation."
2290 result = 'StringContainer'
2291 if self.begin:
2292 result += '@' + unicode(self.begin)
2293 ellipsis = '...'
2294 if len(self.string.strip()) <= 15:
2295 ellipsis = ''
2296 return result + ' (' + self.string.strip()[:15] + ellipsis + ')'
2298 class Constant(StringContainer):
2299 "A constant string"
2301 def __init__(self, text):
2302 self.contents = []
2303 self.string = text
2304 self.output = StringOutput()
2306 def __unicode__(self):
2307 return 'Constant: ' + self.string
2309 class TaggedText(Container):
2310 "Text inside a tag"
2312 output = None
2314 def __init__(self):
2315 self.parser = TextParser(self)
2316 self.output = TaggedOutput()
2318 def complete(self, contents, tag, breaklines=False):
2319 "Complete the tagged text and return it"
2320 self.contents = contents
2321 self.output.tag = tag
2322 self.output.breaklines = breaklines
2323 return self
2325 def constant(self, text, tag, breaklines=False):
2326 "Complete the tagged text with a constant"
2327 constant = Constant(text)
2328 return self.complete([constant], tag, breaklines)
2330 def __unicode__(self):
2331 "Return a printable representation."
2332 if not hasattr(self.output, 'tag'):
2333 return 'Emtpy tagged text'
2334 if not self.output.tag:
2335 return 'Tagged <unknown tag>'
2336 return 'Tagged <' + self.output.tag + '>'
2343 class DocumentParameters(object):
2344 "Global parameters for the document."
2346 pdftitle = None
2347 indentstandard = False
2348 tocdepth = 10
2349 startinglevel = 0
2350 maxdepth = 10
2351 language = None
2352 bibliography = None
2353 outputchanges = False
2354 displaymode = False
2361 class FormulaParser(Parser):
2362 "Parses a formula"
2364 def parseheader(self, reader):
2365 "See if the formula is inlined"
2366 self.begin = reader.linenumber + 1
2367 type = self.parsetype(reader)
2368 if not type:
2369 reader.nextline()
2370 type = self.parsetype(reader)
2371 if not type:
2372 Trace.error('Unknown formula type in ' + reader.currentline().strip())
2373 return ['unknown']
2374 return [type]
2376 def parsetype(self, reader):
2377 "Get the formula type from the first line."
2378 if reader.currentline().find(FormulaConfig.starts['simple']) >= 0:
2379 return 'inline'
2380 if reader.currentline().find(FormulaConfig.starts['complex']) >= 0:
2381 return 'block'
2382 if reader.currentline().find(FormulaConfig.starts['unnumbered']) >= 0:
2383 return 'block'
2384 if reader.currentline().find(FormulaConfig.starts['beginbefore']) >= 0:
2385 return 'numbered'
2386 return None
2388 def parse(self, reader):
2389 "Parse the formula until the end"
2390 formula = self.parseformula(reader)
2391 while not reader.currentline().startswith(self.ending):
2392 stripped = reader.currentline().strip()
2393 if len(stripped) > 0:
2394 Trace.error('Unparsed formula line ' + stripped)
2395 reader.nextline()
2396 reader.nextline()
2397 return formula
2399 def parseformula(self, reader):
2400 "Parse the formula contents"
2401 simple = FormulaConfig.starts['simple']
2402 if simple in reader.currentline():
2403 rest = reader.currentline().split(simple, 1)[1]
2404 if simple in rest:
2405 # formula is $...$
2406 return self.parsesingleliner(reader, simple, simple)
2407 # formula is multiline $...$
2408 return self.parsemultiliner(reader, simple, simple)
2409 if FormulaConfig.starts['complex'] in reader.currentline():
2410 # formula of the form \[...\]
2411 return self.parsemultiliner(reader, FormulaConfig.starts['complex'],
2412 FormulaConfig.endings['complex'])
2413 beginbefore = FormulaConfig.starts['beginbefore']
2414 beginafter = FormulaConfig.starts['beginafter']
2415 if beginbefore in reader.currentline():
2416 if reader.currentline().strip().endswith(beginafter):
2417 current = reader.currentline().strip()
2418 endsplit = current.split(beginbefore)[1].split(beginafter)
2419 startpiece = beginbefore + endsplit[0] + beginafter
2420 endbefore = FormulaConfig.endings['endbefore']
2421 endafter = FormulaConfig.endings['endafter']
2422 endpiece = endbefore + endsplit[0] + endafter
2423 return startpiece + self.parsemultiliner(reader, startpiece, endpiece) + endpiece
2424 Trace.error('Missing ' + beginafter + ' in ' + reader.currentline())
2425 return ''
2426 begincommand = FormulaConfig.starts['command']
2427 beginbracket = FormulaConfig.starts['bracket']
2428 if begincommand in reader.currentline() and beginbracket in reader.currentline():
2429 endbracket = FormulaConfig.endings['bracket']
2430 return self.parsemultiliner(reader, beginbracket, endbracket)
2431 Trace.error('Formula beginning ' + reader.currentline() + ' is unknown')
2432 return ''
2434 def parsesingleliner(self, reader, start, ending):
2435 "Parse a formula in one line"
2436 line = reader.currentline().strip()
2437 if not start in line:
2438 Trace.error('Line ' + line + ' does not contain formula start ' + start)
2439 return ''
2440 if not line.endswith(ending):
2441 Trace.error('Formula ' + line + ' does not end with ' + ending)
2442 return ''
2443 index = line.index(start)
2444 rest = line[index + len(start):-len(ending)]
2445 reader.nextline()
2446 return rest
2448 def parsemultiliner(self, reader, start, ending):
2449 "Parse a formula in multiple lines"
2450 formula = ''
2451 line = reader.currentline()
2452 if not start in line:
2453 Trace.error('Line ' + line.strip() + ' does not contain formula start ' + start)
2454 return ''
2455 index = line.index(start)
2456 line = line[index + len(start):].strip()
2457 while not line.endswith(ending):
2458 formula += line + '\n'
2459 reader.nextline()
2460 line = reader.currentline()
2461 formula += line[:-len(ending)]
2462 reader.nextline()
2463 return formula
2465 class MacroParser(FormulaParser):
2466 "A parser for a formula macro."
2468 def parseheader(self, reader):
2469 "See if the formula is inlined"
2470 self.begin = reader.linenumber + 1
2471 return ['inline']
2473 def parse(self, reader):
2474 "Parse the formula until the end"
2475 formula = self.parsemultiliner(reader, self.parent.start, self.ending)
2476 reader.nextline()
2477 return formula
2487 class FormulaBit(Container):
2488 "A bit of a formula"
2490 type = None
2491 size = 1
2492 original = ''
2494 def __init__(self):
2495 "The formula bit type can be 'alpha', 'number', 'font'."
2496 self.contents = []
2497 self.output = ContentsOutput()
2499 def setfactory(self, factory):
2500 "Set the internal formula factory."
2501 self.factory = factory
2502 return self
2504 def add(self, bit):
2505 "Add any kind of formula bit already processed"
2506 self.contents.append(bit)
2507 self.original += bit.original
2508 bit.parent = self
2510 def skiporiginal(self, string, pos):
2511 "Skip a string and add it to the original formula"
2512 self.original += string
2513 if not pos.checkskip(string):
2514 Trace.error('String ' + string + ' not at ' + pos.identifier())
2516 def computesize(self):
2517 "Compute the size of the bit as the max of the sizes of all contents."
2518 if len(self.contents) == 0:
2519 return 1
2520 self.size = max([element.size for element in self.contents])
2521 return self.size
2523 def clone(self):
2524 "Return a copy of itself."
2525 return self.factory.parseformula(self.original)
2527 def __unicode__(self):
2528 "Get a string representation"
2529 return self.__class__.__name__ + ' read in ' + self.original
2531 class TaggedBit(FormulaBit):
2532 "A tagged string in a formula"
2534 def constant(self, constant, tag):
2535 "Set the constant and the tag"
2536 self.output = TaggedOutput().settag(tag)
2537 self.add(FormulaConstant(constant))
2538 return self
2540 def complete(self, contents, tag, breaklines = False):
2541 "Set the constant and the tag"
2542 self.contents = contents
2543 self.output = TaggedOutput().settag(tag, breaklines)
2544 return self
2546 def selfcomplete(self, tag):
2547 "Set the self-closing tag, no contents (as in <hr/>)."
2548 self.output = TaggedOutput().settag(tag, empty = True)
2549 return self
2551 class FormulaConstant(Constant):
2552 "A constant string in a formula"
2554 def __init__(self, string):
2555 "Set the constant string"
2556 Constant.__init__(self, string)
2557 self.original = string
2558 self.size = 1
2559 self.type = None
2561 def computesize(self):
2562 "Compute the size of the constant: always 1."
2563 return self.size
2565 def clone(self):
2566 "Return a copy of itself."
2567 return FormulaConstant(self.original)
2569 def __unicode__(self):
2570 "Return a printable representation."
2571 return 'Formula constant: ' + self.string
2573 class RawText(FormulaBit):
2574 "A bit of text inside a formula"
2576 def detect(self, pos):
2577 "Detect a bit of raw text"
2578 return pos.current().isalpha()
2580 def parsebit(self, pos):
2581 "Parse alphabetic text"
2582 alpha = pos.globalpha()
2583 self.add(FormulaConstant(alpha))
2584 self.type = 'alpha'
2586 class FormulaSymbol(FormulaBit):
2587 "A symbol inside a formula"
2589 modified = FormulaConfig.modified
2590 unmodified = FormulaConfig.unmodified['characters']
2592 def detect(self, pos):
2593 "Detect a symbol"
2594 if pos.current() in FormulaSymbol.unmodified:
2595 return True
2596 if pos.current() in FormulaSymbol.modified:
2597 return True
2598 return False
2600 def parsebit(self, pos):
2601 "Parse the symbol"
2602 if pos.current() in FormulaSymbol.unmodified:
2603 self.addsymbol(pos.current(), pos)
2604 return
2605 if pos.current() in FormulaSymbol.modified:
2606 self.addsymbol(FormulaSymbol.modified[pos.current()], pos)
2607 return
2608 Trace.error('Symbol ' + pos.current() + ' not found')
2610 def addsymbol(self, symbol, pos):
2611 "Add a symbol"
2612 self.skiporiginal(pos.current(), pos)
2613 self.contents.append(FormulaConstant(symbol))
2615 class FormulaNumber(FormulaBit):
2616 "A string of digits in a formula"
2618 def detect(self, pos):
2619 "Detect a digit"
2620 return pos.current().isdigit()
2622 def parsebit(self, pos):
2623 "Parse a bunch of digits"
2624 digits = pos.glob(lambda: pos.current().isdigit())
2625 self.add(FormulaConstant(digits))
2626 self.type = 'number'
2628 class Comment(FormulaBit):
2629 "A LaTeX comment: % to the end of the line."
2631 start = FormulaConfig.starts['comment']
2633 def detect(self, pos):
2634 "Detect the %."
2635 return pos.current() == self.start
2637 def parsebit(self, pos):
2638 "Parse to the end of the line."
2639 self.original += pos.globincluding('\n')
2641 class WhiteSpace(FormulaBit):
2642 "Some white space inside a formula."
2644 def detect(self, pos):
2645 "Detect the white space."
2646 return pos.current().isspace()
2648 def parsebit(self, pos):
2649 "Parse all whitespace."
2650 self.original += pos.skipspace()
2652 def __unicode__(self):
2653 "Return a printable representation."
2654 return 'Whitespace: *' + self.original + '*'
2656 class Bracket(FormulaBit):
2657 "A {} bracket inside a formula"
2659 start = FormulaConfig.starts['bracket']
2660 ending = FormulaConfig.endings['bracket']
2662 def __init__(self):
2663 "Create a (possibly literal) new bracket"
2664 FormulaBit.__init__(self)
2665 self.inner = None
2667 def detect(self, pos):
2668 "Detect the start of a bracket"
2669 return pos.checkfor(self.start)
2671 def parsebit(self, pos):
2672 "Parse the bracket"
2673 self.parsecomplete(pos, self.innerformula)
2674 return self
2676 def parsetext(self, pos):
2677 "Parse a text bracket"
2678 self.parsecomplete(pos, self.innertext)
2679 return self
2681 def parseliteral(self, pos):
2682 "Parse a literal bracket"
2683 self.parsecomplete(pos, self.innerliteral)
2684 return self
2686 def parsecomplete(self, pos, innerparser):
2687 "Parse the start and end marks"
2688 if not pos.checkfor(self.start):
2689 Trace.error('Bracket should start with ' + self.start + ' at ' + pos.identifier())
2690 return None
2691 self.skiporiginal(self.start, pos)
2692 pos.pushending(self.ending)
2693 innerparser(pos)
2694 self.original += pos.popending(self.ending)
2695 self.computesize()
2697 def innerformula(self, pos):
2698 "Parse a whole formula inside the bracket"
2699 while not pos.finished():
2700 self.add(self.factory.parseany(pos))
2702 def innertext(self, pos):
2703 "Parse some text inside the bracket, following textual rules."
2704 specialchars = FormulaConfig.symbolfunctions.keys()
2705 specialchars.append(FormulaConfig.starts['command'])
2706 specialchars.append(FormulaConfig.starts['bracket'])
2707 specialchars.append(Comment.start)
2708 while not pos.finished():
2709 if pos.current() in specialchars:
2710 self.add(self.factory.parseany(pos))
2711 if pos.checkskip(' '):
2712 self.original += ' '
2713 else:
2714 self.add(FormulaConstant(pos.skipcurrent()))
2716 def innerliteral(self, pos):
2717 "Parse a literal inside the bracket, which does not generate HTML."
2718 self.literal = ''
2719 while not pos.finished() and not pos.current() == self.ending:
2720 if pos.current() == self.start:
2721 self.parseliteral(pos)
2722 else:
2723 self.literal += pos.skipcurrent()
2724 self.original += self.literal
2726 class SquareBracket(Bracket):
2727 "A [] bracket inside a formula"
2729 start = FormulaConfig.starts['squarebracket']
2730 ending = FormulaConfig.endings['squarebracket']
2732 def clone(self):
2733 "Return a new square bracket with the same contents."
2734 bracket = SquareBracket()
2735 bracket.contents = self.contents
2736 return bracket
2740 class MathsProcessor(object):
2741 "A processor for a maths construction inside the FormulaProcessor."
2743 def process(self, contents, index):
2744 "Process an element inside a formula."
2745 Trace.error('Unimplemented process() in ' + unicode(self))
2747 def __unicode__(self):
2748 "Return a printable description."
2749 return 'Maths processor ' + self.__class__.__name__
2751 class FormulaProcessor(object):
2752 "A processor specifically for formulas."
2754 processors = []
2756 def process(self, bit):
2757 "Process the contents of every formula bit, recursively."
2758 self.processcontents(bit)
2759 self.processinsides(bit)
2760 self.traversewhole(bit)
2762 def processcontents(self, bit):
2763 "Process the contents of a formula bit."
2764 if not isinstance(bit, FormulaBit):
2765 return
2766 bit.process()
2767 for element in bit.contents:
2768 self.processcontents(element)
2770 def processinsides(self, bit):
2771 "Process the insides (limits, brackets) in a formula bit."
2772 if not isinstance(bit, FormulaBit):
2773 return
2774 for index, element in enumerate(bit.contents):
2775 for processor in self.processors:
2776 processor.process(bit.contents, index)
2777 # continue with recursive processing
2778 self.processinsides(element)
2780 def traversewhole(self, formula):
2781 "Traverse over the contents to alter variables and space units."
2782 last = None
2783 for bit, contents in self.traverse(formula):
2784 if bit.type == 'alpha':
2785 self.italicize(bit, contents)
2786 elif bit.type == 'font' and last and last.type == 'number':
2787 bit.contents.insert(0, FormulaConstant(u' '))
2788 last = bit
2790 def traverse(self, bit):
2791 "Traverse a formula and yield a flattened structure of (bit, list) pairs."
2792 for element in bit.contents:
2793 if hasattr(element, 'type') and element.type:
2794 yield (element, bit.contents)
2795 elif isinstance(element, FormulaBit):
2796 for pair in self.traverse(element):
2797 yield pair
2799 def italicize(self, bit, contents):
2800 "Italicize the given bit of text."
2801 index = contents.index(bit)
2802 contents[index] = TaggedBit().complete([bit], 'i')
2807 class Formula(Container):
2808 "A LaTeX formula"
2810 def __init__(self):
2811 self.parser = FormulaParser()
2812 self.output = TaggedOutput().settag('span class="formula"')
2814 def process(self):
2815 "Convert the formula to tags"
2816 if self.header[0] == 'inline':
2817 DocumentParameters.displaymode = False
2818 else:
2819 DocumentParameters.displaymode = True
2820 self.output.settag('div class="formula"', True)
2821 if Options.jsmath:
2822 self.jsmath()
2823 elif Options.mathjax:
2824 self.mathjax()
2825 elif Options.googlecharts:
2826 self.googlecharts()
2827 else:
2828 self.classic()
2830 def jsmath(self):
2831 "Make the contents for jsMath."
2832 if self.header[0] != 'inline':
2833 self.output = TaggedOutput().settag('div class="math"')
2834 else:
2835 self.output = TaggedOutput().settag('span class="math"')
2836 self.contents = [Constant(self.parsed)]
2838 def mathjax(self):
2839 "Make the contents for MathJax."
2840 self.output.tag = 'span class="MathJax_Preview"'
2841 tag = 'script type="math/tex'
2842 if self.header[0] != 'inline':
2843 tag += ';mode=display'
2844 self.contents = [TaggedText().constant(self.parsed, tag + '"', True)]
2846 def googlecharts(self):
2847 "Make the contents using Google Charts http://code.google.com/apis/chart/."
2848 url = FormulaConfig.urls['googlecharts'] + urllib.quote_plus(self.parsed)
2849 img = '<img class="chart" src="' + url + '" alt="' + self.parsed + '"/>'
2850 self.contents = [Constant(img)]
2852 def classic(self):
2853 "Make the contents using classic output generation with XHTML and CSS."
2854 whole = FormulaFactory().parseformula(self.parsed)
2855 FormulaProcessor().process(whole)
2856 whole.parent = self
2857 self.contents = [whole]
2859 def parse(self, pos):
2860 "Parse using a parse position instead of self.parser."
2861 if pos.checkskip('$$'):
2862 self.parsedollarblock(pos)
2863 elif pos.checkskip('$'):
2864 self.parsedollarinline(pos)
2865 elif pos.checkskip('\\('):
2866 self.parseinlineto(pos, '\\)')
2867 elif pos.checkskip('\\['):
2868 self.parseblockto(pos, '\\]')
2869 else:
2870 pos.error('Unparseable formula')
2871 self.process()
2872 return self
2874 def parsedollarinline(self, pos):
2875 "Parse a $...$ formula."
2876 self.header = ['inline']
2877 self.parsedollar(pos)
2879 def parsedollarblock(self, pos):
2880 "Parse a $$...$$ formula."
2881 self.header = ['block']
2882 self.parsedollar(pos)
2883 if not pos.checkskip('$'):
2884 pos.error('Formula should be $$...$$, but last $ is missing.')
2886 def parsedollar(self, pos):
2887 "Parse to the next $."
2888 pos.pushending('$')
2889 self.parsed = pos.globexcluding('$')
2890 pos.popending('$')
2892 def parseinlineto(self, pos, limit):
2893 "Parse a \\(...\\) formula."
2894 self.header = ['inline']
2895 self.parseupto(pos, limit)
2897 def parseblockto(self, pos, limit):
2898 "Parse a \\[...\\] formula."
2899 self.header = ['block']
2900 self.parseupto(pos, limit)
2902 def parseupto(self, pos, limit):
2903 "Parse a formula that ends with the given command."
2904 pos.pushending(limit)
2905 self.parsed = pos.glob(lambda: True)
2906 pos.popending(limit)
2908 def __unicode__(self):
2909 "Return a printable representation."
2910 if self.partkey and self.partkey.number:
2911 return 'Formula (' + self.partkey.number + ')'
2912 return 'Unnumbered formula'
2914 class WholeFormula(FormulaBit):
2915 "Parse a whole formula"
2917 def detect(self, pos):
2918 "Not outside the formula is enough."
2919 return not pos.finished()
2921 def parsebit(self, pos):
2922 "Parse with any formula bit"
2923 while not pos.finished():
2924 self.add(self.factory.parseany(pos))
2926 class FormulaFactory(object):
2927 "Construct bits of formula"
2929 # bit types will be appended later
2930 types = [FormulaSymbol, RawText, FormulaNumber, Bracket, Comment, WhiteSpace]
2931 skippedtypes = [Comment, WhiteSpace]
2932 defining = False
2934 def __init__(self):
2935 "Initialize the map of instances."
2936 self.instances = dict()
2938 def detecttype(self, type, pos):
2939 "Detect a bit of a given type."
2940 if pos.finished():
2941 return False
2942 return self.instance(type).detect(pos)
2944 def instance(self, type):
2945 "Get an instance of the given type."
2946 if not type in self.instances or not self.instances[type]:
2947 self.instances[type] = self.create(type)
2948 return self.instances[type]
2950 def create(self, type):
2951 "Create a new formula bit of the given type."
2952 return Cloner.create(type).setfactory(self)
2954 def clearskipped(self, pos):
2955 "Clear any skipped types."
2956 while not pos.finished():
2957 if not self.skipany(pos):
2958 return
2959 return
2961 def skipany(self, pos):
2962 "Skip any skipped types."
2963 for type in self.skippedtypes:
2964 if self.instance(type).detect(pos):
2965 return self.parsetype(type, pos)
2966 return None
2968 def parseany(self, pos):
2969 "Parse any formula bit at the current location."
2970 for type in self.types + self.skippedtypes:
2971 if self.detecttype(type, pos):
2972 return self.parsetype(type, pos)
2973 Trace.error('Unrecognized formula at ' + pos.identifier())
2974 return FormulaConstant(pos.skipcurrent())
2976 def parsetype(self, type, pos):
2977 "Parse the given type and return it."
2978 bit = self.instance(type)
2979 self.instances[type] = None
2980 returnedbit = bit.parsebit(pos)
2981 if returnedbit:
2982 return returnedbit.setfactory(self)
2983 return bit
2985 def parseformula(self, formula):
2986 "Parse a string of text that contains a whole formula."
2987 pos = TextPosition(formula)
2988 whole = self.create(WholeFormula)
2989 if whole.detect(pos):
2990 whole.parsebit(pos)
2991 return whole
2992 # no formula found
2993 if not pos.finished():
2994 Trace.error('Unknown formula at: ' + pos.identifier())
2995 whole.add(TaggedBit().constant(formula, 'span class="unknown"'))
2996 return whole
3001 import unicodedata
3014 import gettext
3017 class Translator(object):
3018 "Reads the configuration file and tries to find a translation."
3019 "Otherwise falls back to the messages in the config file."
3021 instance = None
3023 def translate(cls, key):
3024 "Get the translated message for a key."
3025 return cls.instance.getmessage(key)
3027 translate = classmethod(translate)
3029 def __init__(self):
3030 self.translation = None
3031 self.first = True
3033 def findtranslation(self):
3034 "Find the translation for the document language."
3035 self.langcodes = None
3036 if not DocumentParameters.language:
3037 Trace.error('No language in document')
3038 return
3039 if not DocumentParameters.language in TranslationConfig.languages:
3040 Trace.error('Unknown language ' + DocumentParameters.language)
3041 return
3042 if TranslationConfig.languages[DocumentParameters.language] == 'en':
3043 return
3044 langcodes = [TranslationConfig.languages[DocumentParameters.language]]
3045 try:
3046 self.translation = gettext.translation('elyxer', None, langcodes)
3047 except IOError:
3048 Trace.error('No translation for ' + unicode(langcodes))
3050 def getmessage(self, key):
3051 "Get the translated message for the given key."
3052 if self.first:
3053 self.findtranslation()
3054 self.first = False
3055 message = self.getuntranslated(key)
3056 if not self.translation:
3057 return message
3058 try:
3059 message = self.translation.ugettext(message)
3060 except IOError:
3061 pass
3062 return message
3064 def getuntranslated(self, key):
3065 "Get the untranslated message."
3066 if not key in TranslationConfig.constants:
3067 Trace.error('Cannot translate ' + key)
3068 return key
3069 return TranslationConfig.constants[key]
3071 Translator.instance = Translator()
3075 class NumberCounter(object):
3076 "A counter for numbers (by default)."
3077 "The type can be changed to return letters, roman numbers..."
3079 name = None
3080 value = None
3081 mode = None
3082 master = None
3084 letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
3085 symbols = NumberingConfig.sequence['symbols']
3086 romannumerals = [
3087 ('M', 1000), ('CM', 900), ('D', 500), ('CD', 400), ('C', 100),
3088 ('XC', 90), ('L', 50), ('XL', 40), ('X', 10), ('IX', 9), ('V', 5),
3089 ('IV', 4), ('I', 1)
3092 def __init__(self, name):
3093 "Give a name to the counter."
3094 self.name = name
3096 def setmode(self, mode):
3097 "Set the counter mode. Can be changed at runtime."
3098 self.mode = mode
3099 return self
3101 def init(self, value):
3102 "Set an initial value."
3103 self.value = value
3105 def gettext(self):
3106 "Get the next value as a text string."
3107 return unicode(self.value)
3109 def getletter(self):
3110 "Get the next value as a letter."
3111 return self.getsequence(self.letters)
3113 def getsymbol(self):
3114 "Get the next value as a symbol."
3115 return self.getsequence(self.symbols)
3117 def getsequence(self, sequence):
3118 "Get the next value from elyxer.a sequence."
3119 return sequence[(self.value - 1) % len(sequence)]
3121 def getroman(self):
3122 "Get the next value as a roman number."
3123 result = ''
3124 number = self.value
3125 for numeral, value in self.romannumerals:
3126 if number >= value:
3127 result += numeral * (number / value)
3128 number = number % value
3129 return result
3131 def getvalue(self):
3132 "Get the current value as configured in the current mode."
3133 if not self.mode or self.mode in ['text', '1']:
3134 return self.gettext()
3135 if self.mode == 'A':
3136 return self.getletter()
3137 if self.mode == 'a':
3138 return self.getletter().lower()
3139 if self.mode == 'I':
3140 return self.getroman()
3141 if self.mode == '*':
3142 return self.getsymbol()
3143 Trace.error('Unknown counter mode ' + self.mode)
3144 return self.gettext()
3146 def getnext(self):
3147 "Increase the current value and get the next value as configured."
3148 if not self.value:
3149 self.value = 0
3150 self.value += 1
3151 return self.getvalue()
3153 def reset(self):
3154 "Reset the counter."
3155 self.value = 0
3157 def __unicode__(self):
3158 "Return a printable representation."
3159 result = 'Counter ' + self.name
3160 if self.mode:
3161 result += ' in mode ' + self.mode
3162 return result
3164 class DependentCounter(NumberCounter):
3165 "A counter which depends on another one (the master)."
3167 def setmaster(self, master):
3168 "Set the master counter."
3169 self.master = master
3170 self.last = self.master.getvalue()
3171 return self
3173 def getnext(self):
3174 "Increase or, if the master counter has changed, restart."
3175 if self.last != self.master.getvalue():
3176 self.reset()
3177 value = NumberCounter.getnext(self)
3178 self.last = self.master.getvalue()
3179 return value
3181 def getvalue(self):
3182 "Get the value of the combined counter: master.dependent."
3183 return self.master.getvalue() + '.' + NumberCounter.getvalue(self)
3185 class NumberGenerator(object):
3186 "A number generator for unique sequences and hierarchical structures. Used in:"
3187 " * ordered part numbers: Chapter 3, Section 5.3."
3188 " * unique part numbers: Footnote 15, Bibliography cite [15]."
3189 " * chaptered part numbers: Figure 3.15, Equation (8.3)."
3190 " * unique roman part numbers: Part I, Book IV."
3192 chaptered = None
3193 generator = None
3195 romanlayouts = [x.lower() for x in NumberingConfig.layouts['roman']]
3196 orderedlayouts = [x.lower() for x in NumberingConfig.layouts['ordered']]
3198 counters = dict()
3199 appendix = None
3201 def deasterisk(self, type):
3202 "Remove the possible asterisk in a layout type."
3203 return type.replace('*', '')
3205 def isunique(self, type):
3206 "Find out if the layout type corresponds to a unique part."
3207 return self.isroman(type)
3209 def isroman(self, type):
3210 "Find out if the layout type should have roman numeration."
3211 return self.deasterisk(type).lower() in self.romanlayouts
3213 def isinordered(self, type):
3214 "Find out if the layout type corresponds to an (un)ordered part."
3215 return self.deasterisk(type).lower() in self.orderedlayouts
3217 def isnumbered(self, type):
3218 "Find out if the type for a layout corresponds to a numbered layout."
3219 if '*' in type:
3220 return False
3221 if self.isroman(type):
3222 return True
3223 if not self.isinordered(type):
3224 return False
3225 if self.getlevel(type) > DocumentParameters.maxdepth:
3226 return False
3227 return True
3229 def isunordered(self, type):
3230 "Find out if the type contains an asterisk, basically."
3231 return '*' in type
3233 def getlevel(self, type):
3234 "Get the level that corresponds to a layout type."
3235 if self.isunique(type):
3236 return 0
3237 if not self.isinordered(type):
3238 Trace.error('Unknown layout type ' + type)
3239 return 0
3240 type = self.deasterisk(type).lower()
3241 level = self.orderedlayouts.index(type) + 1
3242 return level - DocumentParameters.startinglevel
3244 def getparttype(self, type):
3245 "Obtain the type for the part: without the asterisk, "
3246 "and switched to Appendix if necessary."
3247 if NumberGenerator.appendix and self.getlevel(type) == 1:
3248 return 'Appendix'
3249 return self.deasterisk(type)
3251 def generate(self, type):
3252 "Generate a number for a layout type."
3253 "Unique part types such as Part or Book generate roman numbers: Part I."
3254 "Ordered part types return dot-separated tuples: Chapter 5, Subsection 2.3.5."
3255 "Everything else generates unique numbers: Bibliography [1]."
3256 "Each invocation results in a new number."
3257 return self.getcounter(type).getnext()
3259 def getcounter(self, type):
3260 "Get the counter for the given type."
3261 type = type.lower()
3262 if not type in self.counters:
3263 self.counters[type] = self.create(type)
3264 return self.counters[type]
3266 def create(self, type):
3267 "Create a counter for the given type."
3268 if self.isnumbered(type) and self.getlevel(type) > 1:
3269 index = self.orderedlayouts.index(type)
3270 above = self.orderedlayouts[index - 1]
3271 master = self.getcounter(above)
3272 return self.createdependent(type, master)
3273 counter = NumberCounter(type)
3274 if self.isroman(type):
3275 counter.setmode('I')
3276 return counter
3278 def getdependentcounter(self, type, master):
3279 "Get (or create) a counter of the given type that depends on another."
3280 if not type in self.counters or not self.counters[type].master:
3281 self.counters[type] = self.createdependent(type, master)
3282 return self.counters[type]
3284 def createdependent(self, type, master):
3285 "Create a dependent counter given the master."
3286 return DependentCounter(type).setmaster(master)
3288 def startappendix(self):
3289 "Start appendices here."
3290 firsttype = self.orderedlayouts[DocumentParameters.startinglevel]
3291 counter = self.getcounter(firsttype)
3292 counter.setmode('A').reset()
3293 NumberGenerator.appendix = True
3295 class ChapteredGenerator(NumberGenerator):
3296 "Generate chaptered numbers, as in Chapter.Number."
3297 "Used in equations, figures: Equation (5.3), figure 8.15."
3299 def generate(self, type):
3300 "Generate a number which goes with first-level numbers (chapters). "
3301 "For the article classes a unique number is generated."
3302 if DocumentParameters.startinglevel > 0:
3303 return NumberGenerator.generator.generate(type)
3304 chapter = self.getcounter('Chapter')
3305 return self.getdependentcounter(type, chapter).getnext()
3308 NumberGenerator.chaptered = ChapteredGenerator()
3309 NumberGenerator.generator = NumberGenerator()
3316 class ContainerSize(object):
3317 "The size of a container."
3319 width = None
3320 height = None
3321 maxwidth = None
3322 maxheight = None
3323 scale = None
3325 def set(self, width = None, height = None):
3326 "Set the proper size with width and height."
3327 self.setvalue('width', width)
3328 self.setvalue('height', height)
3329 return self
3331 def setmax(self, maxwidth = None, maxheight = None):
3332 "Set max width and/or height."
3333 self.setvalue('maxwidth', maxwidth)
3334 self.setvalue('maxheight', maxheight)
3335 return self
3337 def readparameters(self, container):
3338 "Read some size parameters off a container."
3339 self.setparameter(container, 'width')
3340 self.setparameter(container, 'height')
3341 self.setparameter(container, 'scale')
3342 self.checkvalidheight(container)
3343 return self
3345 def setparameter(self, container, name):
3346 "Read a size parameter off a container, and set it if present."
3347 value = container.getparameter(name)
3348 self.setvalue(name, value)
3350 def setvalue(self, name, value):
3351 "Set the value of a parameter name, only if it's valid."
3352 value = self.processparameter(value)
3353 if value:
3354 setattr(self, name, value)
3356 def checkvalidheight(self, container):
3357 "Check if the height parameter is valid; otherwise erase it."
3358 heightspecial = container.getparameter('height_special')
3359 if self.height and self.extractnumber(self.height) == '1' and heightspecial == 'totalheight':
3360 self.height = None
3362 def processparameter(self, value):
3363 "Do the full processing on a parameter."
3364 if not value:
3365 return None
3366 if self.extractnumber(value) == '0':
3367 return None
3368 for ignored in StyleConfig.size['ignoredtexts']:
3369 if ignored in value:
3370 value = value.replace(ignored, '')
3371 return value
3373 def extractnumber(self, text):
3374 "Extract the first number in the given text."
3375 result = ''
3376 decimal = False
3377 for char in text:
3378 if char.isdigit():
3379 result += char
3380 elif char == '.' and not decimal:
3381 result += char
3382 decimal = True
3383 else:
3384 return result
3385 return result
3387 def checkimage(self, width, height):
3388 "Check image dimensions, set them if possible."
3389 if width:
3390 self.maxwidth = unicode(width) + 'px'
3391 if self.scale and not self.width:
3392 self.width = self.scalevalue(width)
3393 if height:
3394 self.maxheight = unicode(height) + 'px'
3395 if self.scale and not self.height:
3396 self.height = self.scalevalue(height)
3397 if self.width and not self.height:
3398 self.height = 'auto'
3399 if self.height and not self.width:
3400 self.width = 'auto'
3402 def scalevalue(self, value):
3403 "Scale the value according to the image scale and return it as unicode."
3404 scaled = value * int(self.scale) / 100
3405 return unicode(int(scaled)) + 'px'
3407 def removepercentwidth(self):
3408 "Remove percent width if present, to set it at the figure level."
3409 if not self.width:
3410 return None
3411 if not '%' in self.width:
3412 return None
3413 width = self.width
3414 self.width = None
3415 if self.height == 'auto':
3416 self.height = None
3417 return width
3419 def addstyle(self, container):
3420 "Add the proper style attribute to the output tag."
3421 if not isinstance(container.output, TaggedOutput):
3422 Trace.error('No tag to add style, in ' + unicode(container))
3423 if not self.width and not self.height and not self.maxwidth and not self.maxheight:
3424 # nothing to see here; move along
3425 return
3426 tag = ' style="'
3427 tag += self.styleparameter('width')
3428 tag += self.styleparameter('maxwidth')
3429 tag += self.styleparameter('height')
3430 tag += self.styleparameter('maxheight')
3431 if tag[-1] == ' ':
3432 tag = tag[:-1]
3433 tag += '"'
3434 container.output.tag += tag
3436 def styleparameter(self, name):
3437 "Get the style for a single parameter."
3438 value = getattr(self, name)
3439 if value:
3440 return name.replace('max', 'max-') + ': ' + value + '; '
3441 return ''
3445 class QuoteContainer(Container):
3446 "A container for a pretty quote"
3448 def __init__(self):
3449 self.parser = BoundedParser()
3450 self.output = FixedOutput()
3452 def process(self):
3453 "Process contents"
3454 self.type = self.header[2]
3455 if not self.type in StyleConfig.quotes:
3456 Trace.error('Quote type ' + self.type + ' not found')
3457 self.html = ['"']
3458 return
3459 self.html = [StyleConfig.quotes[self.type]]
3461 class LyXLine(Container):
3462 "A Lyx line"
3464 def __init__(self):
3465 self.parser = LoneCommand()
3466 self.output = FixedOutput()
3468 def process(self):
3469 self.html = ['<hr class="line" />']
3471 class EmphaticText(TaggedText):
3472 "Text with emphatic mode"
3474 def process(self):
3475 self.output.tag = 'i'
3477 class ShapedText(TaggedText):
3478 "Text shaped (italic, slanted)"
3480 def process(self):
3481 self.type = self.header[1]
3482 if not self.type in TagConfig.shaped:
3483 Trace.error('Unrecognized shape ' + self.header[1])
3484 self.output.tag = 'span'
3485 return
3486 self.output.tag = TagConfig.shaped[self.type]
3488 class VersalitasText(TaggedText):
3489 "Text in versalitas"
3491 def process(self):
3492 self.output.tag = 'span class="versalitas"'
3494 class ColorText(TaggedText):
3495 "Colored text"
3497 def process(self):
3498 self.color = self.header[1]
3499 self.output.tag = 'span class="' + self.color + '"'
3501 class SizeText(TaggedText):
3502 "Sized text"
3504 def process(self):
3505 self.size = self.header[1]
3506 self.output.tag = 'span class="' + self.size + '"'
3508 class BoldText(TaggedText):
3509 "Bold text"
3511 def process(self):
3512 self.output.tag = 'b'
3514 class TextFamily(TaggedText):
3515 "A bit of text from elyxer.a different family"
3517 def process(self):
3518 "Parse the type of family"
3519 self.type = self.header[1]
3520 if not self.type in TagConfig.family:
3521 Trace.error('Unrecognized family ' + type)
3522 self.output.tag = 'span'
3523 return
3524 self.output.tag = TagConfig.family[self.type]
3526 class Hfill(TaggedText):
3527 "Horizontall fill"
3529 def process(self):
3530 self.output.tag = 'span class="hfill"'
3532 class BarredText(TaggedText):
3533 "Text with a bar somewhere"
3535 def process(self):
3536 "Parse the type of bar"
3537 self.type = self.header[1]
3538 if not self.type in TagConfig.barred:
3539 Trace.error('Unknown bar type ' + self.type)
3540 self.output.tag = 'span'
3541 return
3542 self.output.tag = TagConfig.barred[self.type]
3544 class LangLine(BlackBox):
3545 "A line with language information"
3547 def process(self):
3548 self.lang = self.header[1]
3550 class InsetLength(BlackBox):
3551 "A length measure inside an inset."
3553 def process(self):
3554 self.length = self.header[1]
3556 class Space(Container):
3557 "A space of several types"
3559 def __init__(self):
3560 self.parser = InsetParser()
3561 self.output = FixedOutput()
3563 def process(self):
3564 self.type = self.header[2]
3565 if self.type not in StyleConfig.hspaces:
3566 Trace.error('Unknown space type ' + self.type)
3567 self.html = [' ']
3568 return
3569 self.html = [StyleConfig.hspaces[self.type]]
3570 length = self.getlength()
3571 if not length:
3572 return
3573 self.output = TaggedOutput().settag('span class="hspace"', False)
3574 ContainerSize().set(length).addstyle(self)
3576 def getlength(self):
3577 "Get the space length from elyxer.the contents or parameters."
3578 if len(self.contents) == 0 or not isinstance(self.contents[0], InsetLength):
3579 return None
3580 return self.contents[0].length
3582 class VerticalSpace(Container):
3583 "An inset that contains a vertical space."
3585 def __init__(self):
3586 self.parser = InsetParser()
3587 self.output = FixedOutput()
3589 def process(self):
3590 "Set the correct tag"
3591 self.type = self.header[2]
3592 if self.type not in StyleConfig.vspaces:
3593 self.output = TaggedOutput().settag('div class="vspace" style="height: ' + self.type + ';"', True)
3594 return
3595 self.html = [StyleConfig.vspaces[self.type]]
3597 class Align(Container):
3598 "Bit of aligned text"
3600 def __init__(self):
3601 self.parser = ExcludingParser()
3602 self.output = TaggedOutput().setbreaklines(True)
3604 def process(self):
3605 self.output.tag = 'div class="' + self.header[1] + '"'
3607 class Newline(Container):
3608 "A newline"
3610 def __init__(self):
3611 self.parser = LoneCommand()
3612 self.output = FixedOutput()
3614 def process(self):
3615 "Process contents"
3616 self.html = ['<br/>\n']
3618 class NewPage(Newline):
3619 "A new page"
3621 def process(self):
3622 "Process contents"
3623 self.html = ['<p><br/>\n</p>\n']
3625 class Separator(Container):
3626 "A separator string which is not extracted by extracttext()."
3628 def __init__(self, constant):
3629 self.output = FixedOutput()
3630 self.contents = []
3631 self.html = [constant]
3633 class StrikeOut(TaggedText):
3634 "Striken out text."
3636 def process(self):
3637 "Set the output tag to strike."
3638 self.output.tag = 'strike'
3640 class StartAppendix(BlackBox):
3641 "Mark to start an appendix here."
3642 "From this point on, all chapters become appendices."
3644 def process(self):
3645 "Activate the special numbering scheme for appendices, using letters."
3646 NumberGenerator.generator.startappendix()
3653 class Link(Container):
3654 "A link to another part of the document"
3656 anchor = None
3657 url = None
3658 type = None
3659 page = None
3660 target = None
3661 destination = None
3662 title = None
3664 def __init__(self):
3665 "Initialize the link, add target if configured."
3666 self.contents = []
3667 self.parser = InsetParser()
3668 self.output = LinkOutput()
3669 if Options.target:
3670 self.target = Options.target
3672 def complete(self, text, anchor = None, url = None, type = None, title = None):
3673 "Complete the link."
3674 self.contents = [Constant(text)]
3675 if anchor:
3676 self.anchor = anchor
3677 if url:
3678 self.url = url
3679 if type:
3680 self.type = type
3681 if title:
3682 self.title = title
3683 return self
3685 def computedestination(self):
3686 "Use the destination link to fill in the destination URL."
3687 if not self.destination:
3688 return
3689 self.url = ''
3690 if self.destination.anchor:
3691 self.url = '#' + self.destination.anchor
3692 if self.destination.page:
3693 self.url = self.destination.page + self.url
3695 def setmutualdestination(self, destination):
3696 "Set another link as destination, and set its destination to this one."
3697 self.destination = destination
3698 destination.destination = self
3700 def __unicode__(self):
3701 "Return a printable representation."
3702 result = 'Link'
3703 if self.anchor:
3704 result += ' #' + self.anchor
3705 if self.url:
3706 result += ' to ' + self.url
3707 return result
3709 class URL(Link):
3710 "A clickable URL"
3712 def process(self):
3713 "Read URL from elyxer.parameters"
3714 target = self.escape(self.getparameter('target'))
3715 self.url = target
3716 type = self.getparameter('type')
3717 if type:
3718 self.url = self.escape(type) + target
3719 name = self.getparameter('name')
3720 if not name:
3721 name = target
3722 self.contents = [Constant(name)]
3724 class FlexURL(URL):
3725 "A flexible URL"
3727 def process(self):
3728 "Read URL from elyxer.contents"
3729 self.url = self.extracttext()
3731 class LinkOutput(ContainerOutput):
3732 "A link pointing to some destination"
3733 "Or an anchor (destination)"
3735 def gethtml(self, link):
3736 "Get the HTML code for the link"
3737 type = link.__class__.__name__
3738 if link.type:
3739 type = link.type
3740 tag = 'a class="' + type + '"'
3741 if link.anchor:
3742 tag += ' name="' + link.anchor + '"'
3743 if link.destination:
3744 link.computedestination()
3745 if link.url:
3746 tag += ' href="' + link.url + '"'
3747 if link.target:
3748 tag += ' target="' + link.target + '"'
3749 if link.title:
3750 tag += ' title="' + link.title + '"'
3751 return TaggedOutput().settag(tag).gethtml(link)
3757 class Postprocessor(object):
3758 "Postprocess a container keeping some context"
3760 stages = []
3762 def __init__(self):
3763 self.stages = StageDict(Postprocessor.stages, self)
3764 self.current = None
3765 self.last = None
3767 def postprocess(self, next):
3768 "Postprocess a container and its contents."
3769 self.postrecursive(self.current)
3770 result = self.postcurrent(next)
3771 self.last = self.current
3772 self.current = next
3773 return result
3775 def postrecursive(self, container):
3776 "Postprocess the container contents recursively"
3777 if not hasattr(container, 'contents'):
3778 return
3779 if len(container.contents) == 0:
3780 return
3781 if hasattr(container, 'postprocess'):
3782 if not container.postprocess:
3783 return
3784 postprocessor = Postprocessor()
3785 contents = []
3786 for element in container.contents:
3787 post = postprocessor.postprocess(element)
3788 if post:
3789 contents.append(post)
3790 # two rounds to empty the pipeline
3791 for i in range(2):
3792 post = postprocessor.postprocess(None)
3793 if post:
3794 contents.append(post)
3795 container.contents = contents
3797 def postcurrent(self, next):
3798 "Postprocess the current element taking into account next and last."
3799 stage = self.stages.getstage(self.current)
3800 if not stage:
3801 return self.current
3802 return stage.postprocess(self.last, self.current, next)
3804 class StageDict(object):
3805 "A dictionary of stages corresponding to classes"
3807 def __init__(self, classes, postprocessor):
3808 "Instantiate an element from elyxer.each class and store as a dictionary"
3809 instances = self.instantiate(classes, postprocessor)
3810 self.stagedict = dict([(x.processedclass, x) for x in instances])
3812 def instantiate(self, classes, postprocessor):
3813 "Instantiate an element from elyxer.each class"
3814 stages = [x.__new__(x) for x in classes]
3815 for element in stages:
3816 element.__init__()
3817 element.postprocessor = postprocessor
3818 return stages
3820 def getstage(self, element):
3821 "Get the stage for a given element, if the type is in the dict"
3822 if not element.__class__ in self.stagedict:
3823 return None
3824 return self.stagedict[element.__class__]
3828 class Label(Link):
3829 "A label to be referenced"
3831 names = dict()
3832 lastlayout = None
3834 def __init__(self):
3835 Link.__init__(self)
3836 self.lastnumbered = None
3838 def process(self):
3839 "Process a label container."
3840 key = self.getparameter('name')
3841 self.create(' ', key)
3842 self.lastnumbered = Label.lastlayout
3844 def create(self, text, key, type = 'Label'):
3845 "Create the label for a given key."
3846 self.key = key
3847 self.complete(text, anchor = key, type = type)
3848 Label.names[key] = self
3849 if key in Reference.references:
3850 for reference in Reference.references[key]:
3851 reference.destination = self
3852 return self
3854 def findpartkey(self):
3855 "Get the part key for the latest numbered container seen."
3856 numbered = self.numbered(self)
3857 if numbered and numbered.partkey:
3858 return numbered.partkey
3859 return ''
3861 def numbered(self, container):
3862 "Get the numbered container for the label."
3863 if container.partkey:
3864 return container
3865 if not container.parent:
3866 if self.lastnumbered:
3867 return self.lastnumbered
3868 return None
3869 return self.numbered(container.parent)
3871 def __unicode__(self):
3872 "Return a printable representation."
3873 if not hasattr(self, 'key'):
3874 return 'Unnamed label'
3875 return 'Label ' + self.key
3877 class Reference(Link):
3878 "A reference to a label."
3880 references = dict()
3881 key = 'none'
3883 def process(self):
3884 "Read the reference and set the arrow."
3885 self.key = self.getparameter('reference')
3886 if self.key in Label.names:
3887 self.direction = u'↑'
3888 label = Label.names[self.key]
3889 else:
3890 self.direction = u'↓'
3891 label = Label().complete(' ', self.key, 'preref')
3892 self.destination = label
3893 self.formatcontents()
3894 if not self.key in Reference.references:
3895 Reference.references[self.key] = []
3896 Reference.references[self.key].append(self)
3898 def formatcontents(self):
3899 "Format the reference contents."
3900 formatkey = self.getparameter('LatexCommand')
3901 if not formatkey:
3902 formatkey = 'ref'
3903 self.formatted = u'↕'
3904 if formatkey in StyleConfig.referenceformats:
3905 self.formatted = StyleConfig.referenceformats[formatkey]
3906 else:
3907 Trace.error('Unknown reference format ' + formatkey)
3908 self.replace(u'↕', self.direction)
3909 self.replace('#', '1')
3910 self.replace('on-page', Translator.translate('on-page'))
3911 partkey = self.destination.findpartkey()
3912 # only if partkey and partkey.number are not null, send partkey.number
3913 self.replace('@', partkey and partkey.number)
3914 self.replace(u'¶', partkey and partkey.tocentry)
3915 if not '$' in self.formatted or not partkey or not partkey.titlecontents:
3916 if '$' in self.formatted:
3917 Trace.error('No title in ' + unicode(partkey))
3918 self.contents = [Constant(self.formatted)]
3919 return
3920 pieces = self.formatted.split('$')
3921 self.contents = [Constant(pieces[0])]
3922 for piece in pieces[1:]:
3923 self.contents += partkey.titlecontents
3924 self.contents.append(Constant(piece))
3926 def replace(self, key, value):
3927 "Replace a key in the format template with a value."
3928 if not key in self.formatted:
3929 return
3930 if not value:
3931 value = ''
3932 self.formatted = self.formatted.replace(key, value)
3934 def __unicode__(self):
3935 "Return a printable representation."
3936 return 'Reference ' + self.key
3940 class FormulaCommand(FormulaBit):
3941 "A LaTeX command inside a formula"
3943 types = []
3944 start = FormulaConfig.starts['command']
3945 commandmap = None
3947 def detect(self, pos):
3948 "Find the current command."
3949 return pos.checkfor(FormulaCommand.start)
3951 def parsebit(self, pos):
3952 "Parse the command."
3953 command = self.extractcommand(pos)
3954 bit = self.parsewithcommand(command, pos)
3955 if bit:
3956 return bit
3957 if command.startswith('\\up') or command.startswith('\\Up'):
3958 upgreek = self.parseupgreek(command, pos)
3959 if upgreek:
3960 return upgreek
3961 if not self.factory.defining:
3962 Trace.error('Unknown command ' + command)
3963 self.output = TaggedOutput().settag('span class="unknown"')
3964 self.add(FormulaConstant(command))
3965 return None
3967 def parsewithcommand(self, command, pos):
3968 "Parse the command type once we have the command."
3969 for type in FormulaCommand.types:
3970 if command in type.commandmap:
3971 return self.parsecommandtype(command, type, pos)
3972 return None
3974 def parsecommandtype(self, command, type, pos):
3975 "Parse a given command type."
3976 bit = self.factory.create(type)
3977 bit.setcommand(command)
3978 returned = bit.parsebit(pos)
3979 if returned:
3980 return returned
3981 return bit
3983 def extractcommand(self, pos):
3984 "Extract the command from elyxer.the current position."
3985 if not pos.checkskip(FormulaCommand.start):
3986 pos.error('Missing command start ' + FormulaCommand.start)
3987 return
3988 if pos.finished():
3989 return self.emptycommand(pos)
3990 if pos.current().isalpha():
3991 # alpha command
3992 command = FormulaCommand.start + pos.globalpha()
3993 # skip mark of short command
3994 pos.checkskip('*')
3995 return command
3996 # symbol command
3997 return FormulaCommand.start + pos.skipcurrent()
3999 def emptycommand(self, pos):
4000 """Check for an empty command: look for command disguised as ending.
4001 Special case against '{ \{ \} }' situation."""
4002 command = ''
4003 if not pos.isout():
4004 ending = pos.nextending()
4005 if ending and pos.checkskip(ending):
4006 command = ending
4007 return FormulaCommand.start + command
4009 def parseupgreek(self, command, pos):
4010 "Parse the Greek \\up command.."
4011 if len(command) < 4:
4012 return None
4013 if command.startswith('\\up'):
4014 upcommand = '\\' + command[3:]
4015 elif pos.checkskip('\\Up'):
4016 upcommand = '\\' + command[3:4].upper() + command[4:]
4017 else:
4018 Trace.error('Impossible upgreek command: ' + command)
4019 return
4020 upgreek = self.parsewithcommand(upcommand, pos)
4021 if upgreek:
4022 upgreek.type = 'font'
4023 return upgreek
4025 class CommandBit(FormulaCommand):
4026 "A formula bit that includes a command"
4028 def setcommand(self, command):
4029 "Set the command in the bit"
4030 self.command = command
4031 if self.commandmap:
4032 self.original += command
4033 self.translated = self.commandmap[self.command]
4035 def parseparameter(self, pos):
4036 "Parse a parameter at the current position"
4037 self.factory.clearskipped(pos)
4038 if pos.finished():
4039 return None
4040 parameter = self.factory.parseany(pos)
4041 self.add(parameter)
4042 return parameter
4044 def parsesquare(self, pos):
4045 "Parse a square bracket"
4046 self.factory.clearskipped(pos)
4047 if not self.factory.detecttype(SquareBracket, pos):
4048 return None
4049 bracket = self.factory.parsetype(SquareBracket, pos)
4050 self.add(bracket)
4051 return bracket
4053 def parseliteral(self, pos):
4054 "Parse a literal bracket."
4055 self.factory.clearskipped(pos)
4056 if not self.factory.detecttype(Bracket, pos):
4057 if not pos.isvalue():
4058 Trace.error('No literal parameter found at: ' + pos.identifier())
4059 return None
4060 return pos.globvalue()
4061 bracket = Bracket().setfactory(self.factory)
4062 self.add(bracket.parseliteral(pos))
4063 return bracket.literal
4065 def parsesquareliteral(self, pos):
4066 "Parse a square bracket literally."
4067 self.factory.clearskipped(pos)
4068 if not self.factory.detecttype(SquareBracket, pos):
4069 return None
4070 bracket = SquareBracket().setfactory(self.factory)
4071 self.add(bracket.parseliteral(pos))
4072 return bracket.literal
4074 def parsetext(self, pos):
4075 "Parse a text parameter."
4076 self.factory.clearskipped(pos)
4077 if not self.factory.detecttype(Bracket, pos):
4078 Trace.error('No text parameter for ' + self.command)
4079 return None
4080 bracket = Bracket().setfactory(self.factory).parsetext(pos)
4081 self.add(bracket)
4082 return bracket
4084 class EmptyCommand(CommandBit):
4085 "An empty command (without parameters)"
4087 commandmap = FormulaConfig.commands
4089 def parsebit(self, pos):
4090 "Parse a command without parameters"
4091 self.contents = [FormulaConstant(self.translated)]
4093 class SpacedCommand(CommandBit):
4094 "An empty command which should have math spacing in formulas."
4096 commandmap = FormulaConfig.spacedcommands
4098 def parsebit(self, pos):
4099 "Place as contents the command translated and spaced."
4100 self.contents = [FormulaConstant(u' ' + self.translated + u' ')]
4102 class AlphaCommand(EmptyCommand):
4103 "A command without paramters whose result is alphabetical"
4105 commandmap = FormulaConfig.alphacommands
4107 def parsebit(self, pos):
4108 "Parse the command and set type to alpha"
4109 EmptyCommand.parsebit(self, pos)
4110 self.type = 'alpha'
4112 class OneParamFunction(CommandBit):
4113 "A function of one parameter"
4115 commandmap = FormulaConfig.onefunctions
4116 simplified = False
4118 def parsebit(self, pos):
4119 "Parse a function with one parameter"
4120 self.output = TaggedOutput().settag(self.translated)
4121 self.parseparameter(pos)
4122 self.simplifyifpossible()
4124 def simplifyifpossible(self):
4125 "Try to simplify to a single character."
4126 if self.original in self.commandmap:
4127 self.output = FixedOutput()
4128 self.html = [self.commandmap[self.original]]
4129 self.simplified = True
4131 class SymbolFunction(CommandBit):
4132 "Find a function which is represented by a symbol (like _ or ^)"
4134 commandmap = FormulaConfig.symbolfunctions
4136 def detect(self, pos):
4137 "Find the symbol"
4138 return pos.current() in SymbolFunction.commandmap
4140 def parsebit(self, pos):
4141 "Parse the symbol"
4142 self.setcommand(pos.current())
4143 pos.skip(self.command)
4144 self.output = TaggedOutput().settag(self.translated)
4145 self.parseparameter(pos)
4147 class TextFunction(CommandBit):
4148 "A function where parameters are read as text."
4150 commandmap = FormulaConfig.textfunctions
4152 def parsebit(self, pos):
4153 "Parse a text parameter"
4154 self.output = TaggedOutput().settag(self.translated)
4155 self.parsetext(pos)
4157 def process(self):
4158 "Set the type to font"
4159 self.type = 'font'
4161 class LabelFunction(CommandBit):
4162 "A function that acts as a label"
4164 commandmap = FormulaConfig.labelfunctions
4166 def parsebit(self, pos):
4167 "Parse a literal parameter"
4168 self.key = self.parseliteral(pos)
4170 def process(self):
4171 "Add an anchor with the label contents."
4172 self.type = 'font'
4173 self.label = Label().create(' ', self.key, type = 'eqnumber')
4174 self.contents = [self.label]
4175 # store as a Label so we know it's been seen
4176 Label.names[self.key] = self.label
4178 class FontFunction(OneParamFunction):
4179 "A function of one parameter that changes the font"
4181 commandmap = FormulaConfig.fontfunctions
4183 def process(self):
4184 "Simplify if possible using a single character."
4185 self.type = 'font'
4186 self.simplifyifpossible()
4188 FormulaFactory.types += [FormulaCommand, SymbolFunction]
4189 FormulaCommand.types = [
4190 AlphaCommand, EmptyCommand, OneParamFunction, FontFunction, LabelFunction,
4191 TextFunction, SpacedCommand,
4205 class BigSymbol(object):
4206 "A big symbol generator."
4208 symbols = FormulaConfig.bigsymbols
4210 def __init__(self, symbol):
4211 "Create the big symbol."
4212 self.symbol = symbol
4214 def getpieces(self):
4215 "Get an array with all pieces."
4216 if not self.symbol in self.symbols:
4217 return [self.symbol]
4218 if self.smalllimit():
4219 return [self.symbol]
4220 return self.symbols[self.symbol]
4222 def smalllimit(self):
4223 "Decide if the limit should be a small, one-line symbol."
4224 if not DocumentParameters.displaymode:
4225 return True
4226 if len(self.symbols[self.symbol]) == 1:
4227 return True
4228 return Options.simplemath
4230 class BigBracket(BigSymbol):
4231 "A big bracket generator."
4233 def __init__(self, size, bracket, alignment='l'):
4234 "Set the size and symbol for the bracket."
4235 self.size = size
4236 self.original = bracket
4237 self.alignment = alignment
4238 self.pieces = None
4239 if bracket in FormulaConfig.bigbrackets:
4240 self.pieces = FormulaConfig.bigbrackets[bracket]
4242 def getpiece(self, index):
4243 "Return the nth piece for the bracket."
4244 function = getattr(self, 'getpiece' + unicode(len(self.pieces)))
4245 return function(index)
4247 def getpiece1(self, index):
4248 "Return the only piece for a single-piece bracket."
4249 return self.pieces[0]
4251 def getpiece3(self, index):
4252 "Get the nth piece for a 3-piece bracket: parenthesis or square bracket."
4253 if index == 0:
4254 return self.pieces[0]
4255 if index == self.size - 1:
4256 return self.pieces[-1]
4257 return self.pieces[1]
4259 def getpiece4(self, index):
4260 "Get the nth piece for a 4-piece bracket: curly bracket."
4261 if index == 0:
4262 return self.pieces[0]
4263 if index == self.size - 1:
4264 return self.pieces[3]
4265 if index == (self.size - 1)/2:
4266 return self.pieces[2]
4267 return self.pieces[1]
4269 def getcell(self, index):
4270 "Get the bracket piece as an array cell."
4271 piece = self.getpiece(index)
4272 span = 'span class="bracket align-' + self.alignment + '"'
4273 return TaggedBit().constant(piece, span)
4275 def getcontents(self):
4276 "Get the bracket as an array or as a single bracket."
4277 if self.size == 1 or not self.pieces:
4278 return self.getsinglebracket()
4279 rows = []
4280 for index in range(self.size):
4281 cell = self.getcell(index)
4282 rows.append(TaggedBit().complete([cell], 'span class="arrayrow"'))
4283 return [TaggedBit().complete(rows, 'span class="array"')]
4285 def getsinglebracket(self):
4286 "Return the bracket as a single sign."
4287 if self.original == '.':
4288 return [TaggedBit().constant('', 'span class="emptydot"')]
4289 return [TaggedBit().constant(self.original, 'span class="symbol"')]
4296 class FormulaEquation(CommandBit):
4297 "A simple numbered equation."
4299 piece = 'equation'
4301 def parsebit(self, pos):
4302 "Parse the array"
4303 self.output = ContentsOutput()
4304 self.add(self.factory.parsetype(WholeFormula, pos))
4306 class FormulaCell(FormulaCommand):
4307 "An array cell inside a row"
4309 def setalignment(self, alignment):
4310 self.alignment = alignment
4311 self.output = TaggedOutput().settag('span class="arraycell align-' + alignment +'"', True)
4312 return self
4314 def parsebit(self, pos):
4315 self.factory.clearskipped(pos)
4316 if pos.finished():
4317 return
4318 self.add(self.factory.parsetype(WholeFormula, pos))
4320 class FormulaRow(FormulaCommand):
4321 "An array row inside an array"
4323 cellseparator = FormulaConfig.array['cellseparator']
4325 def setalignments(self, alignments):
4326 self.alignments = alignments
4327 self.output = TaggedOutput().settag('span class="arrayrow"', True)
4328 return self
4330 def parsebit(self, pos):
4331 "Parse a whole row"
4332 index = 0
4333 pos.pushending(self.cellseparator, optional=True)
4334 while not pos.finished():
4335 cell = self.createcell(index)
4336 cell.parsebit(pos)
4337 self.add(cell)
4338 index += 1
4339 pos.checkskip(self.cellseparator)
4340 if len(self.contents) == 0:
4341 self.output = EmptyOutput()
4343 def createcell(self, index):
4344 "Create the cell that corresponds to the given index."
4345 alignment = self.alignments[index % len(self.alignments)]
4346 return self.factory.create(FormulaCell).setalignment(alignment)
4348 class MultiRowFormula(CommandBit):
4349 "A formula with multiple rows."
4351 def parserows(self, pos):
4352 "Parse all rows, finish when no more row ends"
4353 self.rows = []
4354 first = True
4355 for row in self.iteraterows(pos):
4356 if first:
4357 first = False
4358 else:
4359 # intersparse empty rows
4360 self.addempty()
4361 row.parsebit(pos)
4362 self.addrow(row)
4363 self.size = len(self.rows)
4365 def iteraterows(self, pos):
4366 "Iterate over all rows, end when no more row ends"
4367 rowseparator = FormulaConfig.array['rowseparator']
4368 while True:
4369 pos.pushending(rowseparator, True)
4370 row = self.factory.create(FormulaRow)
4371 yield row.setalignments(self.alignments)
4372 if pos.checkfor(rowseparator):
4373 self.original += pos.popending(rowseparator)
4374 else:
4375 return
4377 def addempty(self):
4378 "Add an empty row."
4379 row = self.factory.create(FormulaRow).setalignments(self.alignments)
4380 for index, originalcell in enumerate(self.rows[-1].contents):
4381 cell = row.createcell(index)
4382 cell.add(FormulaConstant(u' '))
4383 row.add(cell)
4384 self.addrow(row)
4386 def addrow(self, row):
4387 "Add a row to the contents and to the list of rows."
4388 self.rows.append(row)
4389 self.add(row)
4391 class FormulaArray(MultiRowFormula):
4392 "An array within a formula"
4394 piece = 'array'
4396 def parsebit(self, pos):
4397 "Parse the array"
4398 self.output = TaggedOutput().settag('span class="array"', False)
4399 self.parsealignments(pos)
4400 self.parserows(pos)
4402 def parsealignments(self, pos):
4403 "Parse the different alignments"
4404 # vertical
4405 self.valign = 'c'
4406 literal = self.parsesquareliteral(pos)
4407 if literal:
4408 self.valign = literal
4409 # horizontal
4410 literal = self.parseliteral(pos)
4411 self.alignments = []
4412 for l in literal:
4413 self.alignments.append(l)
4415 class FormulaMatrix(MultiRowFormula):
4416 "A matrix (array with center alignment)."
4418 piece = 'matrix'
4420 def parsebit(self, pos):
4421 "Parse the matrix, set alignments to 'c'."
4422 self.output = TaggedOutput().settag('span class="array"', False)
4423 self.valign = 'c'
4424 self.alignments = ['c']
4425 self.parserows(pos)
4427 class FormulaCases(MultiRowFormula):
4428 "A cases statement"
4430 piece = 'cases'
4432 def parsebit(self, pos):
4433 "Parse the cases"
4434 self.output = ContentsOutput()
4435 self.alignments = ['l', 'l']
4436 self.parserows(pos)
4437 for row in self.contents:
4438 for cell in row.contents:
4439 cell.output.settag('span class="case align-l"', True)
4440 cell.contents.append(FormulaConstant(u' '))
4441 array = TaggedBit().complete(self.contents, 'span class="bracketcases"', True)
4442 brace = BigBracket(len(self.contents), '{', 'l')
4443 self.contents = brace.getcontents() + [array]
4445 class EquationEnvironment(MultiRowFormula):
4446 "A \\begin{}...\\end equation environment with rows and cells."
4448 def parsebit(self, pos):
4449 "Parse the whole environment."
4450 self.output = TaggedOutput().settag('span class="environment"', False)
4451 environment = self.piece.replace('*', '')
4452 if environment in FormulaConfig.environments:
4453 self.alignments = FormulaConfig.environments[environment]
4454 else:
4455 Trace.error('Unknown equation environment ' + self.piece)
4456 self.alignments = ['l']
4457 self.parserows(pos)
4459 class BeginCommand(CommandBit):
4460 "A \\begin{}...\end command and what it entails (array, cases, aligned)"
4462 commandmap = {FormulaConfig.array['begin']:''}
4464 types = [FormulaEquation, FormulaArray, FormulaCases, FormulaMatrix]
4466 def parsebit(self, pos):
4467 "Parse the begin command"
4468 command = self.parseliteral(pos)
4469 bit = self.findbit(command)
4470 ending = FormulaConfig.array['end'] + '{' + command + '}'
4471 pos.pushending(ending)
4472 bit.parsebit(pos)
4473 self.add(bit)
4474 self.original += pos.popending(ending)
4475 self.size = bit.size
4477 def findbit(self, piece):
4478 "Find the command bit corresponding to the \\begin{piece}"
4479 for type in BeginCommand.types:
4480 if piece.replace('*', '') == type.piece:
4481 return self.factory.create(type)
4482 bit = self.factory.create(EquationEnvironment)
4483 bit.piece = piece
4484 return bit
4486 FormulaCommand.types += [BeginCommand]
4490 class CombiningFunction(OneParamFunction):
4492 commandmap = FormulaConfig.combiningfunctions
4494 def parsebit(self, pos):
4495 "Parse a combining function."
4496 self.type = 'alpha'
4497 combining = self.translated
4498 parameter = self.parsesingleparameter(pos)
4499 if not parameter:
4500 Trace.error('Empty parameter for combining function ' + self.command)
4501 elif len(parameter.extracttext()) != 1:
4502 Trace.error('Applying combining function ' + self.command + ' to invalid string "' + parameter.extracttext() + '"')
4503 self.contents.append(Constant(combining))
4505 def parsesingleparameter(self, pos):
4506 "Parse a parameter, or a single letter."
4507 self.factory.clearskipped(pos)
4508 if pos.finished():
4509 Trace.error('Error while parsing single parameter at ' + pos.identifier())
4510 return None
4511 if self.factory.detecttype(Bracket, pos) \
4512 or self.factory.detecttype(FormulaCommand, pos):
4513 return self.parseparameter(pos)
4514 letter = FormulaConstant(pos.skipcurrent())
4515 self.add(letter)
4516 return letter
4518 class DecoratingFunction(OneParamFunction):
4519 "A function that decorates some bit of text"
4521 commandmap = FormulaConfig.decoratingfunctions
4523 def parsebit(self, pos):
4524 "Parse a decorating function"
4525 self.type = 'alpha'
4526 symbol = self.translated
4527 self.symbol = TaggedBit().constant(symbol, 'span class="symbolover"')
4528 self.parameter = self.parseparameter(pos)
4529 self.output = TaggedOutput().settag('span class="withsymbol"')
4530 self.contents.insert(0, self.symbol)
4531 self.parameter.output = TaggedOutput().settag('span class="undersymbol"')
4532 self.simplifyifpossible()
4534 class LimitCommand(EmptyCommand):
4535 "A command which accepts limits above and below, in display mode."
4537 commandmap = FormulaConfig.limitcommands
4539 def parsebit(self, pos):
4540 "Parse a limit command."
4541 pieces = BigSymbol(self.translated).getpieces()
4542 self.output = TaggedOutput().settag('span class="limits"')
4543 for piece in pieces:
4544 self.contents.append(TaggedBit().constant(piece, 'span class="limit"'))
4546 class LimitPreviousCommand(LimitCommand):
4547 "A command to limit the previous command."
4549 commandmap = None
4551 def parsebit(self, pos):
4552 "Do nothing."
4553 self.output = TaggedOutput().settag('span class="limits"')
4554 self.factory.clearskipped(pos)
4556 def __unicode__(self):
4557 "Return a printable representation."
4558 return 'Limit previous command'
4560 class LimitsProcessor(MathsProcessor):
4561 "A processor for limits inside an element."
4563 def process(self, contents, index):
4564 "Process the limits for an element."
4565 if Options.simplemath:
4566 return
4567 if self.checklimits(contents, index):
4568 self.modifylimits(contents, index)
4569 if self.checkscript(contents, index) and self.checkscript(contents, index + 1):
4570 self.modifyscripts(contents, index)
4572 def checklimits(self, contents, index):
4573 "Check if the current position has a limits command."
4574 if not DocumentParameters.displaymode:
4575 return False
4576 if self.checkcommand(contents, index + 1, LimitPreviousCommand):
4577 self.limitsahead(contents, index)
4578 return False
4579 if not isinstance(contents[index], LimitCommand):
4580 return False
4581 return self.checkscript(contents, index + 1)
4583 def limitsahead(self, contents, index):
4584 "Limit the current element based on the next."
4585 contents[index + 1].add(contents[index].clone())
4586 contents[index].output = EmptyOutput()
4588 def modifylimits(self, contents, index):
4589 "Modify a limits commands so that the limits appear above and below."
4590 limited = contents[index]
4591 subscript = self.getlimit(contents, index + 1)
4592 limited.contents.append(subscript)
4593 if self.checkscript(contents, index + 1):
4594 superscript = self.getlimit(contents, index + 1)
4595 else:
4596 superscript = TaggedBit().constant(u' ', 'sup class="limit"')
4597 limited.contents.insert(0, superscript)
4599 def getlimit(self, contents, index):
4600 "Get the limit for a limits command."
4601 limit = self.getscript(contents, index)
4602 limit.output.tag = limit.output.tag.replace('script', 'limit')
4603 return limit
4605 def modifyscripts(self, contents, index):
4606 "Modify the super- and subscript to appear vertically aligned."
4607 subscript = self.getscript(contents, index)
4608 # subscript removed so instead of index + 1 we get index again
4609 superscript = self.getscript(contents, index)
4610 scripts = TaggedBit().complete([superscript, subscript], 'span class="scripts"')
4611 contents.insert(index, scripts)
4613 def checkscript(self, contents, index):
4614 "Check if the current element is a sub- or superscript."
4615 return self.checkcommand(contents, index, SymbolFunction)
4617 def checkcommand(self, contents, index, type):
4618 "Check for the given type as the current element."
4619 if len(contents) <= index:
4620 return False
4621 return isinstance(contents[index], type)
4623 def getscript(self, contents, index):
4624 "Get the sub- or superscript."
4625 bit = contents[index]
4626 bit.output.tag += ' class="script"'
4627 del contents[index]
4628 return bit
4630 class BracketCommand(OneParamFunction):
4631 "A command which defines a bracket."
4633 commandmap = FormulaConfig.bracketcommands
4635 def parsebit(self, pos):
4636 "Parse the bracket."
4637 OneParamFunction.parsebit(self, pos)
4639 def create(self, direction, character):
4640 "Create the bracket for the given character."
4641 self.original = character
4642 self.command = '\\' + direction
4643 self.contents = [FormulaConstant(character)]
4644 return self
4646 class BracketProcessor(MathsProcessor):
4647 "A processor for bracket commands."
4649 def process(self, contents, index):
4650 "Convert the bracket using Unicode pieces, if possible."
4651 if Options.simplemath:
4652 return
4653 if self.checkleft(contents, index):
4654 return self.processleft(contents, index)
4656 def processleft(self, contents, index):
4657 "Process a left bracket."
4658 rightindex = self.findright(contents, index + 1)
4659 if not rightindex:
4660 return
4661 size = self.findmax(contents, index, rightindex)
4662 self.resize(contents[index], size)
4663 self.resize(contents[rightindex], size)
4665 def checkleft(self, contents, index):
4666 "Check if the command at the given index is left."
4667 return self.checkdirection(contents[index], '\\left')
4669 def checkright(self, contents, index):
4670 "Check if the command at the given index is right."
4671 return self.checkdirection(contents[index], '\\right')
4673 def checkdirection(self, bit, command):
4674 "Check if the given bit is the desired bracket command."
4675 if not isinstance(bit, BracketCommand):
4676 return False
4677 return bit.command == command
4679 def findright(self, contents, index):
4680 "Find the right bracket starting at the given index, or 0."
4681 depth = 1
4682 while index < len(contents):
4683 if self.checkleft(contents, index):
4684 depth += 1
4685 if self.checkright(contents, index):
4686 depth -= 1
4687 if depth == 0:
4688 return index
4689 index += 1
4690 return None
4692 def findmax(self, contents, leftindex, rightindex):
4693 "Find the max size of the contents between the two given indices."
4694 sliced = contents[leftindex:rightindex]
4695 return max([element.size for element in sliced])
4697 def resize(self, command, size):
4698 "Resize a bracket command to the given size."
4699 character = command.extracttext()
4700 alignment = command.command.replace('\\', '')
4701 bracket = BigBracket(size, character, alignment)
4702 command.output = ContentsOutput()
4703 command.contents = bracket.getcontents()
4706 FormulaCommand.types += [
4707 DecoratingFunction, CombiningFunction, LimitCommand, BracketCommand,
4710 FormulaProcessor.processors += [
4711 LimitsProcessor(), BracketProcessor(),
4716 class ParameterDefinition(object):
4717 "The definition of a parameter in a hybrid function."
4718 "[] parameters are optional, {} parameters are mandatory."
4719 "Each parameter has a one-character name, like {$1} or {$p}."
4720 "A parameter that ends in ! like {$p!} is a literal."
4721 "Example: [$1]{$p!} reads an optional parameter $1 and a literal mandatory parameter p."
4723 parambrackets = [('[', ']'), ('{', '}')]
4725 def __init__(self):
4726 self.name = None
4727 self.literal = False
4728 self.optional = False
4729 self.value = None
4730 self.literalvalue = None
4732 def parse(self, pos):
4733 "Parse a parameter definition: [$0], {$x}, {$1!}..."
4734 for (opening, closing) in ParameterDefinition.parambrackets:
4735 if pos.checkskip(opening):
4736 if opening == '[':
4737 self.optional = True
4738 if not pos.checkskip('$'):
4739 Trace.error('Wrong parameter name, did you mean $' + pos.current() + '?')
4740 return None
4741 self.name = pos.skipcurrent()
4742 if pos.checkskip('!'):
4743 self.literal = True
4744 if not pos.checkskip(closing):
4745 Trace.error('Wrong parameter closing ' + pos.skipcurrent())
4746 return None
4747 return self
4748 Trace.error('Wrong character in parameter template: ' + pos.skipcurrent())
4749 return None
4751 def read(self, pos, function):
4752 "Read the parameter itself using the definition."
4753 if self.literal:
4754 if self.optional:
4755 self.literalvalue = function.parsesquareliteral(pos)
4756 else:
4757 self.literalvalue = function.parseliteral(pos)
4758 if self.literalvalue:
4759 self.value = FormulaConstant(self.literalvalue)
4760 elif self.optional:
4761 self.value = function.parsesquare(pos)
4762 else:
4763 self.value = function.parseparameter(pos)
4765 def __unicode__(self):
4766 "Return a printable representation."
4767 result = 'param ' + self.name
4768 if self.value:
4769 result += ': ' + unicode(self.value)
4770 else:
4771 result += ' (empty)'
4772 return result
4774 class ParameterFunction(CommandBit):
4775 "A function with a variable number of parameters defined in a template."
4776 "The parameters are defined as a parameter definition."
4778 def readparams(self, readtemplate, pos):
4779 "Read the params according to the template."
4780 self.params = dict()
4781 for paramdef in self.paramdefs(readtemplate):
4782 paramdef.read(pos, self)
4783 self.params['$' + paramdef.name] = paramdef
4785 def paramdefs(self, readtemplate):
4786 "Read each param definition in the template"
4787 pos = TextPosition(readtemplate)
4788 while not pos.finished():
4789 paramdef = ParameterDefinition().parse(pos)
4790 if paramdef:
4791 yield paramdef
4793 def getparam(self, name):
4794 "Get a parameter as parsed."
4795 if not name in self.params:
4796 return None
4797 return self.params[name]
4799 def getvalue(self, name):
4800 "Get the value of a parameter."
4801 return self.getparam(name).value
4803 def getliteralvalue(self, name):
4804 "Get the literal value of a parameter."
4805 param = self.getparam(name)
4806 if not param or not param.literalvalue:
4807 return None
4808 return param.literalvalue
4810 class HybridFunction(ParameterFunction):
4812 A parameter function where the output is also defined using a template.
4813 The template can use a number of functions; each function has an associated
4814 tag.
4815 Example: [f0{$1},span class="fbox"] defines a function f0 which corresponds
4816 to a span of class fbox, yielding <span class="fbox">$1</span>.
4817 Literal parameters can be used in tags definitions:
4818 [f0{$1},span style="color: $p;"]
4819 yields <span style="color: $p;">$1</span>, where $p is a literal parameter.
4820 Sizes can be specified in hybridsizes, e.g. adding parameter sizes. By
4821 default the resulting size is the max of all arguments. Sizes are used
4822 to generate the right parameters.
4823 A function followed by a single / is output as a self-closing XHTML tag:
4824 [f0/,hr]
4825 will generate <hr/>.
4828 commandmap = FormulaConfig.hybridfunctions
4830 def parsebit(self, pos):
4831 "Parse a function with [] and {} parameters"
4832 readtemplate = self.translated[0]
4833 writetemplate = self.translated[1]
4834 self.readparams(readtemplate, pos)
4835 self.contents = self.writeparams(writetemplate)
4836 self.computehybridsize()
4838 def writeparams(self, writetemplate):
4839 "Write all params according to the template"
4840 return self.writepos(TextPosition(writetemplate))
4842 def writepos(self, pos):
4843 "Write all params as read in the parse position."
4844 result = []
4845 while not pos.finished():
4846 if pos.checkskip('$'):
4847 param = self.writeparam(pos)
4848 if param:
4849 result.append(param)
4850 elif pos.checkskip('f'):
4851 function = self.writefunction(pos)
4852 if function:
4853 function.type = None
4854 result.append(function)
4855 elif pos.checkskip('('):
4856 result.append(self.writebracket('left', '('))
4857 elif pos.checkskip(')'):
4858 result.append(self.writebracket('right', ')'))
4859 else:
4860 result.append(FormulaConstant(pos.skipcurrent()))
4861 return result
4863 def writeparam(self, pos):
4864 "Write a single param of the form $0, $x..."
4865 name = '$' + pos.skipcurrent()
4866 if not name in self.params:
4867 Trace.error('Unknown parameter ' + name)
4868 return None
4869 if not self.params[name]:
4870 return None
4871 if pos.checkskip('.'):
4872 self.params[name].value.type = pos.globalpha()
4873 return self.params[name].value
4875 def writefunction(self, pos):
4876 "Write a single function f0,...,fn."
4877 tag = self.readtag(pos)
4878 if not tag:
4879 return None
4880 if pos.checkskip('/'):
4881 # self-closing XHTML tag, such as <hr/>
4882 return TaggedBit().selfcomplete(tag)
4883 if not pos.checkskip('{'):
4884 Trace.error('Function should be defined in {}')
4885 return None
4886 pos.pushending('}')
4887 contents = self.writepos(pos)
4888 pos.popending()
4889 if len(contents) == 0:
4890 return None
4891 return TaggedBit().complete(contents, tag)
4893 def readtag(self, pos):
4894 "Get the tag corresponding to the given index. Does parameter substitution."
4895 if not pos.current().isdigit():
4896 Trace.error('Function should be f0,...,f9: f' + pos.current())
4897 return None
4898 index = int(pos.skipcurrent())
4899 if 2 + index > len(self.translated):
4900 Trace.error('Function f' + unicode(index) + ' is not defined')
4901 return None
4902 tag = self.translated[2 + index]
4903 if not '$' in tag:
4904 return tag
4905 for variable in self.params:
4906 if variable in tag:
4907 param = self.params[variable]
4908 if not param.literal:
4909 Trace.error('Parameters in tag ' + tag + ' should be literal: {' + variable + '!}')
4910 continue
4911 if param.literalvalue:
4912 value = param.literalvalue
4913 else:
4914 value = ''
4915 tag = tag.replace(variable, value)
4916 return tag
4918 def writebracket(self, direction, character):
4919 "Return a new bracket looking at the given direction."
4920 return self.factory.create(BracketCommand).create(direction, character)
4922 def computehybridsize(self):
4923 "Compute the size of the hybrid function."
4924 if not self.command in HybridSize.configsizes:
4925 self.computesize()
4926 return
4927 self.size = HybridSize().getsize(self)
4928 # set the size in all elements at first level
4929 for element in self.contents:
4930 element.size = self.size
4932 class HybridSize(object):
4933 "The size associated with a hybrid function."
4935 configsizes = FormulaConfig.hybridsizes
4937 def getsize(self, function):
4938 "Read the size for a function and parse it."
4939 sizestring = self.configsizes[function.command]
4940 for name in function.params:
4941 if name in sizestring:
4942 size = function.params[name].value.computesize()
4943 sizestring = sizestring.replace(name, unicode(size))
4944 if '$' in sizestring:
4945 Trace.error('Unconverted variable in hybrid size: ' + sizestring)
4946 return 1
4947 return eval(sizestring)
4950 FormulaCommand.types += [HybridFunction]
4960 class HeaderParser(Parser):
4961 "Parses the LyX header"
4963 def parse(self, reader):
4964 "Parse header parameters into a dictionary, return the preamble."
4965 contents = []
4966 self.parseending(reader, lambda: self.parseline(reader, contents))
4967 # skip last line
4968 reader.nextline()
4969 return contents
4971 def parseline(self, reader, contents):
4972 "Parse a single line as a parameter or as a start"
4973 line = reader.currentline()
4974 if line.startswith(HeaderConfig.parameters['branch']):
4975 self.parsebranch(reader)
4976 return
4977 elif line.startswith(HeaderConfig.parameters['lstset']):
4978 LstParser().parselstset(reader)
4979 return
4980 elif line.startswith(HeaderConfig.parameters['beginpreamble']):
4981 contents.append(self.factory.createcontainer(reader))
4982 return
4983 # no match
4984 self.parseparameter(reader)
4986 def parsebranch(self, reader):
4987 "Parse all branch definitions."
4988 branch = reader.currentline().split()[1]
4989 reader.nextline()
4990 subparser = HeaderParser().complete(HeaderConfig.parameters['endbranch'])
4991 subparser.parse(reader)
4992 options = BranchOptions(branch)
4993 for key in subparser.parameters:
4994 options.set(key, subparser.parameters[key])
4995 Options.branches[branch] = options
4997 def complete(self, ending):
4998 "Complete the parser with the given ending."
4999 self.ending = ending
5000 return self
5002 class PreambleParser(Parser):
5003 "A parser for the LyX preamble."
5005 preamble = []
5007 def parse(self, reader):
5008 "Parse the full preamble with all statements."
5009 self.ending = HeaderConfig.parameters['endpreamble']
5010 self.parseending(reader, lambda: self.parsepreambleline(reader))
5011 return []
5013 def parsepreambleline(self, reader):
5014 "Parse a single preamble line."
5015 PreambleParser.preamble.append(reader.currentline())
5016 reader.nextline()
5018 class LstParser(object):
5019 "Parse global and local lstparams."
5021 globalparams = dict()
5023 def parselstset(self, reader):
5024 "Parse a declaration of lstparams in lstset."
5025 paramtext = self.extractlstset(reader)
5026 if not '{' in paramtext:
5027 Trace.error('Missing opening bracket in lstset: ' + paramtext)
5028 return
5029 lefttext = paramtext.split('{')[1]
5030 croppedtext = lefttext[:-1]
5031 LstParser.globalparams = self.parselstparams(croppedtext)
5033 def extractlstset(self, reader):
5034 "Extract the global lstset parameters."
5035 paramtext = ''
5036 while not reader.finished():
5037 paramtext += reader.currentline()
5038 reader.nextline()
5039 if paramtext.endswith('}'):
5040 return paramtext
5041 Trace.error('Could not find end of \\lstset settings; aborting')
5043 def parsecontainer(self, container):
5044 "Parse some lstparams from elyxer.a container."
5045 container.lstparams = LstParser.globalparams.copy()
5046 paramlist = container.getparameterlist('lstparams')
5047 container.lstparams.update(self.parselstparams(paramlist))
5049 def parselstparams(self, paramlist):
5050 "Process a number of lstparams from elyxer.a list."
5051 paramdict = dict()
5052 for param in paramlist:
5053 if not '=' in param:
5054 if len(param.strip()) > 0:
5055 Trace.error('Invalid listing parameter ' + param)
5056 else:
5057 key, value = param.split('=', 1)
5058 paramdict[key] = value
5059 return paramdict
5064 class MacroDefinition(CommandBit):
5065 "A function that defines a new command (a macro)."
5067 macros = dict()
5069 def parsebit(self, pos):
5070 "Parse the function that defines the macro."
5071 self.output = EmptyOutput()
5072 self.parameternumber = 0
5073 self.defaults = []
5074 self.factory.defining = True
5075 self.parseparameters(pos)
5076 self.factory.defining = False
5077 Trace.debug('New command ' + self.newcommand + ' (' + \
5078 unicode(self.parameternumber) + ' parameters)')
5079 self.macros[self.newcommand] = self
5081 def parseparameters(self, pos):
5082 "Parse all optional parameters (number of parameters, default values)"
5083 "and the mandatory definition."
5084 self.newcommand = self.parsenewcommand(pos)
5085 # parse number of parameters
5086 literal = self.parsesquareliteral(pos)
5087 if literal:
5088 self.parameternumber = int(literal)
5089 # parse all default values
5090 bracket = self.parsesquare(pos)
5091 while bracket:
5092 self.defaults.append(bracket)
5093 bracket = self.parsesquare(pos)
5094 # parse mandatory definition
5095 self.definition = self.parseparameter(pos)
5097 def parsenewcommand(self, pos):
5098 "Parse the name of the new command."
5099 self.factory.clearskipped(pos)
5100 if self.factory.detecttype(Bracket, pos):
5101 return self.parseliteral(pos)
5102 if self.factory.detecttype(FormulaCommand, pos):
5103 return self.factory.create(FormulaCommand).extractcommand(pos)
5104 Trace.error('Unknown formula bit in defining function at ' + pos.identifier())
5105 return 'unknown'
5107 def instantiate(self):
5108 "Return an instance of the macro."
5109 return self.definition.clone()
5111 class MacroParameter(FormulaBit):
5112 "A parameter from elyxer.a macro."
5114 def detect(self, pos):
5115 "Find a macro parameter: #n."
5116 return pos.checkfor('#')
5118 def parsebit(self, pos):
5119 "Parse the parameter: #n."
5120 if not pos.checkskip('#'):
5121 Trace.error('Missing parameter start #.')
5122 return
5123 self.number = int(pos.skipcurrent())
5124 self.original = '#' + unicode(self.number)
5125 self.contents = [TaggedBit().constant('#' + unicode(self.number), 'span class="unknown"')]
5127 class MacroFunction(CommandBit):
5128 "A function that was defined using a macro."
5130 commandmap = MacroDefinition.macros
5132 def parsebit(self, pos):
5133 "Parse a number of input parameters."
5134 self.output = FilteredOutput()
5135 self.values = []
5136 macro = self.translated
5137 self.parseparameters(pos, macro)
5138 self.completemacro(macro)
5140 def parseparameters(self, pos, macro):
5141 "Parse as many parameters as are needed."
5142 self.parseoptional(pos, list(macro.defaults))
5143 self.parsemandatory(pos, macro.parameternumber - len(macro.defaults))
5144 if len(self.values) < macro.parameternumber:
5145 Trace.error('Missing parameters in macro ' + unicode(self))
5147 def parseoptional(self, pos, defaults):
5148 "Parse optional parameters."
5149 optional = []
5150 while self.factory.detecttype(SquareBracket, pos):
5151 optional.append(self.parsesquare(pos))
5152 if len(optional) > len(defaults):
5153 break
5154 for value in optional:
5155 default = defaults.pop()
5156 if len(value.contents) > 0:
5157 self.values.append(value)
5158 else:
5159 self.values.append(default)
5160 self.values += defaults
5162 def parsemandatory(self, pos, number):
5163 "Parse a number of mandatory parameters."
5164 for index in range(number):
5165 parameter = self.parsemacroparameter(pos, number - index)
5166 if not parameter:
5167 return
5168 self.values.append(parameter)
5170 def parsemacroparameter(self, pos, remaining):
5171 "Parse a macro parameter. Could be a bracket or a single letter."
5172 "If there are just two values remaining and there is a running number,"
5173 "parse as two separater numbers."
5174 self.factory.clearskipped(pos)
5175 if pos.finished():
5176 return None
5177 if self.factory.detecttype(FormulaNumber, pos):
5178 return self.parsenumbers(pos, remaining)
5179 return self.parseparameter(pos)
5181 def parsenumbers(self, pos, remaining):
5182 "Parse the remaining parameters as a running number."
5183 "For example, 12 would be {1}{2}."
5184 number = self.factory.parsetype(FormulaNumber, pos)
5185 if not len(number.original) == remaining:
5186 return number
5187 for digit in number.original:
5188 value = self.factory.create(FormulaNumber)
5189 value.add(FormulaConstant(digit))
5190 value.type = number
5191 self.values.append(value)
5192 return None
5194 def completemacro(self, macro):
5195 "Complete the macro with the parameters read."
5196 self.contents = [macro.instantiate()]
5197 replaced = [False] * len(self.values)
5198 for parameter in self.searchall(MacroParameter):
5199 index = parameter.number - 1
5200 if index >= len(self.values):
5201 Trace.error('Macro parameter index out of bounds: ' + unicode(index))
5202 return
5203 replaced[index] = True
5204 parameter.contents = [self.values[index].clone()]
5205 for index in range(len(self.values)):
5206 if not replaced[index]:
5207 self.addfilter(index, self.values[index])
5209 def addfilter(self, index, value):
5210 "Add a filter for the given parameter number and parameter value."
5211 original = '#' + unicode(index + 1)
5212 value = ''.join(self.values[0].gethtml())
5213 self.output.addfilter(original, value)
5215 class FormulaMacro(Formula):
5216 "A math macro defined in an inset."
5218 def __init__(self):
5219 self.parser = MacroParser()
5220 self.output = EmptyOutput()
5222 def __unicode__(self):
5223 "Return a printable representation."
5224 return 'Math macro'
5226 FormulaFactory.types += [ MacroParameter ]
5228 FormulaCommand.types += [
5229 MacroFunction,
5234 def math2html(formula):
5235 "Convert some TeX math to HTML."
5236 factory = FormulaFactory()
5237 whole = factory.parseformula(formula)
5238 FormulaProcessor().process(whole)
5239 whole.process()
5240 return ''.join(whole.gethtml())
5242 def main():
5243 "Main function, called if invoked from elyxer.the command line"
5244 args = sys.argv
5245 Options().parseoptions(args)
5246 if len(args) != 1:
5247 Trace.error('Usage: math2html.py escaped_string')
5248 exit()
5249 result = math2html(args[0])
5250 Trace.message(result)
5252 if __name__ == '__main__':
5253 main()