Math update, fixes and latex2html converter.
[docutils.git] / docutils / math / latex2html.py
blob21a7d64e9f97a1301f6809c7dffd3a1cb1485b3c
1 #! /usr/bin/env python
2 # -*- coding: utf-8 -*-
4 # math2html: convert LaTeX equations to HTML output.
6 # Copyright (C) 2009,2010 Alex Fernández
7 # Released without warranties or conditions of any kind
8 # under the terms of the Apache License, Version 2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Based on eLyXer: convert LyX source files to HTML output.
12 # http://elyxer.nongnu.org/
14 # --end--
15 # Alex 20101110
16 # eLyXer standalone formula conversion to HTML.
21 import sys
23 class Trace(object):
24 "A tracing class"
26 debugmode = False
27 quietmode = False
28 showlinesmode = False
30 prefix = None
32 def debug(cls, message):
33 "Show a debug message"
34 if not Trace.debugmode or Trace.quietmode:
35 return
36 Trace.show(message, sys.stdout)
38 def message(cls, message):
39 "Show a trace message"
40 if Trace.quietmode:
41 return
42 if Trace.prefix and Trace.showlinesmode:
43 message = Trace.prefix + message
44 Trace.show(message, sys.stdout)
46 def error(cls, message):
47 "Show an error message"
48 message = '* ' + message
49 if Trace.prefix and Trace.showlinesmode:
50 message = Trace.prefix + message
51 Trace.show(message, sys.stderr)
53 def fatal(cls, message):
54 "Show an error message and terminate"
55 Trace.error('FATAL: ' + message)
56 exit(-1)
58 def show(cls, message, channel):
59 "Show a message out of a channel"
60 message = message.encode('utf-8')
61 channel.write(message + '\n')
63 debug = classmethod(debug)
64 message = classmethod(message)
65 error = classmethod(error)
66 fatal = classmethod(fatal)
67 show = classmethod(show)
80 class Cloner(object):
81 "An object used to clone other objects."
83 def clone(cls, original):
84 "Return an exact copy of an object."
85 "The original object must have an empty constructor."
86 return cls.create(original.__class__)
88 def create(cls, type):
89 "Create an object of a given class."
90 clone = type.__new__(type)
91 clone.__init__()
92 return clone
94 clone = classmethod(clone)
95 create = classmethod(create)
97 class ContainerExtractor(object):
98 "A class to extract certain containers."
100 def __init__(self, config):
101 "The config parameter is a map containing three lists: allowed, copied and extracted."
102 "Each of the three is a list of class names for containers."
103 "Allowed containers are included as is into the result."
104 "Cloned containers are cloned and placed into the result."
105 "Extracted containers are looked into."
106 "All other containers are silently ignored."
107 self.allowed = config['allowed']
108 self.cloned = config['cloned']
109 self.extracted = config['extracted']
111 def extract(self, container):
112 "Extract a group of selected containers from a container."
113 list = []
114 locate = lambda c: c.__class__.__name__ in self.allowed + self.cloned
115 recursive = lambda c: c.__class__.__name__ in self.extracted
116 process = lambda c: self.process(c, list)
117 container.recursivesearch(locate, recursive, process)
118 return list
120 def process(self, container, list):
121 "Add allowed containers, clone cloned containers and add the clone."
122 name = container.__class__.__name__
123 if name in self.allowed:
124 list.append(container)
125 elif name in self.cloned:
126 list.append(self.safeclone(container))
127 else:
128 Trace.error('Unknown container class ' + name)
130 def safeclone(self, container):
131 "Return a new container with contents only in a safe list, recursively."
132 clone = Cloner.clone(container)
133 clone.output = container.output
134 clone.contents = self.extract(container)
135 return clone
143 import os.path
144 import sys
147 class BibStylesConfig(object):
148 "Configuration class from config file"
150 abbrvnat = {
152 u'@article':u'$authors. $title. <i>$journal</i>,{ {$volume:}$pages,} $month $year.{ doi: $doi.}{ URL <a href="$url">$url</a>.}{ $note.}',
153 u'cite':u'$surname($year)',
154 u'default':u'$authors. <i>$title</i>. $publisher, $year.{ URL <a href="$url">$url</a>.}{ $note.}',
157 alpha = {
159 u'@article':u'$authors. $title.{ <i>$journal</i>{, {$volume}{($number)}}{: $pages}{, $year}.}{ <a href="$url">$url</a>.}{ <a href="$filename">$filename</a>.}{ $note.}',
160 u'cite':u'$Sur$YY',
161 u'default':u'$authors. $title.{ <i>$journal</i>,} $year.{ <a href="$url">$url</a>.}{ <a href="$filename">$filename</a>.}{ $note.}',
164 authordate2 = {
166 u'@article':u'$authors. $year. $title. <i>$journal</i>, <b>$volume</b>($number), $pages.{ URL <a href="$url">$url</a>.}{ $note.}',
167 u'@book':u'$authors. $year. <i>$title</i>. $publisher.{ URL <a href="$url">$url</a>.}{ $note.}',
168 u'cite':u'$surname, $year',
169 u'default':u'$authors. $year. <i>$title</i>. $publisher.{ URL <a href="$url">$url</a>.}{ $note.}',
172 default = {
174 u'@article':u'$authors: “$title”, <i>$journal</i>,{ pp. $pages,} $year.{ URL <a href="$url">$url</a>.}{ $note.}',
175 u'@book':u'{$authors: }<i>$title</i>{ ($editor, ed.)}.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
176 u'@booklet':u'$authors: <i>$title</i>.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
177 u'@conference':u'$authors: “$title”, <i>$journal</i>,{ pp. $pages,} $year.{ URL <a href="$url">$url</a>.}{ $note.}',
178 u'@inbook':u'$authors: <i>$title</i>.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
179 u'@incollection':u'$authors: <i>$title</i>{ in <i>$booktitle</i>{ ($editor, ed.)}}.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
180 u'@inproceedings':u'$authors: “$title”, <i>$journal</i>,{ pp. $pages,} $year.{ URL <a href="$url">$url</a>.}{ $note.}',
181 u'@manual':u'$authors: <i>$title</i>.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
182 u'@mastersthesis':u'$authors: <i>$title</i>.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
183 u'@misc':u'$authors: <i>$title</i>.{{ $publisher,}{ $howpublished,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
184 u'@phdthesis':u'$authors: <i>$title</i>.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
185 u'@proceedings':u'$authors: “$title”, <i>$journal</i>,{ pp. $pages,} $year.{ URL <a href="$url">$url</a>.}{ $note.}',
186 u'@techreport':u'$authors: <i>$title</i>, $year.{ URL <a href="$url">$url</a>.}{ $note.}',
187 u'@unpublished':u'$authors: “$title”, <i>$journal</i>, $year.{ URL <a href="$url">$url</a>.}{ $note.}',
188 u'cite':u'$index',
189 u'default':u'$authors: <i>$title</i>.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
192 defaulttags = {
193 u'YY':u'??', u'authors':u'', u'surname':u'',
196 ieeetr = {
198 u'@article':u'$authors, “$title”, <i>$journal</i>, vol. $volume, no. $number, pp. $pages, $year.{ URL <a href="$url">$url</a>.}{ $note.}',
199 u'@book':u'$authors, <i>$title</i>. $publisher, $year.{ URL <a href="$url">$url</a>.}{ $note.}',
200 u'cite':u'$index',
201 u'default':u'$authors, “$title”. $year.{ URL <a href="$url">$url</a>.}{ $note.}',
204 plain = {
206 u'@article':u'$authors. $title.{ <i>$journal</i>{, {$volume}{($number)}}{:$pages}{, $year}.}{ URL <a href="$url">$url</a>.}{ $note.}',
207 u'@book':u'$authors. <i>$title</i>. $publisher,{ $month} $year.{ URL <a href="$url">$url</a>.}{ $note.}',
208 u'@incollection':u'$authors. $title.{ In <i>$booktitle</i> {($editor, ed.)}.} $publisher,{ $month} $year.{ URL <a href="$url">$url</a>.}{ $note.}',
209 u'@inproceedings':u'$authors. $title. { <i>$booktitle</i>{, {$volume}{($number)}}{:$pages}{, $year}.}{ URL <a href="$url">$url</a>.}{ $note.}',
210 u'cite':u'$index',
211 u'default':u'{$authors. }$title.{{ $publisher,} $year.}{ URL <a href="$url">$url</a>.}{ $note.}',
214 vancouver = {
216 u'@article':u'$authors. $title. <i>$journal</i>, $year{;{<b>$volume</b>}{($number)}{:$pages}}.{ URL: <a href="$url">$url</a>.}{ $note.}',
217 u'@book':u'$authors. $title. {$publisher, }$year.{ URL: <a href="$url">$url</a>.}{ $note.}',
218 u'cite':u'$index',
219 u'default':u'$authors. $title; {$publisher, }$year.{ $howpublished.}{ URL: <a href="$url">$url</a>.}{ $note.}',
222 class BibTeXConfig(object):
223 "Configuration class from config file"
225 escaped = {
226 u'\\"A':u'Ä', u'\\"E':u'Ë', u'\\"I':u'Ï', u'\\"O':u'Ö', u'\\"U':u'Ü',
227 u'\\"\\i':u'ï', u'\\"a':u'ä', u'\\"e':u'ë', u'\\"i':u'ï', u'\\"o':u'ö',
228 u'\\"u':u'ü', u'\\"y':u'ÿ', u'\\"{A}':u'Ä', u'\\"{E}':u'Ë',
229 u'\\"{I}':u'Ï', u'\\"{O}':u'Ö', u'\\"{U}':u'Ü', u'\\"{\\i}':u'ï',
230 u'\\#':u'#', u'\\$':u'$', u'\\%':u'%', u'\\&':u'&', u'\\\'A':u'Á',
231 u'\\\'E':u'É', u'\\\'I':u'Í', u'\\\'O':u'Ó', u'\\\'U':u'Ú',
232 u'\\\'Y':u'Ý', u'\\\'\\i':u'í', u'\\\'a':u'á', u'\\\'e':u'é',
233 u'\\\'i':u'í', u'\\\'o':u'ó', u'\\\'u':u'ú', u'\\\'y':u'ý',
234 u'\\\'{A}':u'Á', u'\\\'{E}':u'É', u'\\\'{I}':u'Í', u'\\\'{O}':u'Ó',
235 u'\\\'{U}':u'Ú', u'\\\'{Y}':u'Ý', u'\\\'{\\i}':u'í', u'\\\'{c}':u'ć',
236 u'\\,':u' ', u'\\;':u' ', u'\\AA':u'Å', u'\\AE':u'Æ', u'\\DH':u'Ð',
237 u'\\O':u'Ø', u'\\TH':u'Þ', u'\\^A':u'Â', u'\\^E':u'Ê', u'\\^I':u'Î',
238 u'\\^O':u'Ô', u'\\^U':u'Û', u'\\^\\i':u'î', u'\\^a':u'â', u'\\^e':u'ê',
239 u'\\^i':u'î', u'\\^o':u'ô', u'\\^u':u'û', u'\\^{A}':u'Â', u'\\^{E}':u'Ê',
240 u'\\^{I}':u'Î', u'\\^{O}':u'Ô', u'\\^{U}':u'Û', u'\\^{\\i}':u'î',
241 u'\\`A':u'À', u'\\`E':u'È', u'\\`I':u'Ì', u'\\`O':u'Ò', u'\\`U':u'Ù',
242 u'\\`\\i':u'ì', u'\\`a':u'à', u'\\`e':u'è', u'\\`i':u'ì', u'\\`o':u'ò',
243 u'\\`u':u'ù', u'\\`{A}':u'À', u'\\`{E}':u'È', u'\\`{I}':u'Ì',
244 u'\\`{O}':u'Ò', u'\\`{U}':u'Ù', u'\\`{\\i}':u'ì', u'\\aa':u'å',
245 u'\\ae':u'æ', u'\\c C':u'Ç', u'\\c c':u'ç', u'\\c {C}':u'Ç',
246 u'\\copyright':u'©', u'\\c{C}':u'Ç', u'\\c{c}':u'ç', u'\\c{{C}}':u'Ç',
247 u'\\dh':u'ð', u'\\emph':u'', u'\\o':u'ø', u'\\r A':u'Å', u'\\r a':u'å',
248 u'\\r {A}':u'Å', u'\\r{A}':u'Å', u'\\r{a}':u'å', u'\\r{{A}}':u'Å',
249 u'\\ss':u'ß', u'\\textordfeminine':u'ª', u'\\textordmasculine':u'º',
250 u'\\textregistered':u'®', u'\\texttrademark':u'™', u'\\th':u'þ',
251 u'\\tt':u'', u'\\~A':u'Ã', u'\\~N':u'Ñ', u'\\~O':u'Õ', u'\\~a':u'ã',
252 u'\\~n':u'ñ', u'\\~o':u'õ', u'\\~{A}':u'Ã', u'\\~{N}':u'Ñ',
253 u'\\~{O}':u'Õ',
256 replaced = {
257 u'--':u'—', u'..':u'.',
260 class ContainerConfig(object):
261 "Configuration class from config file"
263 endings = {
264 u'Align':u'\\end_layout', u'BarredText':u'\\bar',
265 u'BoldText':u'\\series', u'Cell':u'</cell',
266 u'ChangeDeleted':u'\\change_unchanged',
267 u'ChangeInserted':u'\\change_unchanged', u'ColorText':u'\\color',
268 u'EmphaticText':u'\\emph', u'Hfill':u'\\hfill', u'Inset':u'\\end_inset',
269 u'Layout':u'\\end_layout', u'LyXFooter':u'\\end_document',
270 u'LyXHeader':u'\\end_header', u'Row':u'</row', u'ShapedText':u'\\shape',
271 u'SizeText':u'\\size', u'TextFamily':u'\\family',
272 u'VersalitasText':u'\\noun',
275 extracttext = {
276 u'allowed':[u'StringContainer',u'Constant',u'FormulaConstant',],
277 u'cloned':[u'',],
278 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',],
281 startendings = {
282 u'\\begin_deeper':u'\\end_deeper', u'\\begin_inset':u'\\end_inset',
283 u'\\begin_layout':u'\\end_layout',
286 starts = {
287 u'':u'StringContainer', u'#LyX':u'BlackBox', u'</lyxtabular':u'BlackBox',
288 u'<cell':u'Cell', u'<column':u'Column', u'<row':u'Row',
289 u'\\align':u'Align', u'\\bar':u'BarredText',
290 u'\\bar default':u'BlackBox', u'\\bar no':u'BlackBox',
291 u'\\begin_body':u'BlackBox', u'\\begin_deeper':u'DeeperList',
292 u'\\begin_document':u'BlackBox', u'\\begin_header':u'LyXHeader',
293 u'\\begin_inset Box':u'BoxInset', u'\\begin_inset Branch':u'Branch',
294 u'\\begin_inset Caption':u'Caption',
295 u'\\begin_inset CommandInset bibitem':u'BiblioEntry',
296 u'\\begin_inset CommandInset bibtex':u'BibTeX',
297 u'\\begin_inset CommandInset citation':u'BiblioCitation',
298 u'\\begin_inset CommandInset href':u'URL',
299 u'\\begin_inset CommandInset include':u'IncludeInset',
300 u'\\begin_inset CommandInset index_print':u'PrintIndex',
301 u'\\begin_inset CommandInset label':u'Label',
302 u'\\begin_inset CommandInset nomencl_print':u'PrintNomenclature',
303 u'\\begin_inset CommandInset nomenclature':u'NomenclatureEntry',
304 u'\\begin_inset CommandInset ref':u'Reference',
305 u'\\begin_inset CommandInset toc':u'TableOfContents',
306 u'\\begin_inset ERT':u'ERT', u'\\begin_inset Flex':u'FlexInset',
307 u'\\begin_inset Flex Chunkref':u'NewfangledChunkRef',
308 u'\\begin_inset Flex Marginnote':u'SideNote',
309 u'\\begin_inset Flex Sidenote':u'SideNote',
310 u'\\begin_inset Flex URL':u'FlexURL', u'\\begin_inset Float':u'Float',
311 u'\\begin_inset FloatList':u'ListOf', u'\\begin_inset Foot':u'Footnote',
312 u'\\begin_inset Formula':u'Formula',
313 u'\\begin_inset FormulaMacro':u'FormulaMacro',
314 u'\\begin_inset Graphics':u'Image',
315 u'\\begin_inset Index':u'IndexReference',
316 u'\\begin_inset Info':u'InfoInset',
317 u'\\begin_inset LatexCommand bibitem':u'BiblioEntry',
318 u'\\begin_inset LatexCommand bibtex':u'BibTeX',
319 u'\\begin_inset LatexCommand cite':u'BiblioCitation',
320 u'\\begin_inset LatexCommand citealt':u'BiblioCitation',
321 u'\\begin_inset LatexCommand citep':u'BiblioCitation',
322 u'\\begin_inset LatexCommand citet':u'BiblioCitation',
323 u'\\begin_inset LatexCommand htmlurl':u'URL',
324 u'\\begin_inset LatexCommand index':u'IndexReference',
325 u'\\begin_inset LatexCommand label':u'Label',
326 u'\\begin_inset LatexCommand nomenclature':u'NomenclatureEntry',
327 u'\\begin_inset LatexCommand prettyref':u'Reference',
328 u'\\begin_inset LatexCommand printindex':u'PrintIndex',
329 u'\\begin_inset LatexCommand printnomenclature':u'PrintNomenclature',
330 u'\\begin_inset LatexCommand ref':u'Reference',
331 u'\\begin_inset LatexCommand tableofcontents':u'TableOfContents',
332 u'\\begin_inset LatexCommand url':u'URL',
333 u'\\begin_inset LatexCommand vref':u'Reference',
334 u'\\begin_inset Marginal':u'SideNote',
335 u'\\begin_inset Newline':u'NewlineInset',
336 u'\\begin_inset Newpage':u'NewPageInset', u'\\begin_inset Note':u'Note',
337 u'\\begin_inset OptArg':u'ShortTitle',
338 u'\\begin_inset Quotes':u'QuoteContainer',
339 u'\\begin_inset Tabular':u'Table', u'\\begin_inset Text':u'InsetText',
340 u'\\begin_inset VSpace':u'VerticalSpace', u'\\begin_inset Wrap':u'Wrap',
341 u'\\begin_inset listings':u'Listing', u'\\begin_inset space':u'Space',
342 u'\\begin_layout':u'Layout', u'\\begin_layout Abstract':u'Abstract',
343 u'\\begin_layout Author':u'Author',
344 u'\\begin_layout Bibliography':u'Bibliography',
345 u'\\begin_layout Chunk':u'NewfangledChunk',
346 u'\\begin_layout Description':u'Description',
347 u'\\begin_layout Enumerate':u'ListItem',
348 u'\\begin_layout Itemize':u'ListItem', u'\\begin_layout List':u'List',
349 u'\\begin_layout LyX-Code':u'LyXCode',
350 u'\\begin_layout Plain':u'PlainLayout',
351 u'\\begin_layout Standard':u'StandardLayout',
352 u'\\begin_layout Title':u'Title', u'\\begin_preamble':u'LyXPreamble',
353 u'\\change_deleted':u'ChangeDeleted',
354 u'\\change_inserted':u'ChangeInserted',
355 u'\\change_unchanged':u'BlackBox', u'\\color':u'ColorText',
356 u'\\color inherit':u'BlackBox', u'\\color none':u'BlackBox',
357 u'\\emph default':u'BlackBox', u'\\emph off':u'BlackBox',
358 u'\\emph on':u'EmphaticText', u'\\emph toggle':u'EmphaticText',
359 u'\\end_body':u'LyXFooter', u'\\family':u'TextFamily',
360 u'\\family default':u'BlackBox', u'\\family roman':u'BlackBox',
361 u'\\hfill':u'Hfill', u'\\labelwidthstring':u'BlackBox',
362 u'\\lang':u'LangLine', u'\\length':u'InsetLength',
363 u'\\lyxformat':u'LyXFormat', u'\\lyxline':u'LyXLine',
364 u'\\newline':u'Newline', u'\\newpage':u'NewPage',
365 u'\\noindent':u'BlackBox', u'\\noun default':u'BlackBox',
366 u'\\noun off':u'BlackBox', u'\\noun on':u'VersalitasText',
367 u'\\paragraph_spacing':u'BlackBox', u'\\series bold':u'BoldText',
368 u'\\series default':u'BlackBox', u'\\series medium':u'BlackBox',
369 u'\\shape':u'ShapedText', u'\\shape default':u'BlackBox',
370 u'\\shape up':u'BlackBox', u'\\size':u'SizeText',
371 u'\\size normal':u'BlackBox', u'\\start_of_appendix':u'StartAppendix',
374 string = {
375 u'startcommand':u'\\',
378 table = {
379 u'headers':[u'<lyxtabular',u'<features',],
382 class EscapeConfig(object):
383 "Configuration class from config file"
385 chars = {
386 u'\n':u'', u' -- ':u' — ', u'\'':u'’', u'---':u'—', u'`':u'‘',
389 commands = {
390 u'\\InsetSpace \\space{}':u' ', u'\\InsetSpace \\thinspace{}':u' ',
391 u'\\InsetSpace ~':u' ', u'\\SpecialChar \\-':u'',
392 u'\\SpecialChar \\@.':u'.', u'\\SpecialChar \\ldots{}':u'…',
393 u'\\SpecialChar \\menuseparator':u' ▷ ',
394 u'\\SpecialChar \\nobreakdash-':u'-', u'\\SpecialChar \\slash{}':u'/',
395 u'\\SpecialChar \\textcompwordmark{}':u'', u'\\backslash':u'\\',
398 entities = {
399 u'&':u'&amp;', u'<':u'&lt;', u'>':u'&gt;',
402 html = {
403 u'/>':u'>',
406 iso885915 = {
407 u' ':u'&nbsp;', u' ':u'&emsp;', u' ':u'&#8197;',
410 nonunicode = {
411 u' ':u' ',
414 class FormulaConfig(object):
415 "Configuration class from config file"
417 alphacommands = {
418 u'\\AA':u'Å', u'\\AE':u'Æ', u'\\L':u'Ł', u'\\O':u'Ø', u'\\OE':u'Œ',
419 u'\\aa':u'å', u'\\ae':u'æ', u'\\alpha':u'α', u'\\beta':u'β',
420 u'\\delta':u'δ', u'\\epsilon':u'ϵ', u'\\eta':u'η', u'\\gamma':u'γ',
421 u'\\i':u'ı', u'\\iota':u'ι', u'\\j':u'ȷ', u'\\kappa':u'κ', u'\\l':u'ł',
422 u'\\lambda':u'λ', u'\\mu':u'μ', u'\\nu':u'ν', u'\\o':u'ø', u'\\oe':u'œ',
423 u'\\omega':u'ω', u'\\phi':u'φ', u'\\pi':u'π', u'\\psi':u'ψ',
424 u'\\rho':u'ρ', u'\\sigma':u'σ', u'\\ss':u'ß', u'\\tau':u'τ',
425 u'\\textcrh':u'ħ', u'\\theta':u'θ', u'\\upsilon':u'υ',
426 u'\\varDelta':u'∆', u'\\varGamma':u'Γ', u'\\varLambda':u'Λ',
427 u'\\varOmega':u'Ω', u'\\varPhi':u'Φ', u'\\varPi':u'Π', u'\\varPsi':u'Ψ',
428 u'\\varSigma':u'Σ', u'\\varTheta':u'Θ', u'\\varUpsilon':u'Υ',
429 u'\\varXi':u'Ξ', u'\\varepsilon':u'ε', u'\\varkappa':u'ϰ',
430 u'\\varphi':u'φ', u'\\varpi':u'ϖ', u'\\varrho':u'ϱ', u'\\varsigma':u'ς',
431 u'\\vartheta':u'ϑ', u'\\xi':u'ξ', u'\\zeta':u'ζ',
434 array = {
435 u'begin':u'\\begin', u'cellseparator':u'&', u'end':u'\\end',
436 u'rowseparator':u'\\\\',
439 combiningfunctions = {
440 u'\\acute':u'́', u'\\bar':u'̄', u'\\breve':u'̆', u'\\c':u'̧',
441 u'\\check':u'̌', u'\\dddot':u'⃛', u'\\ddot':u'̈', u'\\dot':u'̇',
442 u'\\grave':u'̀', u'\\hat':u'̂', u'\\mathring':u'̊',
443 u'\\overleftarrow':u'⃖', u'\\overrightarrow':u'⃗', u'\\r':u'̥',
444 u'\\s':u'̩', u'\\textsubring':u'̥', u'\\tilde':u'̃', u'\\vec':u'⃗',
447 commands = {
448 u'\\ ':u' ', u'\\!':u'', u'\\$':u'$', u'\\%':u'%', u'\\,':u' ',
449 u'\\:':u' ', u'\\;':u' ', u'\\APLdownarrowbox':u'⍗',
450 u'\\APLleftarrowbox':u'⍇', u'\\APLrightarrowbox':u'⍈',
451 u'\\APLuparrowbox':u'⍐', u'\\Box':u'□', u'\\Bumpeq':u'≎',
452 u'\\CIRCLE':u'●', u'\\Cap':u'⋒', u'\\CheckedBox':u'☑', u'\\Circle':u'○',
453 u'\\Coloneqq':u'⩴', u'\\Corresponds':u'≙', u'\\Cup':u'⋓',
454 u'\\Delta':u'Δ', u'\\Diamond':u'◇', u'\\Downarrow':u'⇓', u'\\EUR':u'€',
455 u'\\Gamma':u'Γ', u'\\Im':u'ℑ', u'\\Join':u'⨝', u'\\LEFTCIRCLE':u'◖',
456 u'\\LEFTcircle':u'◐', u'\\Lambda':u'Λ', u'\\Leftarrow':u'⇐',
457 u'\\Leftrightarrow':u' ⇔ ', u'\\Lleftarrow':u'⇚',
458 u'\\Longleftarrow':u'⟸', u'\\Longleftrightarrow':u'⟺',
459 u'\\Longrightarrow':u'⟹', u'\\Lsh':u'↰', u'\\Mapsfrom':u'⇐|',
460 u'\\Mapsto':u'|⇒', u'\\Omega':u'Ω', u'\\P':u'¶', u'\\Phi':u'Φ',
461 u'\\Pi':u'Π', u'\\Pr':u'Pr', u'\\Psi':u'Ψ', u'\\RIGHTCIRCLE':u'◗',
462 u'\\RIGHTcircle':u'◑', u'\\Re':u'ℜ', u'\\Rightarrow':u' ⇒ ',
463 u'\\Rrightarrow':u'⇛', u'\\Rsh':u'↱', u'\\S':u'§', u'\\Sigma':u'Σ',
464 u'\\Square':u'☐', u'\\Subset':u'⋐', u'\\Supset':u'⋑', u'\\Theta':u'Θ',
465 u'\\Uparrow':u'⇑', u'\\Updownarrow':u'⇕', u'\\Upsilon':u'Υ',
466 u'\\Vdash':u'⊩', u'\\Vert':u'∥', u'\\Vvdash':u'⊪', u'\\XBox':u'☒',
467 u'\\Xi':u'Ξ', u'\\Yup':u'⅄', u'\\\\':u'<br/>', u'\\_':u'_',
468 u'\\aleph':u'ℵ', u'\\amalg':u'∐', u'\\angle':u'∠', u'\\approx':u' ≈ ',
469 u'\\aquarius':u'♒', u'\\arccos':u'arccos', u'\\arcsin':u'arcsin',
470 u'\\arctan':u'arctan', u'\\arg':u'arg', u'\\aries':u'♈', u'\\ast':u'∗',
471 u'\\asymp':u'≍', u'\\backepsilon':u'∍', u'\\backprime':u'‵',
472 u'\\backsimeq':u'⋍', u'\\backslash':u'\\', u'\\barwedge':u'⊼',
473 u'\\because':u'∵', u'\\beth':u'ℶ', u'\\between':u'≬', u'\\bigcap':u'∩',
474 u'\\bigcirc':u'○', u'\\bigcup':u'∪', u'\\bigodot':u'⊙',
475 u'\\bigoplus':u'⊕', u'\\bigotimes':u'⊗', u'\\bigsqcup':u'⊔',
476 u'\\bigstar':u'★', u'\\bigtriangledown':u'▽', u'\\bigtriangleup':u'△',
477 u'\\biguplus':u'⊎', u'\\bigvee':u'∨', u'\\bigwedge':u'∧',
478 u'\\blacklozenge':u'⧫', u'\\blacksmiley':u'☻', u'\\blacksquare':u'■',
479 u'\\blacktriangle':u'▲', u'\\blacktriangledown':u'▼',
480 u'\\blacktriangleright':u'▶', u'\\bot':u'⊥', u'\\bowtie':u'⋈',
481 u'\\box':u'▫', u'\\boxdot':u'⊡', u'\\bullet':u'•', u'\\bumpeq':u'≏',
482 u'\\cancer':u'♋', u'\\cap':u'∩', u'\\capricornus':u'♑', u'\\cdot':u'⋅',
483 u'\\cdots':u'⋯', u'\\centerdot':u'∙', u'\\checkmark':u'✓', u'\\chi':u'χ',
484 u'\\circ':u'○', u'\\circeq':u'≗', u'\\circledR':u'®',
485 u'\\circledast':u'⊛', u'\\circledcirc':u'⊚', u'\\circleddash':u'⊝',
486 u'\\clubsuit':u'♣', u'\\coloneqq':u'≔', u'\\complement':u'∁',
487 u'\\cong':u'≅', u'\\coprod':u'∐', u'\\copyright':u'©', u'\\cos':u'cos',
488 u'\\cosh':u'cosh', u'\\cot':u'cot', u'\\coth':u'coth', u'\\csc':u'csc',
489 u'\\cup':u'∪', u'\\curvearrowleft':u'↶', u'\\curvearrowright':u'↷',
490 u'\\dag':u'†', u'\\dagger':u'†', u'\\daleth':u'ℸ',
491 u'\\dashleftarrow':u'⇠', u'\\dashrightarrow':u' ⇢ ', u'\\dashv':u'⊣',
492 u'\\ddag':u'‡', u'\\ddagger':u'‡', u'\\ddots':u'⋱', u'\\deg':u'deg',
493 u'\\det':u'det', u'\\diagdown':u'╲', u'\\diagup':u'╱', u'\\diamond':u'◇',
494 u'\\diamondsuit':u'♦', u'\\dim':u'dim', u'\\displaystyle':u'',
495 u'\\div':u'÷', u'\\divideontimes':u'⋇', u'\\dotdiv':u'∸',
496 u'\\doteq':u'≐', u'\\doteqdot':u'≑', u'\\dotplus':u'∔', u'\\dots':u'…',
497 u'\\doublebarwedge':u'⌆', u'\\downarrow':u'↓', u'\\downdownarrows':u'⇊',
498 u'\\downharpoonleft':u'⇃', u'\\downharpoonright':u'⇂', u'\\earth':u'♁',
499 u'\\ell':u'ℓ', u'\\emptyset':u'∅', u'\\eqcirc':u'≖', u'\\eqcolon':u'≕',
500 u'\\eqsim':u'≂', u'\\equiv':u' ≡ ', u'\\euro':u'€', u'\\exists':u'∃',
501 u'\\exp':u'exp', u'\\fallingdotseq':u'≒', u'\\female':u'♀',
502 u'\\flat':u'♭', u'\\forall':u'∀', u'\\frown':u'⌢', u'\\frownie':u'☹',
503 u'\\gcd':u'gcd', u'\\ge':u' ≥ ', u'\\gemini':u'♊', u'\\geq':u' ≥ ',
504 u'\\geq)':u'≥', u'\\geqq':u'≧', u'\\geqslant':u'≥', u'\\gets':u'←',
505 u'\\gg':u'≫', u'\\ggg':u'⋙', u'\\gimel':u'ℷ', u'\\gneqq':u'≩',
506 u'\\gnsim':u'⋧', u'\\gtrdot':u'⋗', u'\\gtreqless':u'⋚',
507 u'\\gtreqqless':u'⪌', u'\\gtrless':u'≷', u'\\gtrsim':u'≳',
508 u'\\hbar':u'ℏ', u'\\heartsuit':u'♥',
509 u'\\hfill':u'<span class="hfill"> </span>', u'\\hom':u'hom',
510 u'\\hookleftarrow':u'↩', u'\\hookrightarrow':u'↪', u'\\hslash':u'ℏ',
511 u'\\idotsint':u'<span class="bigsymbol">∫⋯∫</span>',
512 u'\\iiint':u'<span class="bigsymbol">∭</span>',
513 u'\\iint':u'<span class="bigsymbol">∬</span>', u'\\imath':u'ı',
514 u'\\implies':u'  ⇒  ', u'\\in':u' ∈ ', u'\\inf':u'inf', u'\\infty':u'∞',
515 u'\\int':u'<span class="bigsymbol">∫</span>',
516 u'\\intop':u'<span class="bigsymbol">∫</span>', u'\\invneg':u'⌐',
517 u'\\jmath':u'ȷ', u'\\jupiter':u'♃', u'\\ker':u'ker', u'\\land':u'∧',
518 u'\\landupint':u'<span class="bigsymbol">∱</span>', u'\\langle':u'⟨',
519 u'\\lbrace':u'{', u'\\lbrace)':u'{', u'\\lbrack':u'[', u'\\lceil':u'⌈',
520 u'\\ldots':u'…', u'\\le':u'≤', u'\\leadsto':u'⇝', u'\\leftarrow':u' ← ',
521 u'\\leftarrow)':u'←', u'\\leftarrowtail':u'↢', u'\\leftarrowtobar':u'⇤',
522 u'\\leftharpoondown':u'↽', u'\\leftharpoonup':u'↼',
523 u'\\leftleftarrows':u'⇇', u'\\leftleftharpoons':u'⥢', u'\\leftmoon':u'☾',
524 u'\\leftrightarrow':u'↔', u'\\leftrightarrows':u'⇆',
525 u'\\leftrightharpoons':u'⇋', u'\\leftthreetimes':u'⋋', u'\\leo':u'♌',
526 u'\\leq':u' ≤ ', u'\\leq)':u'≤', u'\\leqq':u'≦', u'\\leqslant':u'≤',
527 u'\\lessdot':u'⋖', u'\\lesseqgtr':u'⋛', u'\\lesseqqgtr':u'⪋',
528 u'\\lessgtr':u'≶', u'\\lesssim':u'≲', u'\\lfloor':u'⌊', u'\\lg':u'lg',
529 u'\\lhd':u'⊲', u'\\libra':u'♎', u'\\lightning':u'↯', u'\\lim':u'lim',
530 u'\\liminf':u'liminf', u'\\limsup':u'limsup', u'\\ll':u'≪',
531 u'\\lll':u'⋘', u'\\ln':u'ln', u'\\lneqq':u'≨', u'\\lnot':u'¬',
532 u'\\lnsim':u'⋦', u'\\log':u'log', u'\\longleftarrow':u'⟵',
533 u'\\longleftrightarrow':u'⟷', u'\\longmapsto':u'⟼',
534 u'\\longrightarrow':u'⟶', u'\\looparrowleft':u'↫',
535 u'\\looparrowright':u'↬', u'\\lor':u'∨', u'\\lozenge':u'◊',
536 u'\\ltimes':u'⋉', u'\\lyxlock':u'', u'\\male':u'♂', u'\\maltese':u'✠',
537 u'\\mapsfrom':u'↤', u'\\mapsto':u'↦', u'\\mathcircumflex':u'^',
538 u'\\max':u'max', u'\\measuredangle':u'∡', u'\\mercury':u'☿',
539 u'\\mho':u'℧', u'\\mid':u'∣', u'\\min':u'min', u'\\models':u'⊨',
540 u'\\mp':u'∓', u'\\multimap':u'⊸', u'\\nLeftarrow':u'⇍',
541 u'\\nLeftrightarrow':u'⇎', u'\\nRightarrow':u'⇏', u'\\nVDash':u'⊯',
542 u'\\nabla':u'∇', u'\\napprox':u'≉', u'\\natural':u'♮', u'\\ncong':u'≇',
543 u'\\ne':u' ≠ ', u'\\nearrow':u'↗', u'\\neg':u'¬', u'\\neg)':u'¬',
544 u'\\neptune':u'♆', u'\\neq':u' ≠ ', u'\\nequiv':u'≢', u'\\nexists':u'∄',
545 u'\\ngeqslant':u'≱', u'\\ngtr':u'≯', u'\\ngtrless':u'≹', u'\\ni':u'∋',
546 u'\\ni)':u'∋', u'\\nleftarrow':u'↚', u'\\nleftrightarrow':u'↮',
547 u'\\nleqslant':u'≰', u'\\nless':u'≮', u'\\nlessgtr':u'≸', u'\\nmid':u'∤',
548 u'\\nonumber':u'', u'\\not':u'¬', u'\\not<':u'≮', u'\\not=':u'≠',
549 u'\\not>':u'≯', u'\\not\\in':u' ∉ ', u'\\notbackslash':u'⍀',
550 u'\\notin':u'∉', u'\\notni':u'∌', u'\\notslash':u'⌿',
551 u'\\nparallel':u'∦', u'\\nprec':u'⊀', u'\\nrightarrow':u'↛',
552 u'\\nsim':u'≁', u'\\nsimeq':u'≄', u'\\nsqsubset':u'⊏̸',
553 u'\\nsubseteq':u'⊈', u'\\nsucc':u'⊁', u'\\nsucccurlyeq':u'⋡',
554 u'\\nsupset':u'⊅', u'\\nsupseteq':u'⊉', u'\\ntriangleleft':u'⋪',
555 u'\\ntrianglelefteq':u'⋬', u'\\ntriangleright':u'⋫',
556 u'\\ntrianglerighteq':u'⋭', u'\\nvDash':u'⊭', u'\\nvdash':u'⊬',
557 u'\\nwarrow':u'↖', u'\\odot':u'⊙', u'\\officialeuro':u'€',
558 u'\\oiiint':u'<span class="bigsymbol">∰</span>',
559 u'\\oiint':u'<span class="bigsymbol">∯</span>',
560 u'\\oint':u'<span class="bigsymbol">∮</span>',
561 u'\\ointclockwise':u'<span class="bigsymbol">∲</span>',
562 u'\\ointctrclockwise':u'<span class="bigsymbol">∳</span>',
563 u'\\ominus':u'⊖', u'\\oplus':u'⊕', u'\\oslash':u'⊘', u'\\otimes':u'⊗',
564 u'\\owns':u'∋', u'\\parallel':u'∥', u'\\partial':u'∂', u'\\perp':u'⊥',
565 u'\\pisces':u'♓', u'\\pitchfork':u'⋔', u'\\pluto':u'♇', u'\\pm':u'±',
566 u'\\pointer':u'➪', u'\\pounds':u'£', u'\\prec':u'≺',
567 u'\\preccurlyeq':u'≼', u'\\preceq':u'≼', u'\\precsim':u'≾',
568 u'\\prime':u'′', u'\\prod':u'<span class="bigsymbol">∏</span>',
569 u'\\prompto':u'∝', u'\\propto':u' ∝ ', u'\\qquad':u' ', u'\\quad':u' ',
570 u'\\quarternote':u'♩', u'\\rangle':u'⟩', u'\\rbrace':u'}',
571 u'\\rbrace)':u'}', u'\\rbrack':u']', u'\\rceil':u'⌉', u'\\rfloor':u'⌋',
572 u'\\rhd':u'⊳', u'\\rightarrow':u' → ', u'\\rightarrow)':u'→',
573 u'\\rightarrowtail':u'↣', u'\\rightarrowtobar':u'⇥',
574 u'\\rightharpoondown':u'⇁', u'\\rightharpoonup':u'⇀',
575 u'\\rightharpooondown':u'⇁', u'\\rightharpooonup':u'⇀',
576 u'\\rightleftarrows':u'⇄', u'\\rightleftharpoons':u'⇌',
577 u'\\rightmoon':u'☽', u'\\rightrightarrows':u'⇉',
578 u'\\rightrightharpoons':u'⥤', u'\\rightsquigarrow':u' ⇝ ',
579 u'\\rightthreetimes':u'⋌', u'\\risingdotseq':u'≓', u'\\rtimes':u'⋊',
580 u'\\sagittarius':u'♐', u'\\saturn':u'♄', u'\\scorpio':u'♏',
581 u'\\scriptscriptstyle':u'', u'\\scriptstyle':u'', u'\\searrow':u'↘',
582 u'\\sec':u'sec', u'\\setminus':u'∖', u'\\sharp':u'♯', u'\\sim':u' ~ ',
583 u'\\simeq':u'≃', u'\\sin':u'sin', u'\\sinh':u'sinh', u'\\slash':u'∕',
584 u'\\smile':u'⌣', u'\\smiley':u'☺', u'\\spadesuit':u'♠',
585 u'\\sphericalangle':u'∢', u'\\sqcap':u'⊓', u'\\sqcup':u'⊔',
586 u'\\sqsubset':u'⊏', u'\\sqsubseteq':u'⊑', u'\\sqsupset':u'⊐',
587 u'\\sqsupseteq':u'⊒', u'\\square':u'□', u'\\star':u'⋆',
588 u'\\subset':u' ⊂ ', u'\\subseteq':u'⊆', u'\\subseteqq':u'⫅',
589 u'\\subsetneqq':u'⫋', u'\\succ':u'≻', u'\\succcurlyeq':u'≽',
590 u'\\succeq':u'≽', u'\\succnsim':u'⋩', u'\\succsim':u'≿',
591 u'\\sum':u'<span class="bigsymbol">∑</span>', u'\\sun':u'☼',
592 u'\\sup':u'sup', u'\\supset':u' ⊃ ', u'\\supseteq':u'⊇',
593 u'\\supseteqq':u'⫆', u'\\supsetneqq':u'⫌', u'\\surd':u'√',
594 u'\\swarrow':u'↙', u'\\tan':u'tan', u'\\tanh':u'tanh', u'\\taurus':u'♉',
595 u'\\textasciicircum':u'^', u'\\textasciitilde':u'~',
596 u'\\textbackslash':u'\\', u'\\textendash':u'—', u'\\textgreater':u'>',
597 u'\\textless':u'<', u'\\textquotedblleft':u'“',
598 u'\\textquotedblright':u'”', u'\\textstyle':u'', u'\\therefore':u'∴',
599 u'\\times':u' × ', u'\\to':u'→', u'\\top':u'⊤', u'\\triangle':u'△',
600 u'\\triangleleft':u'⊲', u'\\trianglelefteq':u'⊴', u'\\triangleq':u'≜',
601 u'\\triangleright':u'▷', u'\\trianglerighteq':u'⊵',
602 u'\\twoheadleftarrow':u'↞', u'\\twoheadrightarrow':u'↠',
603 u'\\twonotes':u'♫', u'\\udot':u'⊍', u'\\unlhd':u'⊴', u'\\unrhd':u'⊵',
604 u'\\unrhl':u'⊵', u'\\uparrow':u'↑', u'\\updownarrow':u'↕',
605 u'\\upharpoonleft':u'↿', u'\\upharpoonright':u'↾', u'\\uplus':u'⊎',
606 u'\\upuparrows':u'⇈', u'\\uranus':u'♅', u'\\vDash':u'⊨',
607 u'\\varclubsuit':u'♧', u'\\vardiamondsuit':u'♦', u'\\varheartsuit':u'♥',
608 u'\\varnothing':u'∅', u'\\varspadesuit':u'♤', u'\\vdash':u'⊢',
609 u'\\vdots':u'⋮', u'\\vee':u'∨', u'\\vee)':u'∨', u'\\veebar':u'⊻',
610 u'\\vert':u'∣', u'\\virgo':u'♍', u'\\wedge':u'∧', u'\\wedge)':u'∧',
611 u'\\wp':u'℘', u'\\wr':u'≀', u'\\yen':u'¥', u'\\{':u'{', u'\\|':u'∥',
612 u'\\}':u'}',
615 decoratingfunctions = {
616 u'\\overleftarrow':u'⟵', u'\\overrightarrow':u'⟶', u'\\widehat':u'^',
619 definingfunctions = {
620 u'\\newcommand':u'[$n!][$1][$2][$3][$4][$5][$6][$7][$8][$9]{$d}',
621 u'\\renewcommand':u'[$n!][$1][$2][$3][$4][$5][$6][$7][$8][$9]{$d}',
624 endings = {
625 u'bracket':u'}', u'complex':u'\\]', u'endafter':u'}',
626 u'endbefore':u'\\end{', u'squarebracket':u']',
629 environments = {
630 u'align':[u'r',u'l',], u'eqnarray':[u'r',u'c',u'l',],
631 u'gathered':[u'l',u'l',],
634 fontfunctions = {
635 u'\\boldsymbol':u'b', u'\\mathbb':u'span class="blackboard"',
636 u'\\mathbb{A}':u'𝔸', u'\\mathbb{B}':u'𝔹', u'\\mathbb{C}':u'ℂ',
637 u'\\mathbb{D}':u'𝔻', u'\\mathbb{E}':u'𝔼', u'\\mathbb{F}':u'𝔽',
638 u'\\mathbb{G}':u'𝔾', u'\\mathbb{H}':u'ℍ', u'\\mathbb{J}':u'𝕁',
639 u'\\mathbb{K}':u'𝕂', u'\\mathbb{L}':u'𝕃', u'\\mathbb{N}':u'ℕ',
640 u'\\mathbb{O}':u'𝕆', u'\\mathbb{P}':u'ℙ', u'\\mathbb{Q}':u'ℚ',
641 u'\\mathbb{R}':u'ℝ', u'\\mathbb{S}':u'𝕊', u'\\mathbb{T}':u'𝕋',
642 u'\\mathbb{W}':u'𝕎', u'\\mathbb{Z}':u'ℤ', u'\\mathbf':u'b',
643 u'\\mathcal':u'span class="script"',
644 u'\\mathfrak':u'span class="fraktur"', u'\\mathfrak{C}':u'ℭ',
645 u'\\mathfrak{F}':u'𝔉', u'\\mathfrak{H}':u'ℌ', u'\\mathfrak{I}':u'ℑ',
646 u'\\mathfrak{R}':u'ℜ', u'\\mathfrak{Z}':u'ℨ', u'\\mathit':u'i',
647 u'\\mathring{A}':u'Å', u'\\mathring{U}':u'Ů', u'\\mathring{a}':u'å',
648 u'\\mathring{u}':u'ů', u'\\mathring{w}':u'ẘ', u'\\mathring{y}':u'ẙ',
649 u'\\mathrm':u'span class="mathrm"', u'\\mathscr':u'span class="script"',
650 u'\\mathscr{B}':u'ℬ', u'\\mathscr{E}':u'ℰ', u'\\mathscr{F}':u'ℱ',
651 u'\\mathscr{H}':u'ℋ', u'\\mathscr{I}':u'ℐ', u'\\mathscr{L}':u'ℒ',
652 u'\\mathscr{M}':u'ℳ', u'\\mathscr{R}':u'ℛ',
653 u'\\mathsf':u'span class="mathsf"', u'\\mathtt':u'tt',
656 hybridfunctions = {
658 u'\\binom':[u'{$1}{$2}',u'f3{(}f0{f1{$1}f2{$2}}f3{)}',u'span class="binom"',u'span class="upbinom"',u'span class="downbinom"',u'span class="bigsymbol"',],
659 u'\\boxed':[u'{$1}',u'f0{$1}',u'span class="boxed"',],
660 u'\\cfrac':[u'[$p!]{$1}{$2}',u'f0{f1{$1}f2{$2}}',u'span class="fullfraction"',u'span class="numerator$p"',u'span class="denominator"',],
661 u'\\color':[u'{$p!}{$1}',u'f0{$1}',u'span style="color: $p;"',],
662 u'\\colorbox':[u'{$p!}{$1}',u'f0{$1}',u'span class="colorbox" style="background: $p;"',],
663 u'\\dbinom':[u'{$1}{$2}',u'f3{(}f0{f1{$1}f2{$2}}f3{)}',u'span class="fullbinom"',u'span class="upbinom"',u'span class="downbinom"',u'span class="bigsymbol"',],
664 u'\\dfrac':[u'{$1}{$2}',u'f0{f1{$1}f2{$2}}',u'span class="fullfraction"',u'span class="numerator"',u'span class="denominator"',],
665 u'\\fbox':[u'{$1}',u'f0{$1}',u'span class="fbox"',],
666 u'\\fcolorbox':[u'{$p!}{$q!}{$1}',u'f0{$1}',u'span class="boxed" style="border-color: $p; background: $q;"',],
667 u'\\frac':[u'{$1}{$2}',u'f0{f1{$1}f2{$2}}',u'span class="fraction"',u'span class="numerator"',u'span class="denominator"',],
668 u'\\framebox':[u'[$p!][$q!]{$1}',u'f0{$1}',u'span class="framebox-$q" style="width: $p;"',],
669 u'\\hspace':[u'{$p!}',u'f0{ }',u'span class="hspace" style="width: $p;"',],
670 u'\\leftroot':[u'{$p!}',u'f0{ }',u'span class="leftroot" style="width: $p;px"',],
671 u'\\nicefrac':[u'{$1}{$2}',u'f0{f1{$1}⁄f2{$2}}',u'span class="fraction"',u'sup class="numerator"',u'sub class="denominator"',],
672 u'\\raisebox':[u'{$p!}{$1}',u'f0{$1}',u'span class="raisebox" style="vertical-align: $p;"',],
673 u'\\renewenvironment':[u'{$1!}{$2!}{$3!}',u'',],
674 u'\\sqrt':[u'[$0]{$1}',u'f1{$0}f0{f2{√}f3{$1}}',u'span class="sqrt"',u'sup',u'span class="radical"',u'span class="root"',],
675 u'\\stackrel':[u'{$1}{$2}',u'f0{f1{$1}f2{$2}}',u'span class="stackrel"',u'span class="upstackrel"',u'span class="downstackrel"',],
676 u'\\tbinom':[u'{$1}{$2}',u'f3{(}f0{f1{$1}f2{$2}}f3{)}',u'span class="fullbinom"',u'span class="upbinom"',u'span class="downbinom"',u'span class="bigsymbol"',],
677 u'\\textcolor':[u'{$p!}{$1}',u'f0{$1}',u'span style="color: $p;"',],
678 u'\\unit':[u'[$0]{$1}',u'$0f0{$1.font}',u'span class="unit"',],
679 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"',],
680 u'\\uproot':[u'{$p!}',u'f0{ }',u'span class="uproot" style="width: $p;px"',],
681 u'\\vspace':[u'{$p!}',u'f0{ }',u'span class="vspace" style="height: $p;"',],
684 labelfunctions = {
685 u'\\label':u'a name="#"',
688 limits = {
689 u'commands':[u'\\sum',u'\\int',u'\\intop',], u'operands':[u'^',u'_',],
692 modified = {
693 u'\n':u'', u' ':u'', u'$':u'', u'&':u' ', u'\'':u'’', u'+':u' + ',
694 u',':u', ', u'-':u' − ', u'/':u' ⁄ ', u'<':u' &lt; ', u'=':u' = ',
695 u'>':u' &gt; ', u'@':u'', u'~':u'',
698 onefunctions = {
699 u'\\Big':u'span class="bigsymbol"', u'\\Bigg':u'span class="hugesymbol"',
700 u'\\bar':u'span class="bar"', u'\\begin{array}':u'span class="arraydef"',
701 u'\\big':u'span class="symbol"', u'\\bigg':u'span class="largesymbol"',
702 u'\\bigl':u'span class="bigsymbol"', u'\\bigr':u'span class="bigsymbol"',
703 u'\\ensuremath':u'span class="ensuremath"',
704 u'\\hphantom':u'span class="phantom"', u'\\left':u'span class="symbol"',
705 u'\\left.':u'<span class="leftdot"></span>',
706 u'\\middle':u'span class="symbol"',
707 u'\\overbrace':u'span class="overbrace"',
708 u'\\overline':u'span class="overline"',
709 u'\\phantom':u'span class="phantom"', u'\\right':u'span class="symbol"',
710 u'\\right.':u'<span class="rightdot"></span>',
711 u'\\underbrace':u'span class="underbrace"', u'\\underline':u'u',
712 u'\\vphantom':u'span class="phantom"',
715 preamblefunctions = {
716 u'\\setcounter':[u'{$p!}{$n!}',u'setcounter',],
719 starts = {
720 u'beginafter':u'}', u'beginbefore':u'\\begin{', u'bracket':u'{',
721 u'command':u'\\', u'comment':u'%', u'complex':u'\\[', u'simple':u'$',
722 u'squarebracket':u'[', u'unnumbered':u'*',
725 symbolfunctions = {
726 u'^':u'sup', u'_':u'sub',
729 textfunctions = {
730 u'\\mbox':u'span class="mbox"', u'\\text':u'span class="text"',
731 u'\\textbf':u'b', u'\\textipa':u'span class="textipa"', u'\\textit':u'i',
732 u'\\textnormal':u'span class="textnormal"',
733 u'\\textrm':u'span class="textrm"',
734 u'\\textsc':u'span class="versalitas"',
735 u'\\textsf':u'span class="textsf"', u'\\textsl':u'i', u'\\texttt':u'tt',
736 u'\\textup':u'span class="normal"',
739 unmodified = {
741 u'characters':[u'.',u'*',u'€',u'(',u')',u'[',u']',u':',u'·',u'!',u';',u'|',u'§',u'"',],
744 class GeneralConfig(object):
745 "Configuration class from config file"
747 version = {
748 u'date':u'2010-11-23', u'lyxformat':u'398', u'number':u'1.1.0',
751 class HeaderConfig(object):
752 "Configuration class from config file"
754 parameters = {
755 u'beginpreamble':u'\\begin_preamble', u'branch':u'\\branch',
756 u'documentclass':u'\\textclass', u'endbranch':u'\\end_branch',
757 u'endpreamble':u'\\end_preamble', u'language':u'\\language',
758 u'lstset':u'\\lstset', u'outputchanges':u'\\output_changes',
759 u'paragraphseparation':u'\\paragraph_separation',
760 u'pdftitle':u'\\pdf_title', u'secnumdepth':u'\\secnumdepth',
761 u'tocdepth':u'\\tocdepth',
764 styles = {
766 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',],
767 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',],
770 class ImageConfig(object):
771 "Configuration class from config file"
773 converters = {
775 u'imagemagick':u'convert[ -density $scale][ -define $format:use-cropbox=true] "$input" "$output"',
776 u'inkscape':u'inkscape "$input" --export-png="$output"',
779 cropboxformats = {
780 u'.eps':u'ps', u'.pdf':u'pdf', u'.ps':u'ps',
783 formats = {
784 u'default':u'.png', u'vector':[u'.svg',u'.eps',],
787 class LayoutConfig(object):
788 "Configuration class from config file"
790 groupable = {
792 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',],
795 class NewfangleConfig(object):
796 "Configuration class from config file"
798 constants = {
799 u'chunkref':u'chunkref{', u'endcommand':u'}', u'endmark':u'&gt;',
800 u'startcommand':u'\\', u'startmark':u'=&lt;',
803 class NumberingConfig(object):
804 "Configuration class from config file"
806 layouts = {
808 u'ordered':[u'Chapter',u'Section',u'Subsection',u'Subsubsection',u'Paragraph',],
809 u'roman':[u'Part',u'Book',],
812 sequence = {
813 u'symbols':[u'*',u'**',u'†',u'‡',u'§',u'§§',u'¶',u'¶¶',u'#',u'##',],
816 class StyleConfig(object):
817 "Configuration class from config file"
819 hspaces = {
820 u'\\enskip{}':u' ', u'\\hfill{}':u'<span class="hfill"> </span>',
821 u'\\hspace*{\\fill}':u' ', u'\\hspace*{}':u'', u'\\hspace{}':u' ',
822 u'\\negthinspace{}':u'', u'\\qquad{}':u'  ', u'\\quad{}':u' ',
823 u'\\space{}':u' ', u'\\thinspace{}':u' ', u'~':u' ',
826 quotes = {
827 u'ald':u'»', u'als':u'›', u'ard':u'«', u'ars':u'‹', u'eld':u'“',
828 u'els':u'‘', u'erd':u'”', u'ers':u'’', u'fld':u'«', u'fls':u'‹',
829 u'frd':u'»', u'frs':u'›', u'gld':u'„', u'gls':u'‚', u'grd':u'“',
830 u'grs':u'‘', u'pld':u'„', u'pls':u'‚', u'prd':u'”', u'prs':u'’',
831 u'sld':u'”', u'srd':u'”',
834 size = {
835 u'ignoredtexts':[u'col',u'text',u'line',u'page',u'theight',u'pheight',],
838 vspaces = {
839 u'bigskip':u'<div class="bigskip"> </div>',
840 u'defskip':u'<div class="defskip"> </div>',
841 u'medskip':u'<div class="medskip"> </div>',
842 u'smallskip':u'<div class="smallskip"> </div>',
843 u'vfill':u'<div class="vfill"> </div>',
846 class TOCConfig(object):
847 "Configuration class from config file"
849 extractplain = {
851 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',],
852 u'cloned':[u'',], u'extracted':[u'',],
855 extracttitle = {
856 u'allowed':[u'StringContainer',u'Constant',u'Space',],
857 u'cloned':[u'TextFamily',u'EmphaticText',u'VersalitasText',u'BarredText',u'SizeText',u'ColorText',u'LangLine',u'Formula',],
858 u'extracted':[u'PlainLayout',u'TaggedText',u'Align',u'Caption',u'StandardLayout',],
861 class TagConfig(object):
862 "Configuration class from config file"
864 barred = {
865 u'under':u'u',
868 family = {
869 u'sans':u'span class="sans"', u'typewriter':u'tt',
872 flex = {
873 u'CharStyle:Code':u'span class="code"',
874 u'CharStyle:MenuItem':u'span class="menuitem"',
877 group = {
878 u'layouts':[u'Quotation',u'Quote',],
881 layouts = {
882 u'Center':u'div', u'Chapter':u'h?', u'Date':u'h2', u'Paragraph':u'div',
883 u'Part':u'h1', u'Quotation':u'blockquote', u'Quote':u'blockquote',
884 u'Section':u'h?', u'Subsection':u'h?', u'Subsubsection':u'h?',
887 listitems = {
888 u'Enumerate':u'ol', u'Itemize':u'ul',
891 notes = {
892 u'Comment':u'', u'Greyedout':u'span class="greyedout"', u'Note':u'',
895 shaped = {
896 u'italic':u'i', u'slanted':u'i', u'smallcaps':u'span class="versalitas"',
899 class TranslationConfig(object):
900 "Configuration class from config file"
902 constants = {
903 u'Appendix':u'Appendix', u'Book':u'Book', u'Chapter':u'Chapter',
904 u'Paragraph':u'Paragraph', u'Part':u'Part', u'Section':u'Section',
905 u'Subsection':u'Subsection', u'Subsubsection':u'Subsubsection',
906 u'abstract':u'Abstract', u'bibliography':u'Bibliography',
907 u'figure':u'figure', u'float-algorithm':u'Algorithm ',
908 u'float-figure':u'Figure ', u'float-listing':u'Listing ',
909 u'float-table':u'Table ', u'float-tableau':u'Tableau ',
910 u'footnotes':u'Footnotes', u'generated-by':u'Document generated by ',
911 u'generated-on':u' on ', u'index':u'Index',
912 u'jsmath-enable':u'Please enable JavaScript on your browser.',
913 u'jsmath-requires':u' requires JavaScript to correctly process the mathematics on this page. ',
914 u'jsmath-warning':u'Warning: ', u'list-algorithm':u'List of Algorithms',
915 u'list-figure':u'List of Figures', u'list-table':u'List of Tables',
916 u'list-tableau':u'List of Tableaux', u'main-page':u'Main page',
917 u'next':u'Next', u'nomenclature':u'Nomenclature',
918 u'on-page':u' on page ', u'prev':u'Previous',
919 u'references':u'References', u'toc':u'Table of Contents',
920 u'toc-for':u'Contents for ', u'up':u'Up',
923 languages = {
924 u'american':u'en', u'british':u'en', u'deutsch':u'de', u'dutch':u'nl',
925 u'english':u'en', u'french':u'fr', u'ngerman':u'de', u'spanish':u'es',
933 class CommandLineParser(object):
934 "A parser for runtime options"
936 def __init__(self, options):
937 self.options = options
939 def parseoptions(self, args):
940 "Parse command line options"
941 if len(args) == 0:
942 return None
943 while len(args) > 0 and args[0].startswith('--'):
944 key, value = self.readoption(args)
945 if not key:
946 return 'Option ' + value + ' not recognized'
947 if not value:
948 return 'Option ' + key + ' needs a value'
949 setattr(self.options, key, value)
950 return None
952 def readoption(self, args):
953 "Read the key and value for an option"
954 arg = args[0][2:]
955 del args[0]
956 if '=' in arg:
957 return self.readequals(arg, args)
958 key = arg
959 if not hasattr(self.options, key):
960 return None, key
961 current = getattr(self.options, key)
962 if current.__class__ == bool:
963 return key, True
964 # read value
965 if len(args) == 0:
966 return key, None
967 if args[0].startswith('"'):
968 initial = args[0]
969 del args[0]
970 return key, self.readquoted(args, initial)
971 value = args[0]
972 del args[0]
973 return key, value
975 def readquoted(self, args, initial):
976 "Read a value between quotes"
977 value = initial[1:]
978 while len(args) > 0 and not args[0].endswith('"') and not args[0].startswith('--'):
979 value += ' ' + args[0]
980 del args[0]
981 if len(args) == 0 or args[0].startswith('--'):
982 return None
983 value += ' ' + args[0:-1]
984 return value
986 def readequals(self, arg, args):
987 "Read a value with equals"
988 split = arg.split('=', 1)
989 key = split[0]
990 if not hasattr(self.options, key):
991 return None, key
992 value = split[1]
993 if not value.startswith('"'):
994 return key, value
995 return key, self.readquoted(args, value)
999 class Options(object):
1000 "A set of runtime options"
1002 instance = None
1004 location = None
1005 nocopy = False
1006 copyright = False
1007 debug = False
1008 quiet = False
1009 version = False
1010 hardversion = False
1011 versiondate = False
1012 html = False
1013 help = False
1014 showlines = True
1015 unicode = False
1016 iso885915 = False
1017 css = 'http://www.nongnu.org/elyxer/lyx.css'
1018 title = None
1019 directory = None
1020 destdirectory = None
1021 toc = False
1022 toctarget = ''
1023 forceformat = None
1024 lyxformat = False
1025 target = None
1026 splitpart = None
1027 memory = True
1028 lowmem = False
1029 nobib = False
1030 converter = 'imagemagick'
1031 raw = False
1032 jsmath = None
1033 mathjax = None
1034 nofooter = False
1035 template = None
1036 noconvert = False
1037 notoclabels = False
1038 letterfoot = True
1039 numberfoot = False
1040 symbolfoot = False
1041 hoverfoot = True
1042 marginfoot = False
1043 endfoot = False
1044 supfoot = True
1045 alignfoot = False
1046 footnotes = None
1048 branches = dict()
1050 def parseoptions(self, args):
1051 "Parse command line options"
1052 Options.location = args[0]
1053 del args[0]
1054 parser = CommandLineParser(Options)
1055 result = parser.parseoptions(args)
1056 if result:
1057 Trace.error(result)
1058 self.usage()
1059 if Options.help:
1060 self.usage()
1061 if Options.version:
1062 self.showversion()
1063 if Options.hardversion:
1064 self.showhardversion()
1065 if Options.versiondate:
1066 self.showversiondate()
1067 if Options.lyxformat:
1068 self.showlyxformat()
1069 if Options.splitpart:
1070 try:
1071 Options.splitpart = int(Options.splitpart)
1072 if Options.splitpart <= 0:
1073 Trace.error('--splitpart requires a number bigger than zero')
1074 self.usage()
1075 except:
1076 Trace.error('--splitpart needs a numeric argument, not ' + Options.splitpart)
1077 self.usage()
1078 if Options.lowmem or Options.toc:
1079 Options.memory = False
1080 self.parsefootnotes()
1081 # set in Trace if necessary
1082 for param in dir(Options):
1083 if hasattr(Trace, param + 'mode'):
1084 setattr(Trace, param + 'mode', getattr(self, param))
1086 def usage(self):
1087 "Show correct usage"
1088 Trace.error('Usage: ' + os.path.basename(Options.location) + ' [options] [filein] [fileout]')
1089 Trace.error('Convert LyX input file "filein" to HTML file "fileout".')
1090 Trace.error('If filein (or fileout) is not given use standard input (or output).')
1091 Trace.error('Main program of the eLyXer package (http://elyxer.nongnu.org/).')
1092 self.showoptions()
1094 def parsefootnotes(self):
1095 "Parse footnotes options."
1096 if not Options.footnotes:
1097 return
1098 Options.marginfoot = False
1099 Options.letterfoot = False
1100 options = Options.footnotes.split(',')
1101 for option in options:
1102 footoption = option + 'foot'
1103 if hasattr(Options, footoption):
1104 setattr(Options, footoption, True)
1105 else:
1106 Trace.error('Unknown footnotes option: ' + option)
1107 if not Options.endfoot and not Options.marginfoot and not Options.hoverfoot:
1108 Options.hoverfoot = True
1109 if not Options.numberfoot and not Options.symbolfoot:
1110 Options.letterfoot = True
1112 def showoptions(self):
1113 "Show all possible options"
1114 Trace.error(' Common options:')
1115 Trace.error(' --help: show this online help')
1116 Trace.error(' --quiet: disables all runtime messages')
1117 Trace.error('')
1118 Trace.error(' Advanced options:')
1119 Trace.error(' --debug: enable debugging messages (for developers)')
1120 Trace.error(' --version: show version number and release date')
1121 Trace.error(' --lyxformat: return the highest LyX version supported')
1122 Trace.error(' Options for HTML output:')
1123 Trace.error(' --title "title": set the generated page title')
1124 Trace.error(' --css "file.css": use a custom CSS file')
1125 Trace.error(' --html: output HTML 4.0 instead of the default XHTML')
1126 Trace.error(' --unicode: full Unicode output')
1127 Trace.error(' --iso885915: output a document with ISO-8859-15 encoding')
1128 Trace.error(' --nofooter: remove the footer "create by eLyXer"')
1129 Trace.error(' Options for image output:')
1130 Trace.error(' --directory "img_dir": look for images in the specified directory')
1131 Trace.error(' --destdirectory "dest": put converted images into this directory')
1132 Trace.error(' --forceformat ".ext": force image output format')
1133 Trace.error(' --converter "inkscape": use an alternative program to convert images')
1134 Trace.error(' --noconvert: do not convert images, use in their original format')
1135 Trace.error(' Options for footnote display:')
1136 Trace.error(' --numberfoot: mark footnotes with numbers instead of letters')
1137 Trace.error(' --symbolfoot: mark footnotes with symbols (*, **...)')
1138 Trace.error(' --hoverfoot: show footnotes as hovering text (default)')
1139 Trace.error(' --marginfoot: show footnotes with numbers instead of letters')
1140 Trace.error(' --endfoot: show footnotes at the end of the page')
1141 Trace.error(' --supfoot: use superscript for footnote markers (default)')
1142 Trace.error(' --alignfoot: use aligned text for footnote markers')
1143 Trace.error(' --footnotes "options": specify several comma-separated footnotes options')
1144 Trace.error(' Available options are: "number", "symbol", "hover", "margin", "end",')
1145 Trace.error(' "sup", "align"')
1146 Trace.error(' Advanced output options:')
1147 Trace.error(' --splitpart "depth": split the resulting webpage at the given depth')
1148 Trace.error(' --toc: create a table of contents')
1149 Trace.error(' --target "frame": make all links point to the given frame')
1150 Trace.error(' --toctarget "page": generate a TOC that points to the given page')
1151 Trace.error(' --notoclabels: omit the part labels in the TOC, such as Chapter')
1152 Trace.error(' --lowmem: do the conversion on the fly (conserve memory)')
1153 Trace.error(' --raw: generate HTML without header or footer.')
1154 Trace.error(' --jsmath "URL": use jsMath from the given URL to display equations')
1155 Trace.error(' --mathjax "URL": use MathJax from the given URL to display equations')
1156 Trace.error(' --template "file": use a template, put everything in <!--$content-->')
1157 Trace.error(' --copyright: add a copyright notice at the bottom')
1158 Trace.error(' --nocopy (deprecated): no effect, maintained for backwards compatibility')
1159 sys.exit()
1161 def showversion(self):
1162 "Return the current eLyXer version string"
1163 string = 'eLyXer version ' + GeneralConfig.version['number']
1164 string += ' (' + GeneralConfig.version['date'] + ')'
1165 Trace.error(string)
1166 sys.exit()
1168 def showhardversion(self):
1169 "Return just the version string"
1170 Trace.message(GeneralConfig.version['number'])
1171 sys.exit()
1173 def showversiondate(self):
1174 "Return just the version dte"
1175 Trace.message(GeneralConfig.version['date'])
1176 sys.exit()
1178 def showlyxformat(self):
1179 "Return just the lyxformat parameter"
1180 Trace.message(GeneralConfig.version['lyxformat'])
1181 sys.exit()
1183 class BranchOptions(object):
1184 "A set of options for a branch"
1186 def __init__(self, name):
1187 self.name = name
1188 self.options = {'color':'#ffffff'}
1190 def set(self, key, value):
1191 "Set a branch option"
1192 if not key.startswith(ContainerConfig.string['startcommand']):
1193 Trace.error('Invalid branch option ' + key)
1194 return
1195 key = key.replace(ContainerConfig.string['startcommand'], '')
1196 self.options[key] = value
1198 def isselected(self):
1199 "Return if the branch is selected"
1200 if not 'selected' in self.options:
1201 return False
1202 return self.options['selected'] == '1'
1204 def __unicode__(self):
1205 "String representation"
1206 return 'options for ' + self.name + ': ' + unicode(self.options)
1210 class Parser(object):
1211 "A generic parser"
1213 def __init__(self):
1214 self.begin = 0
1215 self.parameters = dict()
1217 def parseheader(self, reader):
1218 "Parse the header"
1219 header = reader.currentline().split()
1220 reader.nextline()
1221 self.begin = reader.linenumber
1222 return header
1224 def parseparameter(self, reader):
1225 "Parse a parameter"
1226 if reader.currentline().strip().startswith('<'):
1227 key, value = self.parsexml(reader)
1228 self.parameters[key] = value
1229 return
1230 split = reader.currentline().strip().split(' ', 1)
1231 reader.nextline()
1232 if len(split) == 0:
1233 return
1234 key = split[0]
1235 if len(split) == 1:
1236 self.parameters[key] = True
1237 return
1238 if not '"' in split[1]:
1239 self.parameters[key] = split[1].strip()
1240 return
1241 doublesplit = split[1].split('"')
1242 self.parameters[key] = doublesplit[1]
1244 def parsexml(self, reader):
1245 "Parse a parameter in xml form: <param attr1=value...>"
1246 strip = reader.currentline().strip()
1247 reader.nextline()
1248 if not strip.endswith('>'):
1249 Trace.error('XML parameter ' + strip + ' should be <...>')
1250 split = strip[1:-1].split()
1251 if len(split) == 0:
1252 Trace.error('Empty XML parameter <>')
1253 return None, None
1254 key = split[0]
1255 del split[0]
1256 if len(split) == 0:
1257 return key, dict()
1258 attrs = dict()
1259 for attr in split:
1260 if not '=' in attr:
1261 Trace.error('Erroneous attribute ' + attr)
1262 attr += '="0"'
1263 parts = attr.split('=')
1264 attrkey = parts[0]
1265 value = parts[1].split('"')[1]
1266 attrs[attrkey] = value
1267 return key, attrs
1269 def parseending(self, reader, process):
1270 "Parse until the current ending is found"
1271 if not self.ending:
1272 Trace.error('No ending for ' + unicode(self))
1273 return
1274 while not reader.currentline().startswith(self.ending):
1275 process()
1277 def parsecontainer(self, reader, contents):
1278 container = self.factory.createcontainer(reader)
1279 if container:
1280 container.parent = self.parent
1281 contents.append(container)
1283 def __unicode__(self):
1284 "Return a description"
1285 return self.__class__.__name__ + ' (' + unicode(self.begin) + ')'
1287 class LoneCommand(Parser):
1288 "A parser for just one command line"
1290 def parse(self,reader):
1291 "Read nothing"
1292 return []
1294 class TextParser(Parser):
1295 "A parser for a command and a bit of text"
1297 stack = []
1299 def __init__(self, container):
1300 Parser.__init__(self)
1301 self.ending = None
1302 if container.__class__.__name__ in ContainerConfig.endings:
1303 self.ending = ContainerConfig.endings[container.__class__.__name__]
1304 self.endings = []
1306 def parse(self, reader):
1307 "Parse lines as long as they are text"
1308 TextParser.stack.append(self.ending)
1309 self.endings = TextParser.stack + [ContainerConfig.endings['Layout'],
1310 ContainerConfig.endings['Inset'], self.ending]
1311 contents = []
1312 while not self.isending(reader):
1313 self.parsecontainer(reader, contents)
1314 return contents
1316 def isending(self, reader):
1317 "Check if text is ending"
1318 current = reader.currentline().split()
1319 if len(current) == 0:
1320 return False
1321 if current[0] in self.endings:
1322 if current[0] in TextParser.stack:
1323 TextParser.stack.remove(current[0])
1324 else:
1325 TextParser.stack = []
1326 return True
1327 return False
1329 class ExcludingParser(Parser):
1330 "A parser that excludes the final line"
1332 def parse(self, reader):
1333 "Parse everything up to (and excluding) the final line"
1334 contents = []
1335 self.parseending(reader, lambda: self.parsecontainer(reader, contents))
1336 return contents
1338 class BoundedParser(ExcludingParser):
1339 "A parser bound by a final line"
1341 def parse(self, reader):
1342 "Parse everything, including the final line"
1343 contents = ExcludingParser.parse(self, reader)
1344 # skip last line
1345 reader.nextline()
1346 return contents
1348 class BoundedDummy(Parser):
1349 "A bound parser that ignores everything"
1351 def parse(self, reader):
1352 "Parse the contents of the container"
1353 self.parseending(reader, lambda: reader.nextline())
1354 # skip last line
1355 reader.nextline()
1356 return []
1358 class StringParser(Parser):
1359 "Parses just a string"
1361 def parseheader(self, reader):
1362 "Do nothing, just take note"
1363 self.begin = reader.linenumber + 1
1364 return []
1366 def parse(self, reader):
1367 "Parse a single line"
1368 contents = reader.currentline()
1369 reader.nextline()
1370 return contents
1372 class InsetParser(BoundedParser):
1373 "Parses a LyX inset"
1375 def parse(self, reader):
1376 "Parse inset parameters into a dictionary"
1377 startcommand = ContainerConfig.string['startcommand']
1378 while reader.currentline() != '' and not reader.currentline().startswith(startcommand):
1379 self.parseparameter(reader)
1380 return BoundedParser.parse(self, reader)
1387 class ContainerOutput(object):
1388 "The generic HTML output for a container."
1390 def gethtml(self, container):
1391 "Show an error."
1392 Trace.error('gethtml() not implemented for ' + unicode(self))
1394 def isempty(self):
1395 "Decide if the output is empty: by default, not empty."
1396 return False
1398 class EmptyOutput(ContainerOutput):
1400 def gethtml(self, container):
1401 "Return empty HTML code."
1402 return []
1404 def isempty(self):
1405 "This output is particularly empty."
1406 return True
1408 class FixedOutput(ContainerOutput):
1409 "Fixed output"
1411 def gethtml(self, container):
1412 "Return constant HTML code"
1413 return container.html
1415 class ContentsOutput(ContainerOutput):
1416 "Outputs the contents converted to HTML"
1418 def gethtml(self, container):
1419 "Return the HTML code"
1420 html = []
1421 if container.contents == None:
1422 return html
1423 for element in container.contents:
1424 if not hasattr(element, 'gethtml'):
1425 Trace.error('No html in ' + element.__class__.__name__ + ': ' + unicode(element))
1426 return html
1427 html += element.gethtml()
1428 return html
1430 class TaggedOutput(ContentsOutput):
1431 "Outputs an HTML tag surrounding the contents."
1433 tag = None
1434 breaklines = False
1435 empty = False
1437 def settag(self, tag, breaklines=False, empty=False):
1438 "Set the value for the tag and other attributes."
1439 self.tag = tag
1440 if breaklines:
1441 self.breaklines = breaklines
1442 if empty:
1443 self.empty = empty
1444 return self
1446 def setbreaklines(self, breaklines):
1447 "Set the value for breaklines."
1448 self.breaklines = breaklines
1449 return self
1451 def gethtml(self, container):
1452 "Return the HTML code."
1453 if self.empty:
1454 return [self.selfclosing(container)]
1455 html = [self.open(container)]
1456 html += ContentsOutput.gethtml(self, container)
1457 html.append(self.close(container))
1458 return html
1460 def open(self, container):
1461 "Get opening line."
1462 if not self.checktag():
1463 return ''
1464 open = '<' + self.tag + '>'
1465 if self.breaklines:
1466 return open + '\n'
1467 return open
1469 def close(self, container):
1470 "Get closing line."
1471 if not self.checktag():
1472 return ''
1473 close = '</' + self.tag.split()[0] + '>'
1474 if self.breaklines:
1475 return '\n' + close + '\n'
1476 return close
1478 def selfclosing(self, container):
1479 "Get self-closing line."
1480 if not self.checktag():
1481 return ''
1482 selfclosing = '<' + self.tag + '/>'
1483 if self.breaklines:
1484 return selfclosing + '\n'
1485 return selfclosing
1487 def checktag(self):
1488 "Check that the tag is valid."
1489 if not self.tag:
1490 Trace.error('No tag in ' + unicode(container))
1491 return False
1492 if self.tag == '':
1493 return False
1494 return True
1496 class StringOutput(ContainerOutput):
1497 "Returns a bare string as output"
1499 def gethtml(self, container):
1500 "Return a bare string"
1501 return [container.string]
1509 import sys
1510 import codecs
1513 class LineReader(object):
1514 "Reads a file line by line"
1516 def __init__(self, filename):
1517 if isinstance(filename, file):
1518 self.file = filename
1519 else:
1520 self.file = codecs.open(filename, 'rU', 'utf-8')
1521 self.linenumber = 1
1522 self.lastline = None
1523 self.current = None
1524 self.mustread = True
1525 self.depleted = False
1526 try:
1527 self.readline()
1528 except UnicodeDecodeError:
1529 # try compressed file
1530 import gzip
1531 self.file = gzip.open(filename, 'rb')
1532 self.readline()
1534 def setstart(self, firstline):
1535 "Set the first line to read."
1536 for i in range(firstline):
1537 self.file.readline()
1538 self.linenumber = firstline
1540 def setend(self, lastline):
1541 "Set the last line to read."
1542 self.lastline = lastline
1544 def currentline(self):
1545 "Get the current line"
1546 if self.mustread:
1547 self.readline()
1548 return self.current
1550 def nextline(self):
1551 "Go to next line"
1552 if self.depleted:
1553 Trace.fatal('Read beyond file end')
1554 self.mustread = True
1556 def readline(self):
1557 "Read a line from file"
1558 self.current = self.file.readline()
1559 if not isinstance(self.file, codecs.StreamReaderWriter):
1560 self.current = self.current.decode('utf-8')
1561 if len(self.current) == 0:
1562 self.depleted = True
1563 self.current = self.current.rstrip('\n\r')
1564 self.linenumber += 1
1565 self.mustread = False
1566 Trace.prefix = 'Line ' + unicode(self.linenumber) + ': '
1567 if self.linenumber % 1000 == 0:
1568 Trace.message('Parsing')
1570 def finished(self):
1571 "Find out if the file is finished"
1572 if self.lastline and self.linenumber == self.lastline:
1573 return True
1574 if self.mustread:
1575 self.readline()
1576 return self.depleted
1578 def close(self):
1579 self.file.close()
1581 class LineWriter(object):
1582 "Writes a file as a series of lists"
1584 def __init__(self, filename):
1585 if isinstance(filename, file):
1586 self.file = filename
1587 self.filename = None
1588 else:
1589 self.file = codecs.open(filename, 'w', "utf-8")
1590 self.filename = filename
1592 def write(self, strings):
1593 "Write a list of strings"
1594 for string in strings:
1595 if not isinstance(string, basestring):
1596 Trace.error('Not a string: ' + unicode(string) + ' in ' + unicode(strings))
1597 return
1598 self.writestring(string)
1600 def writestring(self, string):
1601 "Write a string"
1602 if self.file == sys.stdout:
1603 string = string.encode('utf-8')
1604 self.file.write(string)
1606 def writeline(self, line):
1607 "Write a line to file"
1608 self.writestring(line + '\n')
1610 def close(self):
1611 self.file.close()
1615 class Position(object):
1616 "A position in a text to parse"
1618 def __init__(self):
1619 self.endinglist = EndingList()
1621 def checkbytemark(self):
1622 "Check for a Unicode byte mark and skip it."
1623 if self.finished():
1624 return
1625 if ord(self.current()) == 0xfeff:
1626 self.skipcurrent()
1628 def skip(self, string):
1629 "Skip a string"
1630 Trace.error('Unimplemented skip()')
1632 def identifier(self):
1633 "Return an identifier for the current position."
1634 Trace.error('Unimplemented identifier()')
1635 return 'Error'
1637 def isout(self):
1638 "Find out if we are out of the position yet."
1639 Trace.error('Unimplemented isout()')
1640 return True
1642 def current(self):
1643 "Return the current character"
1644 Trace.error('Unimplemented current()')
1645 return ''
1647 def extract(self, length):
1648 "Extract the next string of the given length, or None if not enough text."
1649 Trace.error('Unimplemented extract()')
1650 return None
1652 def checkfor(self, string):
1653 "Check for a string at the given position."
1654 return string == self.extract(len(string))
1656 def checkforlower(self, string):
1657 "Check for a string in lower case."
1658 extracted = self.extract(len(string))
1659 if not extracted:
1660 return False
1661 return string.lower() == self.extract(len(string)).lower()
1663 def finished(self):
1664 "Find out if the current formula has finished"
1665 if self.isout():
1666 self.endinglist.checkpending()
1667 return True
1668 return self.endinglist.checkin(self)
1670 def skipcurrent(self):
1671 "Return the current character and skip it."
1672 current = self.current()
1673 self.skip(current)
1674 return current
1676 def next(self):
1677 "Advance the position and return the next character."
1678 self.skipcurrent()
1679 return self.current()
1681 def checkskip(self, string):
1682 "Check for a string at the given position; if there, skip it"
1683 if not self.checkfor(string):
1684 return False
1685 self.skip(string)
1686 return True
1688 def glob(self, currentcheck):
1689 "Glob a bit of text that satisfies a check"
1690 glob = ''
1691 while not self.finished() and currentcheck(self.current()):
1692 glob += self.current()
1693 self.skip(self.current())
1694 return glob
1696 def globalpha(self):
1697 "Glob a bit of alpha text"
1698 return self.glob(lambda current: current.isalpha())
1700 def globnumber(self):
1701 "Glob a row of digits."
1702 return self.glob(lambda current: current.isdigit())
1704 def checkidentifier(self):
1705 "Check if the current character belongs to an identifier."
1706 return self.isidentifier(self.current())
1708 def isidentifier(self, char):
1709 "Return if the given character is alphanumeric or _."
1710 if char.isalnum() or char == '_':
1711 return True
1712 return False
1714 def globidentifier(self):
1715 "Glob alphanumeric and _ symbols."
1716 return self.glob(lambda current: self.isidentifier(current))
1718 def skipspace(self):
1719 "Skip all whitespace at current position"
1720 return self.glob(lambda current: current.isspace())
1722 def globincluding(self, magicchar):
1723 "Glob a bit of text up to (including) the magic char."
1724 glob = self.glob(lambda current: current != magicchar) + magicchar
1725 self.skip(magicchar)
1726 return glob
1728 def globexcluding(self, magicchar):
1729 "Glob a bit of text up until (excluding) the magic char."
1730 return self.glob(lambda current: current != magicchar)
1732 def pushending(self, ending, optional = False):
1733 "Push a new ending to the bottom"
1734 self.endinglist.add(ending, optional)
1736 def popending(self, expected = None):
1737 "Pop the ending found at the current position"
1738 ending = self.endinglist.pop(self)
1739 if expected and expected != ending:
1740 Trace.error('Expected ending ' + expected + ', got ' + ending)
1741 self.skip(ending)
1742 return ending
1744 class TextPosition(Position):
1745 "A parse position based on a raw text."
1747 def __init__(self, text):
1748 "Create the position from some text."
1749 Position.__init__(self)
1750 self.pos = 0
1751 self.text = text
1752 self.checkbytemark()
1754 def skip(self, string):
1755 "Skip a string of characters."
1756 self.pos += len(string)
1758 def identifier(self):
1759 "Return a sample of the remaining text."
1760 length = 30
1761 if self.pos + length > len(self.text):
1762 length = len(self.text) - self.pos - 1
1763 return '*' + self.text[self.pos:self.pos + length]
1765 def isout(self):
1766 "Find out if we are out of the text yet."
1767 return self.pos >= len(self.text)
1769 def current(self):
1770 "Return the current character, assuming we are not out."
1771 return self.text[self.pos]
1773 def extract(self, length):
1774 "Extract the next string of the given length, or None if not enough text."
1775 if self.pos + length > len(self.text):
1776 return None
1777 return self.text[self.pos : self.pos + length]
1779 class FilePosition(Position):
1780 "A parse position based on an underlying file."
1782 def __init__(self, filename):
1783 "Create the position from a file."
1784 Position.__init__(self)
1785 self.reader = LineReader(filename)
1786 self.number = 1
1787 self.pos = 0
1788 self.checkbytemark()
1790 def skip(self, string):
1791 "Skip a string of characters."
1792 length = len(string)
1793 while self.pos + length > len(self.reader.currentline()):
1794 length -= len(self.reader.currentline()) - self.pos + 1
1795 self.nextline()
1796 self.pos += length
1798 def nextline(self):
1799 "Go to the next line."
1800 self.reader.nextline()
1801 self.number += 1
1802 self.pos = 0
1804 def identifier(self):
1805 "Return the current line and line number in the file."
1806 before = self.reader.currentline()[:self.pos - 1]
1807 after = self.reader.currentline()[self.pos:]
1808 return 'line ' + unicode(self.number) + ': ' + before + '*' + after
1810 def isout(self):
1811 "Find out if we are out of the text yet."
1812 if self.pos > len(self.reader.currentline()):
1813 if self.pos > len(self.reader.currentline()) + 1:
1814 Trace.error('Out of the line ' + self.reader.currentline() + ': ' + unicode(self.pos))
1815 self.nextline()
1816 return self.reader.finished()
1818 def current(self):
1819 "Return the current character, assuming we are not out."
1820 if self.pos == len(self.reader.currentline()):
1821 return '\n'
1822 if self.pos > len(self.reader.currentline()):
1823 Trace.error('Out of the line ' + self.reader.currentline() + ': ' + unicode(self.pos))
1824 return '*'
1825 return self.reader.currentline()[self.pos]
1827 def extract(self, length):
1828 "Extract the next string of the given length, or None if not enough text."
1829 if self.pos + length > len(self.reader.currentline()):
1830 return None
1831 return self.reader.currentline()[self.pos : self.pos + length]
1833 class EndingList(object):
1834 "A list of position endings"
1836 def __init__(self):
1837 self.endings = []
1839 def add(self, ending, optional):
1840 "Add a new ending to the list"
1841 self.endings.append(PositionEnding(ending, optional))
1843 def checkin(self, pos):
1844 "Search for an ending"
1845 if self.findending(pos):
1846 return True
1847 return False
1849 def pop(self, pos):
1850 "Remove the ending at the current position"
1851 if pos.isout():
1852 Trace.error('No ending out of bounds')
1853 return ''
1854 ending = self.findending(pos)
1855 if not ending:
1856 Trace.error('No ending at ' + pos.current())
1857 return ''
1858 for each in reversed(self.endings):
1859 self.endings.remove(each)
1860 if each == ending:
1861 return each.ending
1862 elif not each.optional:
1863 Trace.error('Removed non-optional ending ' + each)
1864 Trace.error('No endings left')
1865 return ''
1867 def findending(self, pos):
1868 "Find the ending at the current position"
1869 if len(self.endings) == 0:
1870 return None
1871 for index, ending in enumerate(reversed(self.endings)):
1872 if ending.checkin(pos):
1873 return ending
1874 if not ending.optional:
1875 return None
1876 return None
1878 def checkpending(self):
1879 "Check if there are any pending endings"
1880 if len(self.endings) != 0:
1881 Trace.error('Pending ' + unicode(self) + ' left open')
1883 def __unicode__(self):
1884 "Printable representation"
1885 string = 'endings ['
1886 for ending in self.endings:
1887 string += unicode(ending) + ','
1888 if len(self.endings) > 0:
1889 string = string[:-1]
1890 return string + ']'
1892 class PositionEnding(object):
1893 "An ending for a parsing position"
1895 def __init__(self, ending, optional):
1896 self.ending = ending
1897 self.optional = optional
1899 def checkin(self, pos):
1900 "Check for the ending"
1901 return pos.checkfor(self.ending)
1903 def __unicode__(self):
1904 "Printable representation"
1905 string = 'Ending ' + self.ending
1906 if self.optional:
1907 string += ' (optional)'
1908 return string
1912 class Container(object):
1913 "A container for text and objects in a lyx file"
1915 partkey = None
1916 parent = None
1917 begin = None
1919 def __init__(self):
1920 self.contents = list()
1922 def process(self):
1923 "Process contents"
1924 pass
1926 def gethtml(self):
1927 "Get the resulting HTML"
1928 html = self.output.gethtml(self)
1929 if isinstance(html, basestring):
1930 Trace.error('Raw string ' + html)
1931 html = [html]
1932 return self.escapeall(html)
1934 def escapeall(self, lines):
1935 "Escape all lines in an array according to the output options."
1936 result = []
1937 for line in lines:
1938 if Options.html:
1939 line = self.escape(line, EscapeConfig.html)
1940 if Options.iso885915:
1941 line = self.escape(line, EscapeConfig.iso885915)
1942 line = self.escapeentities(line)
1943 elif not Options.unicode:
1944 line = self.escape(line, EscapeConfig.nonunicode)
1945 result.append(line)
1946 return result
1948 def escape(self, line, replacements = EscapeConfig.entities):
1949 "Escape a line with replacements from a map"
1950 pieces = replacements.keys()
1951 # do them in order
1952 pieces.sort()
1953 for piece in pieces:
1954 if piece in line:
1955 line = line.replace(piece, replacements[piece])
1956 return line
1958 def escapeentities(self, line):
1959 "Escape all Unicode characters to HTML entities."
1960 result = ''
1961 pos = TextPosition(line)
1962 while not pos.finished():
1963 if ord(pos.current()) > 128:
1964 codepoint = hex(ord(pos.current()))
1965 if codepoint == '0xd835':
1966 codepoint = hex(ord(pos.next()) + 0xf800)
1967 result += '&#' + codepoint[1:] + ';'
1968 else:
1969 result += pos.current()
1970 pos.skipcurrent()
1971 return result
1973 def searchall(self, type):
1974 "Search for all embedded containers of a given type"
1975 list = []
1976 self.searchprocess(type, lambda container: list.append(container))
1977 return list
1979 def searchremove(self, type):
1980 "Search for all containers of a type and remove them"
1981 list = self.searchall(type)
1982 for container in list:
1983 container.parent.contents.remove(container)
1984 return list
1986 def searchprocess(self, type, process):
1987 "Search for elements of a given type and process them"
1988 self.locateprocess(lambda container: isinstance(container, type), process)
1990 def locateprocess(self, locate, process):
1991 "Search for all embedded containers and process them"
1992 for container in self.contents:
1993 container.locateprocess(locate, process)
1994 if locate(container):
1995 process(container)
1997 def recursivesearch(self, locate, recursive, process):
1998 "Perform a recursive search in the container."
1999 for container in self.contents:
2000 if recursive(container):
2001 container.recursivesearch(locate, recursive, process)
2002 if locate(container):
2003 process(container)
2005 def extracttext(self):
2006 "Extract all text from allowed containers."
2007 result = ''
2008 constants = ContainerExtractor(ContainerConfig.extracttext).extract(self)
2009 for constant in constants:
2010 result += constant.string
2011 return result
2013 def group(self, index, group, isingroup):
2014 "Group some adjoining elements into a group"
2015 if index >= len(self.contents):
2016 return
2017 if hasattr(self.contents[index], 'grouped'):
2018 return
2019 while index < len(self.contents) and isingroup(self.contents[index]):
2020 self.contents[index].grouped = True
2021 group.contents.append(self.contents[index])
2022 self.contents.pop(index)
2023 self.contents.insert(index, group)
2025 def remove(self, index):
2026 "Remove a container but leave its contents"
2027 container = self.contents[index]
2028 self.contents.pop(index)
2029 while len(container.contents) > 0:
2030 self.contents.insert(index, container.contents.pop())
2032 def tree(self, level = 0):
2033 "Show in a tree"
2034 Trace.debug(" " * level + unicode(self))
2035 for container in self.contents:
2036 container.tree(level + 1)
2038 def getparameter(self, name):
2039 "Get the value of a parameter, if present."
2040 if not name in self.parameters:
2041 return None
2042 return self.parameters[name]
2044 def getparameterlist(self, name):
2045 "Get the value of a comma-separated parameter as a list."
2046 paramtext = self.getparameter(name)
2047 if not paramtext:
2048 return []
2049 return paramtext.split(',')
2051 def hasemptyoutput(self):
2052 "Check if the parent's output is empty."
2053 current = self.parent
2054 while current:
2055 if current.output.isempty():
2056 return True
2057 current = current.parent
2058 return False
2060 def __unicode__(self):
2061 "Get a description"
2062 if not self.begin:
2063 return self.__class__.__name__
2064 return self.__class__.__name__ + '@' + unicode(self.begin)
2066 class BlackBox(Container):
2067 "A container that does not output anything"
2069 def __init__(self):
2070 self.parser = LoneCommand()
2071 self.output = EmptyOutput()
2072 self.contents = []
2074 class LyXFormat(BlackBox):
2075 "Read the lyxformat command"
2077 def process(self):
2078 "Show warning if version < 276"
2079 version = int(self.header[1])
2080 if version < 276:
2081 Trace.error('Warning: unsupported old format version ' + str(version))
2082 if version > int(GeneralConfig.version['lyxformat']):
2083 Trace.error('Warning: unsupported new format version ' + str(version))
2085 class StringContainer(Container):
2086 "A container for a single string"
2088 parsed = None
2090 def __init__(self):
2091 self.parser = StringParser()
2092 self.output = StringOutput()
2093 self.string = ''
2095 def process(self):
2096 "Replace special chars from the contents."
2097 if self.parsed:
2098 self.string = self.replacespecial(self.parsed)
2099 self.parsed = None
2101 def replacespecial(self, line):
2102 "Replace all special chars from a line"
2103 replaced = self.escape(line, EscapeConfig.entities)
2104 replaced = self.changeline(replaced)
2105 if ContainerConfig.string['startcommand'] in replaced and len(replaced) > 1:
2106 # unprocessed commands
2107 if self.begin:
2108 message = 'Unknown command at ' + unicode(self.begin) + ': '
2109 else:
2110 message = 'Unknown command: '
2111 Trace.error(message + replaced.strip())
2112 return replaced
2114 def changeline(self, line):
2115 line = self.escape(line, EscapeConfig.chars)
2116 if not ContainerConfig.string['startcommand'] in line:
2117 return line
2118 line = self.escape(line, EscapeConfig.commands)
2119 return line
2121 def extracttext(self):
2122 "Return all text."
2123 return self.string
2125 def __unicode__(self):
2126 "Return a printable representation."
2127 result = 'StringContainer'
2128 if self.begin:
2129 result += '@' + unicode(self.begin)
2130 ellipsis = '...'
2131 if len(self.string.strip()) <= 15:
2132 ellipsis = ''
2133 return result + ' (' + self.string.strip()[:15] + ellipsis + ')'
2135 class Constant(StringContainer):
2136 "A constant string"
2138 def __init__(self, text):
2139 self.contents = []
2140 self.string = text
2141 self.output = StringOutput()
2143 def __unicode__(self):
2144 return 'Constant: ' + self.string
2146 class TaggedText(Container):
2147 "Text inside a tag"
2149 output = None
2151 def __init__(self):
2152 self.parser = TextParser(self)
2153 self.output = TaggedOutput()
2155 def complete(self, contents, tag, breaklines=False):
2156 "Complete the tagged text and return it"
2157 self.contents = contents
2158 self.output.tag = tag
2159 self.output.breaklines = breaklines
2160 return self
2162 def constant(self, text, tag, breaklines=False):
2163 "Complete the tagged text with a constant"
2164 constant = Constant(text)
2165 return self.complete([constant], tag, breaklines)
2167 def __unicode__(self):
2168 "Return a printable representation."
2169 if not hasattr(self.output, 'tag'):
2170 return 'Emtpy tagged text'
2171 if not self.output.tag:
2172 return 'Tagged <unknown tag>'
2173 return 'Tagged <' + self.output.tag + '>'
2180 class FormulaParser(Parser):
2181 "Parses a formula"
2183 def parseheader(self, reader):
2184 "See if the formula is inlined"
2185 self.begin = reader.linenumber + 1
2186 if reader.currentline().find(FormulaConfig.starts['simple']) > 0:
2187 return ['inline']
2188 if reader.currentline().find(FormulaConfig.starts['complex']) > 0:
2189 return ['block']
2190 if reader.currentline().find(FormulaConfig.starts['unnumbered']) > 0:
2191 return ['block']
2192 return ['numbered']
2194 def parse(self, reader):
2195 "Parse the formula until the end"
2196 formula = self.parseformula(reader)
2197 while not reader.currentline().startswith(self.ending):
2198 stripped = reader.currentline().strip()
2199 if len(stripped) > 0:
2200 Trace.error('Unparsed formula line ' + stripped)
2201 reader.nextline()
2202 reader.nextline()
2203 return formula
2205 def parseformula(self, reader):
2206 "Parse the formula contents"
2207 simple = FormulaConfig.starts['simple']
2208 if simple in reader.currentline():
2209 rest = reader.currentline().split(simple, 1)[1]
2210 if simple in rest:
2211 # formula is $...$
2212 return self.parsesingleliner(reader, simple, simple)
2213 # formula is multiline $...$
2214 return self.parsemultiliner(reader, simple, simple)
2215 if FormulaConfig.starts['complex'] in reader.currentline():
2216 # formula of the form \[...\]
2217 return self.parsemultiliner(reader, FormulaConfig.starts['complex'],
2218 FormulaConfig.endings['complex'])
2219 beginbefore = FormulaConfig.starts['beginbefore']
2220 beginafter = FormulaConfig.starts['beginafter']
2221 if beginbefore in reader.currentline():
2222 if reader.currentline().strip().endswith(beginafter):
2223 current = reader.currentline().strip()
2224 endsplit = current.split(beginbefore)[1].split(beginafter)
2225 startpiece = beginbefore + endsplit[0] + beginafter
2226 endbefore = FormulaConfig.endings['endbefore']
2227 endafter = FormulaConfig.endings['endafter']
2228 endpiece = endbefore + endsplit[0] + endafter
2229 return startpiece + self.parsemultiliner(reader, startpiece, endpiece) + endpiece
2230 Trace.error('Missing ' + beginafter + ' in ' + reader.currentline())
2231 return ''
2232 begincommand = FormulaConfig.starts['command']
2233 beginbracket = FormulaConfig.starts['bracket']
2234 if begincommand in reader.currentline() and beginbracket in reader.currentline():
2235 endbracket = FormulaConfig.endings['bracket']
2236 return self.parsemultiliner(reader, beginbracket, endbracket)
2237 Trace.error('Formula beginning ' + reader.currentline() + ' is unknown')
2238 return ''
2240 def parsesingleliner(self, reader, start, ending):
2241 "Parse a formula in one line"
2242 line = reader.currentline().strip()
2243 if not start in line:
2244 Trace.error('Line ' + line + ' does not contain formula start ' + start)
2245 return ''
2246 if not line.endswith(ending):
2247 Trace.error('Formula ' + line + ' does not end with ' + ending)
2248 return ''
2249 index = line.index(start)
2250 rest = line[index + len(start):-len(ending)]
2251 reader.nextline()
2252 return rest
2254 def parsemultiliner(self, reader, start, ending):
2255 "Parse a formula in multiple lines"
2256 formula = ''
2257 line = reader.currentline()
2258 if not start in line:
2259 Trace.error('Line ' + line.strip() + ' does not contain formula start ' + start)
2260 return ''
2261 index = line.index(start)
2262 line = line[index + len(start):].strip()
2263 while not line.endswith(ending):
2264 formula += line + '\n'
2265 reader.nextline()
2266 line = reader.currentline()
2267 formula += line[:-len(ending)]
2268 reader.nextline()
2269 return formula
2271 class MacroParser(FormulaParser):
2272 "A parser for a formula macro."
2274 def parseheader(self, reader):
2275 "See if the formula is inlined"
2276 self.begin = reader.linenumber + 1
2277 return ['inline']
2279 def parse(self, reader):
2280 "Parse the formula until the end"
2281 formula = self.parsemultiliner(reader, self.parent.start, self.ending)
2282 reader.nextline()
2283 return formula
2293 class FormulaBit(Container):
2294 "A bit of a formula"
2296 def __init__(self):
2297 # type can be 'alpha', 'number', 'font'
2298 self.type = None
2299 self.original = ''
2300 self.contents = []
2301 self.output = ContentsOutput()
2303 def setfactory(self, factory):
2304 "Set the internal formula factory."
2305 self.factory = factory
2306 return self
2308 def add(self, bit):
2309 "Add any kind of formula bit already processed"
2310 self.contents.append(bit)
2311 self.original += bit.original
2312 bit.parent = self
2314 def skiporiginal(self, string, pos):
2315 "Skip a string and add it to the original formula"
2316 self.original += string
2317 if not pos.checkskip(string):
2318 Trace.error('String ' + string + ' not at ' + pos.identifier())
2320 def clone(self):
2321 "Return a copy of itself."
2322 return self.factory.parseformula(self.original)
2324 def __unicode__(self):
2325 "Get a string representation"
2326 return self.__class__.__name__ + ' read in ' + self.original
2328 class TaggedBit(FormulaBit):
2329 "A tagged string in a formula"
2331 def constant(self, constant, tag):
2332 "Set the constant and the tag"
2333 self.output = TaggedOutput().settag(tag)
2334 self.add(FormulaConstant(constant))
2335 return self
2337 def complete(self, contents, tag):
2338 "Set the constant and the tag"
2339 self.contents = contents
2340 self.output = TaggedOutput().settag(tag)
2341 return self
2343 class FormulaConstant(Constant):
2344 "A constant string in a formula"
2346 def __init__(self, string):
2347 "Set the constant string"
2348 Constant.__init__(self, string)
2349 self.original = string
2350 self.type = None
2352 def clone(self):
2353 "Return a copy of itself."
2354 return FormulaConstant(self.original)
2356 def __unicode__(self):
2357 "Return a printable representation."
2358 return 'Formula constant: ' + self.string
2360 class RawText(FormulaBit):
2361 "A bit of text inside a formula"
2363 def detect(self, pos):
2364 "Detect a bit of raw text"
2365 return pos.current().isalpha()
2367 def parsebit(self, pos):
2368 "Parse alphabetic text"
2369 alpha = pos.globalpha()
2370 self.add(FormulaConstant(alpha))
2371 self.type = 'alpha'
2373 class FormulaSymbol(FormulaBit):
2374 "A symbol inside a formula"
2376 modified = FormulaConfig.modified
2377 unmodified = FormulaConfig.unmodified['characters']
2379 def detect(self, pos):
2380 "Detect a symbol"
2381 if pos.current() in FormulaSymbol.unmodified:
2382 return True
2383 if pos.current() in FormulaSymbol.modified:
2384 return True
2385 return False
2387 def parsebit(self, pos):
2388 "Parse the symbol"
2389 if pos.current() in FormulaSymbol.unmodified:
2390 self.addsymbol(pos.current(), pos)
2391 return
2392 if pos.current() in FormulaSymbol.modified:
2393 self.addsymbol(FormulaSymbol.modified[pos.current()], pos)
2394 return
2395 Trace.error('Symbol ' + pos.current() + ' not found')
2397 def addsymbol(self, symbol, pos):
2398 "Add a symbol"
2399 self.skiporiginal(pos.current(), pos)
2400 self.contents.append(FormulaConstant(symbol))
2402 class FormulaNumber(FormulaBit):
2403 "A string of digits in a formula"
2405 def detect(self, pos):
2406 "Detect a digit"
2407 return pos.current().isdigit()
2409 def parsebit(self, pos):
2410 "Parse a bunch of digits"
2411 digits = pos.glob(lambda current: current.isdigit())
2412 self.add(FormulaConstant(digits))
2413 self.type = 'number'
2415 class Comment(FormulaBit):
2416 "A LaTeX comment: % to the end of the line."
2418 start = FormulaConfig.starts['comment']
2420 def detect(self, pos):
2421 "Detect the %."
2422 return pos.current() == self.start
2424 def parsebit(self, pos):
2425 "Parse to the end of the line."
2426 self.original += pos.globincluding('\n')
2428 class WhiteSpace(FormulaBit):
2429 "Some white space inside a formula."
2431 def detect(self, pos):
2432 "Detect the white space."
2433 return pos.current().isspace()
2435 def parsebit(self, pos):
2436 "Parse all whitespace."
2437 self.original += pos.skipspace()
2439 class Bracket(FormulaBit):
2440 "A {} bracket inside a formula"
2442 start = FormulaConfig.starts['bracket']
2443 ending = FormulaConfig.endings['bracket']
2445 def __init__(self):
2446 "Create a (possibly literal) new bracket"
2447 FormulaBit.__init__(self)
2448 self.inner = None
2450 def detect(self, pos):
2451 "Detect the start of a bracket"
2452 return pos.checkfor(self.start)
2454 def parsebit(self, pos):
2455 "Parse the bracket"
2456 self.parsecomplete(pos, self.innerformula)
2457 return self
2459 def parsetext(self, pos):
2460 "Parse a text bracket"
2461 self.parsecomplete(pos, self.innertext)
2462 return self
2464 def parseliteral(self, pos):
2465 "Parse a literal bracket"
2466 self.parsecomplete(pos, self.innerliteral)
2467 return self
2469 def parsecomplete(self, pos, innerparser):
2470 "Parse the start and end marks"
2471 if not pos.checkfor(self.start):
2472 Trace.error('Bracket should start with ' + self.start + ' at ' + pos.identifier())
2473 return None
2474 self.skiporiginal(self.start, pos)
2475 pos.pushending(self.ending)
2476 innerparser(pos)
2477 self.original += pos.popending(self.ending)
2479 def innerformula(self, pos):
2480 "Parse a whole formula inside the bracket"
2481 while self.factory.detectany(pos):
2482 self.add(self.factory.parseany(pos))
2483 if pos.finished():
2484 return
2485 if pos.current() != self.ending:
2486 Trace.error('No formula in bracket at ' + pos.identifier())
2487 return
2489 def innertext(self, pos):
2490 "Parse some text inside the bracket, following textual rules."
2491 specialchars = FormulaConfig.symbolfunctions.keys()
2492 specialchars.append(FormulaConfig.starts['command'])
2493 specialchars.append(FormulaConfig.starts['bracket'])
2494 specialchars.append(Comment.start)
2495 while not pos.finished():
2496 if pos.current() in specialchars:
2497 if self.factory.detectany(pos):
2498 self.add(self.factory.parseany(pos))
2499 pos.checkskip(' ')
2500 else:
2501 self.add(FormulaConstant(pos.skipcurrent()))
2503 def innerliteral(self, pos):
2504 "Parse a literal inside the bracket, which cannot generate html"
2505 self.literal = ''
2506 while not pos.current() == self.ending:
2507 if pos.current() == self.start:
2508 self.parseliteral(pos)
2509 else:
2510 self.literal += pos.skipcurrent()
2511 self.original += self.literal
2513 class SquareBracket(Bracket):
2514 "A [] bracket inside a formula"
2516 start = FormulaConfig.starts['squarebracket']
2517 ending = FormulaConfig.endings['squarebracket']
2522 class FormulaProcessor(object):
2523 "A processor specifically for formulas."
2525 def process(self, bit):
2526 "Process the contents of every formula bit, recursively."
2527 self.processcontents(bit)
2528 self.processlimits(bit)
2529 self.traversewhole(bit)
2531 def processcontents(self, bit):
2532 "Process the contents of a formula bit."
2533 if not isinstance(bit, FormulaBit):
2534 return
2535 bit.process()
2536 for element in bit.contents:
2537 self.processcontents(element)
2539 def processlimits(self, bit):
2540 "Process any limits in a formula bit."
2541 if not isinstance(bit, FormulaBit):
2542 return
2543 for index, element in enumerate(bit.contents):
2544 self.checklimited(bit.contents, index)
2545 self.processlimits(element)
2547 def checklimited(self, contents, index):
2548 "Check for a command with limits"
2549 bit = contents[index]
2550 if not hasattr(bit, 'command'):
2551 return
2552 if not bit.command in FormulaConfig.limits['commands']:
2553 return
2554 limits = self.findlimits(contents, index + 1)
2555 limits.reverse()
2556 if len(limits) == 0:
2557 return
2558 tagged = TaggedBit().complete(limits, 'span class="limits"')
2559 contents.insert(index + 1, tagged)
2561 def findlimits(self, contents, index):
2562 "Find the limits for the command"
2563 limits = []
2564 while index < len(contents):
2565 if not self.checklimits(contents, index):
2566 return limits
2567 limits.append(contents[index])
2568 del contents[index]
2569 return limits
2571 def checklimits(self, contents, index):
2572 "Check for a command making the limits"
2573 bit = contents[index]
2574 if not hasattr(bit, 'command'):
2575 return
2576 if not bit.command in FormulaConfig.limits['operands']:
2577 return False
2578 bit.output.tag += ' class="bigsymbol"'
2579 return True
2581 def traversewhole(self, formula):
2582 "Traverse over the contents to alter variables and space units."
2583 last = None
2584 for bit, contents in self.traverse(formula):
2585 if bit.type == 'alpha':
2586 self.italicize(bit, contents)
2587 elif bit.type == 'font' and last and last.type == 'number':
2588 bit.contents.insert(0, FormulaConstant(u' '))
2589 last = bit
2591 def traverse(self, bit):
2592 "Traverse a formula and yield a flattened structure of (bit, list) pairs."
2593 for element in bit.contents:
2594 if hasattr(element, 'type') and element.type:
2595 yield (element, bit.contents)
2596 elif isinstance(element, FormulaBit):
2597 for pair in self.traverse(element):
2598 yield pair
2600 def italicize(self, bit, contents):
2601 "Italicize the given bit of text."
2602 index = contents.index(bit)
2603 contents[index] = TaggedBit().complete([bit], 'i')
2608 class Formula(Container):
2609 "A LaTeX formula"
2611 def __init__(self):
2612 self.parser = FormulaParser()
2613 self.output = TaggedOutput().settag('span class="formula"')
2615 def process(self):
2616 "Convert the formula to tags"
2617 if self.header[0] != 'inline':
2618 self.output.settag('div class="formula"', True)
2619 if Options.jsmath:
2620 if self.header[0] != 'inline':
2621 self.output = TaggedOutput().settag('div class="math"')
2622 else:
2623 self.output = TaggedOutput().settag('span class="math"')
2624 self.contents = [Constant(self.parsed)]
2625 return
2626 if Options.mathjax:
2627 self.output.tag = 'span class="MathJax_Preview"'
2628 tag = 'script type="math/tex'
2629 if self.header[0] != 'inline':
2630 tag += ';mode=display'
2631 self.contents = [TaggedText().constant(self.parsed, tag + '"', True)]
2632 return
2633 whole = FormulaFactory().parseformula(self.parsed)
2634 FormulaProcessor().process(whole)
2635 whole.parent = self
2636 self.contents = [whole]
2638 def __unicode__(self):
2639 "Return a printable representation."
2640 if self.partkey and self.partkey.number:
2641 return 'Formula (' + self.partkey.number + ')'
2642 return 'Unnumbered formula'
2644 class WholeFormula(FormulaBit):
2645 "Parse a whole formula"
2647 def detect(self, pos):
2648 "Check in the factory"
2649 return self.factory.detectany(pos)
2651 def parsebit(self, pos):
2652 "Parse with any formula bit"
2653 while self.factory.detectany(pos):
2654 bit = self.factory.parseany(pos)
2655 #Trace.debug(bit.original + ' -> ' + unicode(bit.gethtml()))
2656 self.add(bit)
2658 class FormulaFactory(object):
2659 "Construct bits of formula"
2661 # bit types will be appended later
2662 types = [FormulaSymbol, RawText, FormulaNumber, Bracket]
2663 ignoredtypes = [Comment, WhiteSpace]
2664 defining = False
2666 def __init__(self):
2667 "Initialize the map of instances."
2668 self.instances = dict()
2670 def detectany(self, pos):
2671 "Detect if there is a next bit"
2672 if pos.finished():
2673 return False
2674 for type in FormulaFactory.types:
2675 if self.detecttype(type, pos):
2676 return True
2677 return False
2679 def detecttype(self, type, pos):
2680 "Detect a bit of a given type."
2681 self.clearignored(pos)
2682 if pos.finished():
2683 return False
2684 return self.instance(type).detect(pos)
2686 def instance(self, type):
2687 "Get an instance of the given type."
2688 if not type in self.instances or not self.instances[type]:
2689 self.instances[type] = self.create(type)
2690 return self.instances[type]
2692 def create(self, type):
2693 "Create a new formula bit of the given type."
2694 return Cloner.create(type).setfactory(self)
2696 def clearignored(self, pos):
2697 "Clear all ignored types."
2698 while not pos.finished():
2699 if not self.clearany(pos):
2700 return
2702 def clearany(self, pos):
2703 "Cleary any ignored type."
2704 for type in self.ignoredtypes:
2705 if self.instance(type).detect(pos):
2706 self.parsetype(type, pos)
2707 return True
2708 return False
2710 def parseany(self, pos):
2711 "Parse any formula bit at the current location."
2712 for type in FormulaFactory.types:
2713 if self.detecttype(type, pos):
2714 return self.parsetype(type, pos)
2715 Trace.error('Unrecognized formula at ' + pos.identifier())
2716 return FormulaConstant(pos.skipcurrent())
2718 def parsetype(self, type, pos):
2719 "Parse the given type and return it."
2720 bit = self.instance(type)
2721 self.instances[type] = None
2722 returnedbit = bit.parsebit(pos)
2723 if returnedbit:
2724 return returnedbit.setfactory(self)
2725 return bit
2727 def parseformula(self, formula):
2728 "Parse a string of text that contains a whole formula."
2729 pos = TextPosition(formula)
2730 whole = self.create(WholeFormula)
2731 if whole.detect(pos):
2732 whole.parsebit(pos)
2733 return whole
2734 # no formula found
2735 if not pos.finished():
2736 Trace.error('Unknown formula at: ' + pos.identifier())
2737 whole.add(TaggedBit().constant(formula, 'span class="unknown"'))
2738 return whole
2743 import unicodedata
2756 import gettext
2762 class DocumentParameters(object):
2763 "Global parameters for the document."
2765 pdftitle = None
2766 indentstandard = False
2767 tocdepth = 10
2768 startinglevel = 0
2769 maxdepth = 10
2770 language = None
2771 bibliography = None
2772 outputchanges = False
2776 class Translator(object):
2777 "Reads the configuration file and tries to find a translation."
2778 "Otherwise falls back to the messages in the config file."
2780 instance = None
2782 def translate(cls, key):
2783 "Get the translated message for a key."
2784 return cls.instance.getmessage(key)
2786 translate = classmethod(translate)
2788 def __init__(self):
2789 self.translation = None
2790 self.first = True
2792 def findtranslation(self):
2793 "Find the translation for the document language."
2794 self.langcodes = None
2795 if not DocumentParameters.language:
2796 Trace.error('No language in document')
2797 return
2798 if not DocumentParameters.language in TranslationConfig.languages:
2799 Trace.error('Unknown language ' + DocumentParameters.language)
2800 return
2801 if TranslationConfig.languages[DocumentParameters.language] == 'en':
2802 return
2803 langcodes = [TranslationConfig.languages[DocumentParameters.language]]
2804 try:
2805 self.translation = gettext.translation('elyxer', None, langcodes)
2806 except IOError:
2807 Trace.error('No translation for ' + unicode(langcodes))
2809 def getmessage(self, key):
2810 "Get the translated message for the given key."
2811 if self.first:
2812 self.findtranslation()
2813 self.first = False
2814 message = self.getuntranslated(key)
2815 if not self.translation:
2816 return message
2817 try:
2818 message = self.translation.ugettext(message)
2819 except IOError:
2820 pass
2821 return message
2823 def getuntranslated(self, key):
2824 "Get the untranslated message."
2825 if not key in TranslationConfig.constants:
2826 Trace.error('Cannot translate ' + key)
2827 return key
2828 return TranslationConfig.constants[key]
2830 Translator.instance = Translator()
2834 class NumberCounter(object):
2835 "A counter for numbers (by default)."
2836 "The type can be changed to return letters, roman numbers..."
2838 name = None
2839 value = None
2840 mode = None
2841 master = None
2843 letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
2844 symbols = NumberingConfig.sequence['symbols']
2845 romannumerals = [
2846 ('M', 1000), ('CM', 900), ('D', 500), ('CD', 400), ('C', 100),
2847 ('XC', 90), ('L', 50), ('XL', 40), ('X', 10), ('IX', 9), ('V', 5),
2848 ('IV', 4), ('I', 1)
2851 def __init__(self, name):
2852 "Give a name to the counter."
2853 self.name = name
2855 def setmode(self, mode):
2856 "Set the counter mode. Can be changed at runtime."
2857 self.mode = mode
2858 return self
2860 def init(self, value):
2861 "Set an initial value."
2862 self.value = value
2864 def increase(self):
2865 "Increase the counter value and return the counter."
2866 if not self.value:
2867 self.value = 0
2868 self.value += 1
2869 return self
2871 def gettext(self):
2872 "Get the next value as a text string."
2873 return unicode(self.value)
2875 def getletter(self):
2876 "Get the next value as a letter."
2877 return self.getsequence(self.letters)
2879 def getsymbol(self):
2880 "Get the next value as a symbol."
2881 return self.getsequence(self.symbols)
2883 def getsequence(self, sequence):
2884 "Get the next value from a sequence."
2885 return sequence[(self.value - 1) % len(sequence)]
2887 def getroman(self):
2888 "Get the next value as a roman number."
2889 result = ''
2890 number = self.value
2891 for numeral, value in self.romannumerals:
2892 if number >= value:
2893 result += numeral * (number / value)
2894 number = number % value
2895 return result
2897 def getvalue(self):
2898 "Get the current value as configured in the current mode."
2899 if not self.mode or self.mode in ['text', '1']:
2900 return self.gettext()
2901 if self.mode == 'A':
2902 return self.getletter()
2903 if self.mode == 'a':
2904 return self.getletter().lower()
2905 if self.mode == 'I':
2906 return self.getroman()
2907 if self.mode == '*':
2908 return self.getsymbol()
2909 Trace.error('Unknown counter mode ' + self.mode)
2910 return self.gettext()
2912 def getnext(self):
2913 "Get the next value as configured: increase() and getvalue()."
2914 return self.increase().getvalue()
2916 def reset(self):
2917 "Reset the counter."
2918 self.value = 0
2920 def __unicode__(self):
2921 "Return a printable representation."
2922 result = 'Counter ' + self.name
2923 if self.mode:
2924 result += ' in mode ' + self.mode
2925 return result
2927 class DependentCounter(NumberCounter):
2928 "A counter which depends on another one (the master)."
2930 def setmaster(self, master):
2931 "Set the master counter."
2932 self.master = master
2933 self.last = self.master.getvalue()
2934 return self
2936 def increase(self):
2937 "Increase or, if the master counter has changed, restart."
2938 if self.last != self.master.getvalue():
2939 self.reset()
2940 NumberCounter.increase(self)
2941 self.last = self.master.getvalue()
2942 return self
2944 def getvalue(self):
2945 "Get the value of the combined counter: master.dependent."
2946 return self.master.getvalue() + '.' + NumberCounter.getvalue(self)
2948 class NumberGenerator(object):
2949 "A number generator for unique sequences and hierarchical structures. Used in:"
2950 " * ordered part numbers: Chapter 3, Section 5.3."
2951 " * unique part numbers: Footnote 15, Bibliography cite [15]."
2952 " * chaptered part numbers: Figure 3.15, Equation (8.3)."
2953 " * unique roman part numbers: Part I, Book IV."
2955 chaptered = None
2956 generator = None
2958 romanlayouts = [x.lower() for x in NumberingConfig.layouts['roman']]
2959 orderedlayouts = [x.lower() for x in NumberingConfig.layouts['ordered']]
2961 counters = dict()
2962 appendix = None
2964 def deasterisk(self, type):
2965 "Remove the possible asterisk in a layout type."
2966 return type.replace('*', '')
2968 def isunique(self, type):
2969 "Find out if the layout type corresponds to a unique part."
2970 return self.isroman(type)
2972 def isroman(self, type):
2973 "Find out if the layout type should have roman numeration."
2974 return self.deasterisk(type).lower() in self.romanlayouts
2976 def isinordered(self, type):
2977 "Find out if the layout type corresponds to an (un)ordered part."
2978 return self.deasterisk(type).lower() in self.orderedlayouts
2980 def isnumbered(self, type):
2981 "Find out if the type for a layout corresponds to a numbered layout."
2982 if '*' in type:
2983 return False
2984 if self.isroman(type):
2985 return True
2986 if not self.isinordered(type):
2987 return False
2988 if self.getlevel(type) > DocumentParameters.maxdepth:
2989 return False
2990 return True
2992 def isunordered(self, type):
2993 "Find out if the type contains an asterisk, basically."
2994 return '*' in type
2996 def getlevel(self, type):
2997 "Get the level that corresponds to a layout type."
2998 if self.isunique(type):
2999 return 0
3000 if not self.isinordered(type):
3001 Trace.error('Unknown layout type ' + type)
3002 return 0
3003 type = self.deasterisk(type).lower()
3004 level = self.orderedlayouts.index(type) + 1
3005 return level - DocumentParameters.startinglevel
3007 def getparttype(self, type):
3008 "Obtain the type for the part: without the asterisk, "
3009 "and switched to Appendix if necessary."
3010 if NumberGenerator.appendix and self.getlevel(type) == 1:
3011 return 'Appendix'
3012 return self.deasterisk(type)
3014 def generate(self, type):
3015 "Generate a number for a layout type."
3016 "Unique part types such as Part or Book generate roman numbers: Part I."
3017 "Ordered part types return dot-separated tuples: Chapter 5, Subsection 2.3.5."
3018 "Everything else generates unique numbers: Bibliography [1]."
3019 "Each invocation results in a new number."
3020 return self.getcounter(type).getnext()
3022 def getcounter(self, type):
3023 "Get the counter for the given type."
3024 type = type.lower()
3025 if not type in self.counters:
3026 self.counters[type] = self.create(type)
3027 return self.counters[type]
3029 def create(self, type):
3030 "Create a counter for the given type."
3031 if self.isnumbered(type) and self.getlevel(type) > 1:
3032 index = self.orderedlayouts.index(type)
3033 above = self.orderedlayouts[index - 1]
3034 master = self.getcounter(above)
3035 return self.createdependent(type, master)
3036 counter = NumberCounter(type)
3037 if self.isroman(type):
3038 counter.setmode('I')
3039 return counter
3041 def getdependentcounter(self, type, master):
3042 "Get (or create) a counter of the given type that depends on another."
3043 if not type in self.counters or not self.counters[type].master:
3044 self.counters[type] = self.createdependent(type, master)
3045 return self.counters[type]
3047 def createdependent(self, type, master):
3048 "Create a dependent counter given the master."
3049 return DependentCounter(type).setmaster(master)
3051 def startappendix(self):
3052 "Start appendices here."
3053 firsttype = self.orderedlayouts[DocumentParameters.startinglevel]
3054 counter = self.getcounter(firsttype)
3055 counter.setmode('A').reset()
3056 NumberGenerator.appendix = True
3058 class ChapteredGenerator(NumberGenerator):
3059 "Generate chaptered numbers, as in Chapter.Number."
3060 "Used in equations, figures: Equation (5.3), figure 8.15."
3062 def generate(self, type):
3063 "Generate a number which goes with first-level numbers (chapters). "
3064 "For the article classes a unique number is generated."
3065 if DocumentParameters.startinglevel > 0:
3066 return NumberGenerator.generator.generate(type)
3067 chapter = self.getcounter('Chapter')
3068 return self.getdependentcounter(type, chapter).getnext()
3071 NumberGenerator.chaptered = ChapteredGenerator()
3072 NumberGenerator.generator = NumberGenerator()
3079 class ContainerSize(object):
3080 "The size of a container."
3082 width = None
3083 height = None
3084 maxwidth = None
3085 maxheight = None
3086 scale = None
3088 def set(self, width = None, height = None):
3089 "Set the proper size with width and height."
3090 self.setvalue('width', width)
3091 self.setvalue('height', height)
3092 return self
3094 def setmax(self, maxwidth = None, maxheight = None):
3095 "Set max width and/or height."
3096 self.setvalue('maxwidth', maxwidth)
3097 self.setvalue('maxheight', maxheight)
3098 return self
3100 def readparameters(self, container):
3101 "Read some size parameters off a container."
3102 self.setparameter(container, 'width')
3103 self.setparameter(container, 'height')
3104 self.setparameter(container, 'scale')
3105 self.checkvalidheight(container)
3106 return self
3108 def setparameter(self, container, name):
3109 "Read a size parameter off a container, and set it if present."
3110 value = container.getparameter(name)
3111 self.setvalue(name, value)
3113 def setvalue(self, name, value):
3114 "Set the value of a parameter name, only if it's valid."
3115 value = self.processparameter(value)
3116 if value:
3117 setattr(self, name, value)
3119 def checkvalidheight(self, container):
3120 "Check if the height parameter is valid; otherwise erase it."
3121 heightspecial = container.getparameter('height_special')
3122 if self.height and self.extractnumber(self.height) == '1' and heightspecial == 'totalheight':
3123 self.height = None
3125 def processparameter(self, value):
3126 "Do the full processing on a parameter."
3127 if not value:
3128 return None
3129 if self.extractnumber(value) == '0':
3130 return None
3131 for ignored in StyleConfig.size['ignoredtexts']:
3132 if ignored in value:
3133 value = value.replace(ignored, '')
3134 return value
3136 def extractnumber(self, text):
3137 "Extract the first number in the given text."
3138 result = ''
3139 decimal = False
3140 for char in text:
3141 if char.isdigit():
3142 result += char
3143 elif char == '.' and not decimal:
3144 result += char
3145 decimal = True
3146 else:
3147 return result
3148 return result
3150 def checkimage(self, width, height):
3151 "Check image dimensions, set them if possible."
3152 if width:
3153 self.maxwidth = unicode(width) + 'px'
3154 if self.scale and not self.width:
3155 self.width = self.scalevalue(width)
3156 if height:
3157 self.maxheight = unicode(height) + 'px'
3158 if self.scale and not self.height:
3159 self.height = self.scalevalue(height)
3160 if self.width and not self.height:
3161 self.height = 'auto'
3162 if self.height and not self.width:
3163 self.width = 'auto'
3165 def scalevalue(self, value):
3166 "Scale the value according to the image scale and return it as unicode."
3167 scaled = value * int(self.scale) / 100
3168 return unicode(int(scaled)) + 'px'
3170 def removepercentwidth(self):
3171 "Remove percent width if present, to set it at the figure level."
3172 if not self.width:
3173 return None
3174 if not '%' in self.width:
3175 return None
3176 width = self.width
3177 self.width = None
3178 if self.height == 'auto':
3179 self.height = None
3180 return width
3182 def addstyle(self, container):
3183 "Add the proper style attribute to the output tag."
3184 if not isinstance(container.output, TaggedOutput):
3185 Trace.error('No tag to add style, in ' + unicode(container))
3186 if not self.width and not self.height and not self.maxwidth and not self.maxheight:
3187 # nothing to see here; move along
3188 return
3189 tag = ' style="'
3190 tag += self.styleparameter('width')
3191 tag += self.styleparameter('maxwidth')
3192 tag += self.styleparameter('height')
3193 tag += self.styleparameter('maxheight')
3194 if tag[-1] == ' ':
3195 tag = tag[:-1]
3196 tag += '"'
3197 container.output.tag += tag
3199 def styleparameter(self, name):
3200 "Get the style for a single parameter."
3201 value = getattr(self, name)
3202 if value:
3203 return name.replace('max', 'max-') + ': ' + value + '; '
3204 return ''
3208 class QuoteContainer(Container):
3209 "A container for a pretty quote"
3211 def __init__(self):
3212 self.parser = BoundedParser()
3213 self.output = FixedOutput()
3215 def process(self):
3216 "Process contents"
3217 self.type = self.header[2]
3218 if not self.type in StyleConfig.quotes:
3219 Trace.error('Quote type ' + self.type + ' not found')
3220 self.html = ['"']
3221 return
3222 self.html = [StyleConfig.quotes[self.type]]
3224 class LyXLine(Container):
3225 "A Lyx line"
3227 def __init__(self):
3228 self.parser = LoneCommand()
3229 self.output = FixedOutput()
3231 def process(self):
3232 self.html = ['<hr class="line" />']
3234 class EmphaticText(TaggedText):
3235 "Text with emphatic mode"
3237 def process(self):
3238 self.output.tag = 'i'
3240 class ShapedText(TaggedText):
3241 "Text shaped (italic, slanted)"
3243 def process(self):
3244 self.type = self.header[1]
3245 if not self.type in TagConfig.shaped:
3246 Trace.error('Unrecognized shape ' + self.header[1])
3247 self.output.tag = 'span'
3248 return
3249 self.output.tag = TagConfig.shaped[self.type]
3251 class VersalitasText(TaggedText):
3252 "Text in versalitas"
3254 def process(self):
3255 self.output.tag = 'span class="versalitas"'
3257 class ColorText(TaggedText):
3258 "Colored text"
3260 def process(self):
3261 self.color = self.header[1]
3262 self.output.tag = 'span class="' + self.color + '"'
3264 class SizeText(TaggedText):
3265 "Sized text"
3267 def process(self):
3268 self.size = self.header[1]
3269 self.output.tag = 'span class="' + self.size + '"'
3271 class BoldText(TaggedText):
3272 "Bold text"
3274 def process(self):
3275 self.output.tag = 'b'
3277 class TextFamily(TaggedText):
3278 "A bit of text from a different family"
3280 def process(self):
3281 "Parse the type of family"
3282 self.type = self.header[1]
3283 if not self.type in TagConfig.family:
3284 Trace.error('Unrecognized family ' + type)
3285 self.output.tag = 'span'
3286 return
3287 self.output.tag = TagConfig.family[self.type]
3289 class Hfill(TaggedText):
3290 "Horizontall fill"
3292 def process(self):
3293 self.output.tag = 'span class="hfill"'
3295 class BarredText(TaggedText):
3296 "Text with a bar somewhere"
3298 def process(self):
3299 "Parse the type of bar"
3300 self.type = self.header[1]
3301 if not self.type in TagConfig.barred:
3302 Trace.error('Unknown bar type ' + self.type)
3303 self.output.tag = 'span'
3304 return
3305 self.output.tag = TagConfig.barred[self.type]
3307 class LangLine(BlackBox):
3308 "A line with language information"
3310 def process(self):
3311 self.lang = self.header[1]
3313 class InsetLength(BlackBox):
3314 "A length measure inside an inset."
3316 def process(self):
3317 self.length = self.header[1]
3319 class Space(Container):
3320 "A space of several types"
3322 def __init__(self):
3323 self.parser = InsetParser()
3324 self.output = FixedOutput()
3326 def process(self):
3327 self.type = self.header[2]
3328 if self.type not in StyleConfig.hspaces:
3329 Trace.error('Unknown space type ' + self.type)
3330 self.html = [' ']
3331 return
3332 self.html = [StyleConfig.hspaces[self.type]]
3333 length = self.getlength()
3334 if not length:
3335 return
3336 self.output = TaggedOutput().settag('span class="hspace"', False)
3337 ContainerSize().set(length).addstyle(self)
3339 def getlength(self):
3340 "Get the space length from the contents or parameters."
3341 if len(self.contents) == 0 or not isinstance(self.contents[0], InsetLength):
3342 return None
3343 return self.contents[0].length
3345 class VerticalSpace(Container):
3346 "An inset that contains a vertical space."
3348 def __init__(self):
3349 self.parser = InsetParser()
3350 self.output = FixedOutput()
3352 def process(self):
3353 "Set the correct tag"
3354 self.type = self.header[2]
3355 if self.type not in StyleConfig.vspaces:
3356 self.output = TaggedOutput().settag('div class="vspace" style="height: ' + self.type + ';"', True)
3357 return
3358 self.html = [StyleConfig.vspaces[self.type]]
3360 class Align(Container):
3361 "Bit of aligned text"
3363 def __init__(self):
3364 self.parser = ExcludingParser()
3365 self.output = TaggedOutput().setbreaklines(True)
3367 def process(self):
3368 self.output.tag = 'div class="' + self.header[1] + '"'
3370 class Newline(Container):
3371 "A newline"
3373 def __init__(self):
3374 self.parser = LoneCommand()
3375 self.output = FixedOutput()
3377 def process(self):
3378 "Process contents"
3379 self.html = ['<br/>\n']
3381 class NewPage(Newline):
3382 "A new page"
3384 def process(self):
3385 "Process contents"
3386 self.html = ['<p><br/>\n</p>\n']
3388 class Separator(Container):
3389 "A separator string which is not extracted by extracttext()."
3391 def __init__(self, constant):
3392 self.output = FixedOutput()
3393 self.contents = []
3394 self.html = [constant]
3396 class StartAppendix(BlackBox):
3397 "Mark to start an appendix here."
3398 "From this point on, all chapters become appendices."
3400 def process(self):
3401 "Activate the special numbering scheme for appendices, using letters."
3402 NumberGenerator.generator.startappendix()
3404 class ERT(Container):
3405 "Evil Red Text"
3407 def __init__(self):
3408 self.parser = InsetParser()
3409 self.output = EmptyOutput()
3416 class Link(Container):
3417 "A link to another part of the document"
3419 anchor = None
3420 url = None
3421 type = None
3422 page = None
3423 target = None
3424 destination = None
3425 title = None
3427 def __init__(self):
3428 "Initialize the link, add target if configured."
3429 self.contents = []
3430 self.parser = InsetParser()
3431 self.output = LinkOutput()
3432 if Options.target:
3433 self.target = Options.target
3435 def complete(self, text, anchor = None, url = None, type = None, title = None):
3436 "Complete the link."
3437 self.contents = [Constant(text)]
3438 if anchor:
3439 self.anchor = anchor
3440 if url:
3441 self.url = url
3442 if type:
3443 self.type = type
3444 if title:
3445 self.title = title
3446 return self
3448 def computedestination(self):
3449 "Use the destination link to fill in the destination URL."
3450 if not self.destination:
3451 return
3452 self.url = ''
3453 if self.destination.anchor:
3454 self.url = '#' + self.destination.anchor
3455 if self.destination.page:
3456 self.url = self.destination.page + self.url
3458 def setmutualdestination(self, destination):
3459 "Set another link as destination, and set its destination to this one."
3460 self.destination = destination
3461 destination.destination = self
3463 def __unicode__(self):
3464 "Return a printable representation."
3465 result = 'Link'
3466 if self.anchor:
3467 result += ' #' + self.anchor
3468 if self.url:
3469 result += ' to ' + self.url
3470 return result
3472 class URL(Link):
3473 "A clickable URL"
3475 def process(self):
3476 "Read URL from parameters"
3477 target = self.escape(self.getparameter('target'))
3478 self.url = target
3479 type = self.getparameter('type')
3480 if type:
3481 self.url = self.escape(type) + target
3482 name = self.getparameter('name')
3483 if not name:
3484 name = target
3485 self.contents = [Constant(name)]
3487 class FlexURL(URL):
3488 "A flexible URL"
3490 def process(self):
3491 "Read URL from contents"
3492 self.url = self.extracttext()
3494 class LinkOutput(ContainerOutput):
3495 "A link pointing to some destination"
3496 "Or an anchor (destination)"
3498 def gethtml(self, link):
3499 "Get the HTML code for the link"
3500 type = link.__class__.__name__
3501 if link.type:
3502 type = link.type
3503 tag = 'a class="' + type + '"'
3504 if link.anchor:
3505 tag += ' name="' + link.anchor + '"'
3506 if link.destination:
3507 link.computedestination()
3508 if link.url:
3509 tag += ' href="' + link.url + '"'
3510 if link.target:
3511 tag += ' target="' + link.target + '"'
3512 if link.title:
3513 tag += ' title="' + link.title + '"'
3514 return TaggedOutput().settag(tag).gethtml(link)
3520 class Postprocessor(object):
3521 "Postprocess a container keeping some context"
3523 stages = []
3525 def __init__(self):
3526 self.stages = StageDict(Postprocessor.stages, self)
3527 self.current = None
3528 self.last = None
3530 def postprocess(self, next):
3531 "Postprocess a container and its contents."
3532 self.postrecursive(self.current)
3533 result = self.postcurrent(next)
3534 self.last = self.current
3535 self.current = next
3536 return result
3538 def postrecursive(self, container):
3539 "Postprocess the container contents recursively"
3540 if not hasattr(container, 'contents'):
3541 return
3542 if len(container.contents) == 0:
3543 return
3544 if hasattr(container, 'postprocess'):
3545 if not container.postprocess:
3546 return
3547 postprocessor = Postprocessor()
3548 contents = []
3549 for element in container.contents:
3550 post = postprocessor.postprocess(element)
3551 if post:
3552 contents.append(post)
3553 # two rounds to empty the pipeline
3554 for i in range(2):
3555 post = postprocessor.postprocess(None)
3556 if post:
3557 contents.append(post)
3558 container.contents = contents
3560 def postcurrent(self, next):
3561 "Postprocess the current element taking into account next and last."
3562 stage = self.stages.getstage(self.current)
3563 if not stage:
3564 return self.current
3565 return stage.postprocess(self.last, self.current, next)
3567 class StageDict(object):
3568 "A dictionary of stages corresponding to classes"
3570 def __init__(self, classes, postprocessor):
3571 "Instantiate an element from each class and store as a dictionary"
3572 instances = self.instantiate(classes, postprocessor)
3573 self.stagedict = dict([(x.processedclass, x) for x in instances])
3575 def instantiate(self, classes, postprocessor):
3576 "Instantiate an element from each class"
3577 stages = [x.__new__(x) for x in classes]
3578 for element in stages:
3579 element.__init__()
3580 element.postprocessor = postprocessor
3581 return stages
3583 def getstage(self, element):
3584 "Get the stage for a given element, if the type is in the dict"
3585 if not element.__class__ in self.stagedict:
3586 return None
3587 return self.stagedict[element.__class__]
3591 class Label(Link):
3592 "A label to be referenced"
3594 names = dict()
3595 lastlayout = None
3597 def __init__(self):
3598 Link.__init__(self)
3599 self.lastnumbered = None
3601 def process(self):
3602 "Process a label container."
3603 key = self.getparameter('name')
3604 self.create(' ', key)
3605 self.lastnumbered = Label.lastlayout
3607 def create(self, text, key, type = 'Label'):
3608 "Create the label for a given key."
3609 self.key = key
3610 self.complete(text, anchor = key, type = type)
3611 Label.names[key] = self
3612 if key in Reference.references:
3613 for reference in Reference.references[key]:
3614 reference.destination = self
3615 return self
3617 def labelnumber(self):
3618 "Get the number for the latest numbered container seen."
3619 numbered = self.numbered(self)
3620 if numbered and numbered.partkey and numbered.partkey.number:
3621 return numbered.partkey.number
3622 return ''
3624 def numbered(self, container):
3625 "Get the numbered container for the label."
3626 if container.partkey:
3627 return container
3628 if not container.parent:
3629 if self.lastnumbered:
3630 return self.lastnumbered
3631 return None
3632 return self.numbered(container.parent)
3634 def __unicode__(self):
3635 "Return a printable representation."
3636 if not hasattr(self, 'key'):
3637 return 'Unnamed label'
3638 return 'Label ' + self.key
3640 class Reference(Link):
3641 "A reference to a label."
3643 references = dict()
3644 formats = {
3645 'ref':u'@↕', 'eqref':u'(@↕)', 'pageref':u'#↕',
3646 'vref':u'@on-page#↕'
3648 key = 'none'
3650 def process(self):
3651 "Read the reference and set the arrow."
3652 self.key = self.getparameter('reference')
3653 if self.key in Label.names:
3654 self.direction = u'↑'
3655 label = Label.names[self.key]
3656 else:
3657 self.direction = u'↓'
3658 label = Label().complete(' ', self.key, 'preref')
3659 self.destination = label
3660 self.format()
3661 if not self.key in Reference.references:
3662 Reference.references[self.key] = []
3663 Reference.references[self.key].append(self)
3665 def format(self):
3666 "Format the reference contents."
3667 formatkey = self.getparameter('LatexCommand')
3668 if not formatkey:
3669 formatkey = 'ref'
3670 if not formatkey in self.formats:
3671 Trace.error('Unknown reference format ' + formatkey)
3672 formatstring = u'↕'
3673 else:
3674 formatstring = self.formats[formatkey]
3675 formatstring = formatstring.replace(u'↕', self.direction)
3676 formatstring = formatstring.replace('@', self.destination.labelnumber())
3677 formatstring = formatstring.replace('#', '1')
3678 formatstring = formatstring.replace('on-page', Translator.translate('on-page'))
3679 self.contents = [Constant(formatstring)]
3681 def __unicode__(self):
3682 "Return a printable representation."
3683 return 'Reference ' + self.key
3687 class FormulaCommand(FormulaBit):
3688 "A LaTeX command inside a formula"
3690 types = []
3691 start = FormulaConfig.starts['command']
3693 def detect(self, pos):
3694 "Find the current command"
3695 return pos.checkfor(FormulaCommand.start)
3697 def parsebit(self, pos):
3698 "Parse the command"
3699 command = self.extractcommand(pos)
3700 for type in FormulaCommand.types:
3701 if command in type.commandmap:
3702 newbit = self.factory.create(type)
3703 newbit.setcommand(command)
3704 newbit.parsebit(pos)
3705 self.add(newbit)
3706 return newbit
3707 if not self.factory.defining:
3708 Trace.error('Unknown command ' + command)
3709 self.output = TaggedOutput().settag('span class="unknown"')
3710 self.add(FormulaConstant(command))
3711 return None
3713 def extractcommand(self, pos):
3714 "Extract the command from the current position"
3715 if not pos.checkskip(FormulaCommand.start):
3716 Trace.error('Missing command start ' + start)
3717 return
3718 if pos.current().isalpha():
3719 # alpha command
3720 command = FormulaCommand.start + pos.globalpha()
3721 # skip mark of short command
3722 pos.checkskip('*')
3723 return command
3724 # symbol command
3725 return FormulaCommand.start + pos.skipcurrent()
3727 class CommandBit(FormulaCommand):
3728 "A formula bit that includes a command"
3730 def setcommand(self, command):
3731 "Set the command in the bit"
3732 self.command = command
3733 self.original += command
3734 self.translated = self.commandmap[self.command]
3736 def parseparameter(self, pos):
3737 "Parse a parameter at the current position"
3738 if not self.factory.detectany(pos):
3739 Trace.error('No parameter found at: ' + pos.identifier())
3740 return None
3741 parameter = self.factory.parseany(pos)
3742 self.add(parameter)
3743 return parameter
3745 def parsesquare(self, pos):
3746 "Parse a square bracket"
3747 if not self.factory.detecttype(SquareBracket, pos):
3748 return None
3749 bracket = self.factory.parsetype(SquareBracket, pos)
3750 self.add(bracket)
3751 return bracket
3753 def parseliteral(self, pos):
3754 "Parse a literal bracket."
3755 if not self.factory.detecttype(Bracket, pos):
3756 Trace.error('No literal parameter found at: ' + pos.identifier())
3757 return None
3758 bracket = Bracket().setfactory(self.factory)
3759 self.add(bracket.parseliteral(pos))
3760 return bracket.literal
3762 def parsesquareliteral(self, pos):
3763 "Parse a square bracket literally."
3764 if not self.factory.detecttype(SquareBracket, pos):
3765 return None
3766 bracket = SquareBracket().setfactory(self.factory)
3767 self.add(bracket.parseliteral(pos))
3768 return bracket.literal
3770 class EmptyCommand(CommandBit):
3771 "An empty command (without parameters)"
3773 commandmap = FormulaConfig.commands
3775 def parsebit(self, pos):
3776 "Parse a command without parameters"
3777 self.contents = [FormulaConstant(self.translated)]
3779 class AlphaCommand(EmptyCommand):
3780 "A command without paramters whose result is alphabetical"
3782 commandmap = FormulaConfig.alphacommands
3784 def parsebit(self, pos):
3785 "Parse the command and set type to alpha"
3786 EmptyCommand.parsebit(self, pos)
3787 self.type = 'alpha'
3789 class OneParamFunction(CommandBit):
3790 "A function of one parameter"
3792 commandmap = FormulaConfig.onefunctions
3794 def parsebit(self, pos):
3795 "Parse a function with one parameter"
3796 self.output = TaggedOutput().settag(self.translated)
3797 self.parseparameter(pos)
3798 self.simplifyifpossible()
3800 def simplifyifpossible(self):
3801 "Try to simplify to a single character."
3802 if self.original in self.commandmap:
3803 self.output = FixedOutput()
3804 self.html = [self.commandmap[self.original]]
3806 class SymbolFunction(CommandBit):
3807 "Find a function which is represented by a symbol (like _ or ^)"
3809 commandmap = FormulaConfig.symbolfunctions
3811 def detect(self, pos):
3812 "Find the symbol"
3813 return pos.current() in SymbolFunction.commandmap
3815 def parsebit(self, pos):
3816 "Parse the symbol"
3817 self.setcommand(pos.current())
3818 pos.skip(self.command)
3819 self.output = TaggedOutput().settag(self.translated)
3820 self.parseparameter(pos)
3822 class TextFunction(CommandBit):
3823 "A function where parameters are read as text."
3825 commandmap = FormulaConfig.textfunctions
3827 def parsebit(self, pos):
3828 "Parse a text parameter"
3829 self.output = TaggedOutput().settag(self.translated)
3830 if not self.factory.detecttype(Bracket, pos):
3831 Trace.error('No parameter for ' + unicode(self))
3832 bracket = Bracket().setfactory(self.factory).parsetext(pos)
3833 self.add(bracket)
3835 def process(self):
3836 "Set the type to font"
3837 self.type = 'font'
3839 class LabelFunction(CommandBit):
3840 "A function that acts as a label"
3842 commandmap = FormulaConfig.labelfunctions
3844 def parsebit(self, pos):
3845 "Parse a literal parameter"
3846 self.key = self.parseliteral(pos)
3848 def process(self):
3849 "Add an anchor with the label contents."
3850 self.type = 'font'
3851 self.label = Label().create(' ', self.key, type = 'eqnumber')
3852 self.contents = [self.label]
3853 # store as a Label so we know it's been seen
3854 Label.names[self.key] = self.label
3856 class FontFunction(OneParamFunction):
3857 "A function of one parameter that changes the font"
3859 commandmap = FormulaConfig.fontfunctions
3861 def process(self):
3862 "Simplify if possible using a single character."
3863 self.type = 'font'
3864 self.simplifyifpossible()
3866 class CombiningFunction(OneParamFunction):
3868 commandmap = FormulaConfig.combiningfunctions
3870 def parsebit(self, pos):
3871 "Parse a combining function."
3872 self.type = 'alpha'
3873 combining = self.translated
3874 parameter = self.parseparameter(pos)
3875 if len(parameter.extracttext()) != 1:
3876 Trace.error('Applying combining function to invalid string ' + parameter.extracttext())
3877 self.contents.append(Constant(combining))
3879 class DecoratingFunction(OneParamFunction):
3880 "A function that decorates some bit of text"
3882 commandmap = FormulaConfig.decoratingfunctions
3884 def parsebit(self, pos):
3885 "Parse a decorating function"
3886 self.type = 'alpha'
3887 symbol = self.translated
3888 self.symbol = TaggedBit().constant(symbol, 'span class="symbolover"')
3889 self.parameter = self.parseparameter(pos)
3890 self.output = TaggedOutput().settag('span class="withsymbol"')
3891 self.contents.insert(0, self.symbol)
3892 self.parameter.output = TaggedOutput().settag('span class="undersymbol"')
3893 self.simplifyifpossible()
3895 FormulaFactory.types += [FormulaCommand, SymbolFunction]
3896 FormulaCommand.types = [
3897 EmptyCommand, AlphaCommand, OneParamFunction, DecoratingFunction,
3898 FontFunction, LabelFunction, TextFunction, CombiningFunction,
3906 class ParameterDefinition(object):
3907 "The definition of a parameter in a hybrid function."
3908 "[] parameters are optional, {} parameters are mandatory."
3909 "Each parameter has a one-character name, like {$1} or {$p}."
3910 "A parameter that ends in ! like {$p!} is a literal."
3911 "Example: [$1]{$p!} reads an optional parameter $1 and a literal mandatory parameter p."
3913 parambrackets = [('[', ']'), ('{', '}')]
3915 def __init__(self):
3916 self.name = None
3917 self.literal = False
3918 self.optional = False
3919 self.value = None
3920 self.literalvalue = None
3922 def parse(self, pos):
3923 "Parse a parameter definition: [$0], {$x}, {$1!}..."
3924 for (opening, closing) in ParameterDefinition.parambrackets:
3925 if pos.checkskip(opening):
3926 if opening == '[':
3927 self.optional = True
3928 if not pos.checkskip('$'):
3929 Trace.error('Wrong parameter name ' + pos.current())
3930 return None
3931 self.name = pos.skipcurrent()
3932 if pos.checkskip('!'):
3933 self.literal = True
3934 if not pos.checkskip(closing):
3935 Trace.error('Wrong parameter closing ' + pos.skipcurrent())
3936 return None
3937 return self
3938 Trace.error('Wrong character in parameter template: ' + pos.skipcurrent())
3939 return None
3941 def read(self, pos, function):
3942 "Read the parameter itself using the definition."
3943 if self.literal:
3944 if self.optional:
3945 self.literalvalue = function.parsesquareliteral(pos)
3946 else:
3947 self.literalvalue = function.parseliteral(pos)
3948 if self.literalvalue:
3949 self.value = FormulaConstant(self.literalvalue)
3950 elif self.optional:
3951 self.value = function.parsesquare(pos)
3952 else:
3953 self.value = function.parseparameter(pos)
3955 def __unicode__(self):
3956 "Return a printable representation."
3957 result = 'param ' + self.name
3958 if self.value:
3959 result += ': ' + unicode(self.value)
3960 else:
3961 result += ' (empty)'
3962 return result
3964 class ParameterFunction(CommandBit):
3965 "A function with a variable number of parameters defined in a template."
3966 "The parameters are defined as a parameter definition."
3968 def readparams(self, readtemplate, pos):
3969 "Read the params according to the template."
3970 self.params = dict()
3971 for paramdef in self.paramdefs(readtemplate):
3972 paramdef.read(pos, self)
3973 self.params['$' + paramdef.name] = paramdef
3975 def paramdefs(self, readtemplate):
3976 "Read each param definition in the template"
3977 pos = TextPosition(readtemplate)
3978 while not pos.finished():
3979 paramdef = ParameterDefinition().parse(pos)
3980 if paramdef:
3981 yield paramdef
3983 def getparam(self, name):
3984 "Get a parameter as parsed."
3985 if not name in self.params:
3986 return None
3987 return self.params[name]
3989 def getvalue(self, name):
3990 "Get the value of a parameter."
3991 return self.getparam(name).value
3993 def getliteralvalue(self, name):
3994 "Get the literal value of a parameter."
3995 param = self.getparam(name)
3996 if not param or not param.literalvalue:
3997 return None
3998 return param.literalvalue
4000 def getintvalue(self, name):
4001 "Get the value of a literal parameter as an int."
4002 value = self.getliteralvalue(name)
4003 if not value:
4004 return 0
4005 return int(value)
4007 class HybridFunction(ParameterFunction):
4009 A parameter function where the output is also defined using a template.
4010 The template can use a number of functions; each function has an associated tag.
4011 Example: [f0{$1},span class="fbox"] defines a function f0 which corresponds to
4012 a span of class fbox, yielding <span class="fbox">$1</span>.
4013 Literal parameters can be used in tags definitions: [f0{$1},span style="color: $p;"]
4014 yields <span style="color: $p;">$1</span>, where $p is a literal parameter.
4017 commandmap = FormulaConfig.hybridfunctions
4019 def parsebit(self, pos):
4020 "Parse a function with [] and {} parameters"
4021 readtemplate = self.translated[0]
4022 writetemplate = self.translated[1]
4023 self.readparams(readtemplate, pos)
4024 self.contents = self.writeparams(writetemplate)
4026 def writeparams(self, writetemplate):
4027 "Write all params according to the template"
4028 return self.writepos(TextPosition(writetemplate))
4030 def writepos(self, pos):
4031 "Write all params as read in the parse position."
4032 result = []
4033 while not pos.finished():
4034 if pos.checkskip('$'):
4035 param = self.writeparam(pos)
4036 if param:
4037 result.append(param)
4038 elif pos.checkskip('f'):
4039 function = self.writefunction(pos)
4040 if function:
4041 result.append(function)
4042 else:
4043 result.append(FormulaConstant(pos.skipcurrent()))
4044 return result
4046 def writeparam(self, pos):
4047 "Write a single param of the form $0, $x..."
4048 name = '$' + pos.skipcurrent()
4049 if not name in self.params:
4050 Trace.error('Unknown parameter ' + name)
4051 return None
4052 if not self.params[name]:
4053 return None
4054 if pos.checkskip('.'):
4055 self.params[name].value.type = pos.globalpha()
4056 return self.params[name].value
4058 def writefunction(self, pos):
4059 "Write a single function f0,...,fn."
4060 tag = self.readtag(pos)
4061 if not tag:
4062 return None
4063 if not pos.checkskip('{'):
4064 Trace.error('Function should be defined in {}')
4065 return None
4066 pos.pushending('}')
4067 contents = self.writepos(pos)
4068 pos.popending()
4069 if len(contents) == 0:
4070 return None
4071 function = TaggedBit().complete(contents, tag)
4072 function.type = None
4073 return function
4075 def readtag(self, pos):
4076 "Get the tag corresponding to the given index. Does parameter substitution."
4077 if not pos.current().isdigit():
4078 Trace.error('Function should be f0,...,f9: f' + pos.current())
4079 return None
4080 index = int(pos.skipcurrent())
4081 if 2 + index > len(self.translated):
4082 Trace.error('Function f' + unicode(index) + ' is not defined')
4083 return None
4084 tag = self.translated[2 + index]
4085 if not '$' in tag:
4086 return tag
4087 for variable in self.params:
4088 if variable in tag:
4089 param = self.params[variable]
4090 if not param.literal:
4091 Trace.error('Parameters in tag ' + tag + ' should be literal: {' + variable + '!}')
4092 continue
4093 if param.literalvalue:
4094 value = param.literalvalue
4095 else:
4096 value = ''
4097 tag = tag.replace(variable, value)
4098 return tag
4100 FormulaCommand.types += [HybridFunction]
4107 class FormulaEquation(CommandBit):
4108 "A simple numbered equation."
4110 piece = 'equation'
4112 def parsebit(self, pos):
4113 "Parse the array"
4114 self.output = ContentsOutput()
4115 self.add(self.factory.parsetype(WholeFormula, pos))
4117 class FormulaCell(FormulaCommand):
4118 "An array cell inside a row"
4120 def setalignment(self, alignment):
4121 self.alignment = alignment
4122 self.output = TaggedOutput().settag('td class="formula-' + alignment +'"', True)
4123 return self
4125 def parsebit(self, pos):
4126 self.factory.clearignored(pos)
4127 if pos.finished():
4128 return
4129 if not self.factory.detecttype(WholeFormula, pos):
4130 Trace.error('Unexpected end of array cell at ' + pos.identifier())
4131 pos.skip(pos.current())
4132 return
4133 self.add(self.factory.parsetype(WholeFormula, pos))
4135 class FormulaRow(FormulaCommand):
4136 "An array row inside an array"
4138 cellseparator = FormulaConfig.array['cellseparator']
4140 def setalignments(self, alignments):
4141 self.alignments = alignments
4142 self.output = TaggedOutput().settag('tr', True)
4143 return self
4145 def parsebit(self, pos):
4146 "Parse a whole row"
4147 index = 0
4148 pos.pushending(self.cellseparator, optional=True)
4149 while not pos.finished():
4150 alignment = self.alignments[index % len(self.alignments)]
4151 cell = self.factory.create(FormulaCell).setalignment(alignment)
4152 cell.parsebit(pos)
4153 self.add(cell)
4154 index += 1
4155 pos.checkskip(self.cellseparator)
4156 if len(self.contents) == 0:
4157 self.output = EmptyOutput()
4159 class MultiRowFormula(CommandBit):
4160 "A formula with multiple rows."
4162 def parserows(self, pos):
4163 "Parse all rows, finish when no more row ends"
4164 for row in self.iteraterows(pos):
4165 row.parsebit(pos)
4166 self.add(row)
4168 def iteraterows(self, pos):
4169 "Iterate over all rows, end when no more row ends"
4170 rowseparator = FormulaConfig.array['rowseparator']
4171 while True:
4172 pos.pushending(rowseparator, True)
4173 row = self.factory.create(FormulaRow)
4174 yield row.setalignments(self.alignments)
4175 if pos.checkfor(rowseparator):
4176 self.original += pos.popending(rowseparator)
4177 else:
4178 return
4180 class FormulaArray(MultiRowFormula):
4181 "An array within a formula"
4183 piece = 'array'
4185 def parsebit(self, pos):
4186 "Parse the array"
4187 self.output = TaggedOutput().settag('table class="formula"', True)
4188 self.parsealignments(pos)
4189 self.parserows(pos)
4191 def parsealignments(self, pos):
4192 "Parse the different alignments"
4193 # vertical
4194 self.valign = 'c'
4195 literal = self.parsesquareliteral(pos)
4196 if literal:
4197 self.valign = literal
4198 # horizontal
4199 literal = self.parseliteral(pos)
4200 self.alignments = []
4201 for l in literal:
4202 self.alignments.append(l)
4204 class FormulaCases(MultiRowFormula):
4205 "A cases statement"
4207 piece = 'cases'
4209 def parsebit(self, pos):
4210 "Parse the cases"
4211 self.output = TaggedOutput().settag('table class="cases"', True)
4212 self.alignments = ['l', 'l']
4213 self.parserows(pos)
4215 class EquationEnvironment(MultiRowFormula):
4216 "A \\begin{}...\\end equation environment with rows and cells."
4218 def parsebit(self, pos):
4219 "Parse the whole environment."
4220 self.output = TaggedOutput().settag('table class="environment"', True)
4221 environment = self.piece.replace('*', '')
4222 if environment in FormulaConfig.environments:
4223 self.alignments = FormulaConfig.environments[environment]
4224 else:
4225 Trace.error('Unknown equation environment ' + self.piece)
4226 self.alignments = ['l']
4227 self.parserows(pos)
4229 class BeginCommand(CommandBit):
4230 "A \\begin{}...\end command and what it entails (array, cases, aligned)"
4232 commandmap = {FormulaConfig.array['begin']:''}
4234 types = [FormulaEquation, FormulaArray, FormulaCases]
4236 def parsebit(self, pos):
4237 "Parse the begin command"
4238 command = self.parseliteral(pos)
4239 bit = self.findbit(command)
4240 ending = FormulaConfig.array['end'] + '{' + command + '}'
4241 pos.pushending(ending)
4242 bit.parsebit(pos)
4243 self.add(bit)
4244 self.original += pos.popending(ending)
4246 def findbit(self, piece):
4247 "Find the command bit corresponding to the \\begin{piece}"
4248 for type in BeginCommand.types:
4249 if type.piece == piece:
4250 return self.factory.create(type)
4251 bit = self.factory.create(EquationEnvironment)
4252 bit.piece = piece
4253 return bit
4255 FormulaCommand.types += [BeginCommand]
4265 class HeaderParser(Parser):
4266 "Parses the LyX header"
4268 def parse(self, reader):
4269 "Parse header parameters into a dictionary, return the preamble."
4270 contents = []
4271 self.parseending(reader, lambda: self.parseline(reader, contents))
4272 # skip last line
4273 reader.nextline()
4274 return contents
4276 def parseline(self, reader, contents):
4277 "Parse a single line as a parameter or as a start"
4278 line = reader.currentline()
4279 if line.startswith(HeaderConfig.parameters['branch']):
4280 self.parsebranch(reader)
4281 return
4282 elif line.startswith(HeaderConfig.parameters['lstset']):
4283 LstParser().parselstset(reader)
4284 return
4285 elif line.startswith(HeaderConfig.parameters['beginpreamble']):
4286 contents.append(self.factory.createcontainer(reader))
4287 return
4288 # no match
4289 self.parseparameter(reader)
4291 def parsebranch(self, reader):
4292 "Parse all branch definitions."
4293 branch = reader.currentline().split()[1]
4294 reader.nextline()
4295 subparser = HeaderParser().complete(HeaderConfig.parameters['endbranch'])
4296 subparser.parse(reader)
4297 options = BranchOptions(branch)
4298 for key in subparser.parameters:
4299 options.set(key, subparser.parameters[key])
4300 Options.branches[branch] = options
4302 def complete(self, ending):
4303 "Complete the parser with the given ending."
4304 self.ending = ending
4305 return self
4307 class PreambleParser(Parser):
4308 "A parser for the LyX preamble."
4310 preamble = []
4312 def parse(self, reader):
4313 "Parse the full preamble with all statements."
4314 self.ending = HeaderConfig.parameters['endpreamble']
4315 self.parseending(reader, lambda: self.parsepreambleline(reader))
4316 return []
4318 def parsepreambleline(self, reader):
4319 "Parse a single preamble line."
4320 PreambleParser.preamble.append(reader.currentline())
4321 reader.nextline()
4323 class LstParser(object):
4324 "Parse global and local lstparams."
4326 globalparams = dict()
4328 def parselstset(self, reader):
4329 "Parse a declaration of lstparams in lstset."
4330 paramtext = self.extractlstset(reader)
4331 if not '{' in paramtext:
4332 Trace.error('Missing opening bracket in lstset: ' + paramtext)
4333 return
4334 lefttext = paramtext.split('{')[1]
4335 croppedtext = lefttext[:-1]
4336 LstParser.globalparams = self.parselstparams(croppedtext)
4338 def extractlstset(self, reader):
4339 "Extract the global lstset parameters."
4340 paramtext = ''
4341 while not reader.finished():
4342 paramtext += reader.currentline()
4343 reader.nextline()
4344 if paramtext.endswith('}'):
4345 return paramtext
4346 Trace.error('Could not find end of \\lstset settings; aborting')
4348 def parsecontainer(self, container):
4349 "Parse some lstparams from a container."
4350 container.lstparams = LstParser.globalparams.copy()
4351 paramlist = container.getparameterlist('lstparams')
4352 container.lstparams.update(self.parselstparams(paramlist))
4354 def parselstparams(self, paramlist):
4355 "Process a number of lstparams from a list."
4356 paramdict = dict()
4357 for param in paramlist:
4358 if not '=' in param:
4359 if len(param.strip()) > 0:
4360 Trace.error('Invalid listing parameter ' + param)
4361 else:
4362 key, value = param.split('=', 1)
4363 paramdict[key] = value
4364 return paramdict
4369 class MathMacro(object):
4370 "A math macro: command, parameters, default values, definition."
4372 macros = dict()
4374 def __init__(self):
4375 self.newcommand = None
4376 self.parameternumber = 0
4377 self.defaults = []
4378 self.definition = None
4380 def instantiate(self):
4381 "Return an instance of the macro."
4382 return self.definition.clone()
4384 class MacroParameter(FormulaBit):
4385 "A parameter from a macro."
4387 def detect(self, pos):
4388 "Find a macro parameter: #n."
4389 return pos.checkfor('#')
4391 def parsebit(self, pos):
4392 "Parse the parameter: #n."
4393 if not pos.checkskip('#'):
4394 Trace.error('Missing parameter start #.')
4395 return
4396 self.number = int(pos.skipcurrent())
4397 self.original = '#' + unicode(self.number)
4398 self.contents = [TaggedBit().constant('#' + unicode(self.number), 'span class="unknown"')]
4400 class DefiningFunction(ParameterFunction):
4401 "Read a function that defines a new command (a macro)."
4403 commandmap = FormulaConfig.definingfunctions
4405 def parsebit(self, pos):
4406 "Parse a function with [] and {} parameters."
4407 if self.factory.detecttype(Bracket, pos):
4408 newcommand = self.parseliteral(pos)
4409 elif self.factory.detecttype(FormulaCommand, pos):
4410 newcommand = self.factory.create(FormulaCommand).extractcommand(pos)
4411 else:
4412 Trace.error('Unknown formula bit in defining function at ' + pos.identifier())
4413 return
4414 Trace.debug('New command: ' + newcommand)
4415 template = self.translated
4416 self.factory.defining = True
4417 self.readparams(template, pos)
4418 self.factory.defining = False
4419 self.contents = []
4420 macro = MathMacro()
4421 macro.newcommand = newcommand
4422 macro.parameternumber = self.getintvalue('$n')
4423 macro.definition = self.getvalue('$d')
4424 self.extractdefaults(macro)
4425 MathMacro.macros[newcommand] = macro
4427 def extractdefaults(self, macro):
4428 "Extract the default values for existing parameters."
4429 for index in range(9):
4430 value = self.extractdefault(index + 1)
4431 if value:
4432 macro.defaults.append(value)
4433 else:
4434 return
4436 def extractdefault(self, index):
4437 "Extract the default value for parameter index."
4438 value = self.getvalue('$' + unicode(index))
4439 if not value:
4440 return None
4441 if len(value.contents) == 0:
4442 return FormulaConstant('')
4443 return value.contents[0]
4445 class MacroFunction(CommandBit):
4446 "A function that was defined using a macro."
4448 commandmap = MathMacro.macros
4450 def parsebit(self, pos):
4451 "Parse a number of input parameters."
4452 self.values = []
4453 macro = self.translated
4454 while self.factory.detecttype(Bracket, pos):
4455 self.values.append(self.parseparameter(pos))
4456 defaults = list(macro.defaults)
4457 remaining = macro.parameternumber - len(self.values) - len(defaults)
4458 if remaining > 0:
4459 self.parsenumbers(remaining, pos)
4460 while len(self.values) < macro.parameternumber and len(defaults) > 0:
4461 self.values.insert(0, defaults.pop())
4462 if len(self.values) < macro.parameternumber:
4463 Trace.error('Missing parameters in macro ' + unicode(self))
4464 self.completemacro(macro)
4466 def parsenumbers(self, remaining, pos):
4467 "Parse the remaining parameters as a running number."
4468 "For example, 12 would be {1}{2}."
4469 if pos.finished():
4470 return
4471 if not self.factory.detecttype(FormulaNumber, pos):
4472 return
4473 number = self.factory.parsetype(FormulaNumber, pos)
4474 if not len(number.original) == remaining:
4475 self.values.append(number)
4476 return
4477 for digit in number.original:
4478 value = self.factory.create(FormulaNumber)
4479 value.add(FormulaConstant(digit))
4480 value.type = number
4481 self.values.append(value)
4483 def completemacro(self, macro):
4484 "Complete the macro with the parameters read."
4485 self.contents = [macro.instantiate()]
4486 for parameter in self.searchall(MacroParameter):
4487 index = parameter.number - 1
4488 if index >= len(self.values):
4489 return
4490 parameter.contents = [self.values[index].clone()]
4492 class FormulaMacro(Formula):
4493 "A math macro defined in an inset."
4495 def __init__(self):
4496 self.parser = MacroParser()
4497 self.output = EmptyOutput()
4499 def __unicode__(self):
4500 "Return a printable representation."
4501 return 'Math macro'
4503 FormulaFactory.types += [ MacroParameter ]
4505 FormulaCommand.types += [
4506 DefiningFunction, MacroFunction,
4511 def math2html(formula):
4512 "Convert some TeX math to HTML."
4513 factory = FormulaFactory()
4514 whole = factory.parseformula(formula)
4515 FormulaProcessor().process(whole)
4516 whole.process()
4517 return ''.join(whole.gethtml())
4519 def main():
4520 "Main function, called if invoked from the command line"
4521 if len(sys.argv) <= 1:
4522 Trace.error('Usage: math2html.py escaped_string')
4523 exit()
4524 result = math2html(sys.argv[1])
4525 Trace.message(result)
4527 if __name__ == '__main__':
4528 main()