comparison venv/lib/python2.7/site-packages/docutils/utils/math/math2html.py @ 0:d67268158946 draft

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